From 718733a6aaf2f539a49e236c235251bd4b9a60ce Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:20:34 +0000 Subject: [PATCH 1/2] tract-harvest-probe: ONNX as a consumer of the DR64 route / DT64 bind store split bind::route runs the signature match once per node and keeps only node position -> classid (sparse, versioned over the registry version and every entry); bind::resolve finds the registration by classid alone and names the failed link (Route / Class). Classids are unique, so the split gives back exactly what Registry::lookup gives: pinned for every node of 15 tiny fixtures and for the full Jina graph (7,274 routed and resolved, 346 unrouted, equal to the coverage census). The binder reads per-value facts, not the one-tile OnnxGraph, which refuses Jina's 7,620 nodes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v --- crates/tract-harvest-probe/src/bind.rs | 87 +++++++++++++++++++ crates/tract-harvest-probe/src/lib.rs | 1 + crates/tract-harvest-probe/src/plan.rs | 2 +- crates/tract-harvest-probe/tests/jina.rs | 16 ++++ crates/tract-harvest-probe/tests/tiny.rs | 102 +++++++++++++++++++++++ 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 crates/tract-harvest-probe/src/bind.rs diff --git a/crates/tract-harvest-probe/src/bind.rs b/crates/tract-harvest-probe/src/bind.rs new file mode 100644 index 000000000..967e0dd1c --- /dev/null +++ b/crates/tract-harvest-probe/src/bind.rs @@ -0,0 +1,87 @@ +//! The ONNX registry read as a consumer of the DR64 route / DT64 bind store +//! split (D-DT64-6 §1: "DR64 is direct routing … DT64 is the DTO bind +//! store"; EXO-50 drafts -08 / -09: `address --route--> classid --binding--> +//! method`). +//! +//! [`Registry::lookup`] does both halves in one call: it picks a +//! registration from the node's signature and hands back its body. Here they +//! are separated without changing either: +//! +//! - [`route`] is the binder. It runs the signature match once per node and +//! records only `node position → classid`, sparse; an unregistered node +//! has no route. This is the table DR would own. +//! - [`resolve`] is the bind-store side. It takes a routed classid and finds +//! its registration by classid alone, never by signature. This works +//! because every registration carries its own classid and no classid is +//! registered twice (`classids_are_unique` pins that). +//! +//! Nothing here is the DT64 crate or the DR route table; both are other +//! owners' (EXO-50 -05 decision). This module only shows that the ONNX +//! instance already factors into them, so adapting it is a move, not a +//! redesign. +use crate::registry::{Fact, Registration, Registry}; +use crate::source::SourceInventory; +use lance_graph_contract::class_view::ClassId; +use std::collections::{BTreeMap, HashMap}; + +/// `node position → classid`, sparse. `version` digests the registry +/// version and every entry, so a re-route is a different version. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Routes { + pub version: u64, + pub map: BTreeMap, +} + +/// Which link of `position → classid → registration` failed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Unresolved { + /// No route at this position: UNKNOWN, never "no behaviour". + Route { node: usize }, + /// Routed to a classid the bind store does not hold. + Class { node: usize, classid: ClassId }, +} + +/// Bind every node by signature once, keeping only its classid. The binder +/// reads per-value facts, not a tile: a node position is the source node +/// index, so a graph larger than one 64-node tile routes too. Initializers +/// without a fact get their protobuf dtype and dims, as in `fold`. +pub fn route(src: &SourceInventory, facts: &HashMap, registry: &Registry) -> Routes { + let facts = crate::fold::with_initializer_facts(src, facts); + let mut map = BTreeMap::new(); + for (n, node) in src.nodes.iter().enumerate() { + let args: Vec> = node.inputs.iter().map(|i| facts.get(i)).collect(); + if let Some(r) = registry.lookup(node, &args) { + map.insert(n, r.classid); + } + } + let mut parts: Vec> = vec![registry.version.to_le_bytes().to_vec()]; + parts.extend(map.iter().map(|(n, c)| { + let mut b = (*n as u64).to_le_bytes().to_vec(); + b.extend((*c as u64).to_le_bytes()); + b + })); + let refs: Vec<&[u8]> = parts.iter().map(Vec::as_slice).collect(); + Routes { + version: crate::plan::digest64(&refs), + map, + } +} + +impl Registry { + /// The registration holding `classid`; the bind-store lookup. + pub fn by_classid(&self, classid: ClassId) -> Option<&Registration> { + self.regs.iter().find(|r| r.classid == classid) + } +} + +/// `position → classid → registration`, naming the link that failed. +pub fn resolve<'r>( + routes: &Routes, + registry: &'r Registry, + node: usize, +) -> Result<&'r Registration, Unresolved> { + let &classid = routes.map.get(&node).ok_or(Unresolved::Route { node })?; + registry + .by_classid(classid) + .ok_or(Unresolved::Class { node, classid }) +} diff --git a/crates/tract-harvest-probe/src/lib.rs b/crates/tract-harvest-probe/src/lib.rs index 6ebfbff85..7db7de13a 100644 --- a/crates/tract-harvest-probe/src/lib.rs +++ b/crates/tract-harvest-probe/src/lib.rs @@ -18,6 +18,7 @@ //! //! Execution never calls Tract. No native crate depends on this crate. +pub mod bind; pub mod dr3; pub mod fold; pub mod gather; diff --git a/crates/tract-harvest-probe/src/plan.rs b/crates/tract-harvest-probe/src/plan.rs index 59e1d0460..f7a148971 100644 --- a/crates/tract-harvest-probe/src/plan.rs +++ b/crates/tract-harvest-probe/src/plan.rs @@ -44,7 +44,7 @@ pub struct Plan<'r> { frame: Frame, } -fn digest64(parts: &[&[u8]]) -> u64 { +pub(crate) fn digest64(parts: &[&[u8]]) -> u64 { let mut h = Sha256::new(); for p in parts { h.update((p.len() as u64).to_le_bytes()); diff --git a/crates/tract-harvest-probe/tests/jina.rs b/crates/tract-harvest-probe/tests/jina.rs index 750c8021f..5be24573e 100644 --- a/crates/tract-harvest-probe/tests/jina.rs +++ b/crates/tract-harvest-probe/tests/jina.rs @@ -501,6 +501,22 @@ fn full_graph_facts_and_coverage() { }; assert_eq!(tot(&fs.typed), [4238, 256, 3126]); assert_eq!(tot(&fs.merged()), [7274, 0, 346]); + // The same graph as a bind-store consumer: every registered node has a + // route to its classid and resolves through it; the rest have none. + // 7,620 nodes: more than one 64-node tile, so the binder must not need + // the one-tile `OnnxGraph`. + let routes = tract_harvest_probe::bind::route(&src, &fs.merged(), ®); + let resolved = (0..src.nodes.len()) + .filter(|&n| tract_harvest_probe::bind::resolve(&routes, ®, n).is_ok()) + .count(); + assert_eq!( + ( + routes.map.len(), + resolved, + src.nodes.len() - routes.map.len() + ), + (7274, 7274, 346) + ); } /// The real Jina rotary embedding, folded cold at `[1, 10]` from the source diff --git a/crates/tract-harvest-probe/tests/tiny.rs b/crates/tract-harvest-probe/tests/tiny.rs index 69a222008..4af3a88be 100644 --- a/crates/tract-harvest-probe/tests/tiny.rs +++ b/crates/tract-harvest-probe/tests/tiny.rs @@ -1258,3 +1258,105 @@ fn facts_of_another_model_refuse_to_bind() { let own = tract_fact_sources(&bytes("base"), &[]).unwrap(); assert!(own.merged_for(&base).is_ok()); } + +/// The bind-store lookup is by classid alone, so a classid may name one +/// registration only. +#[test] +fn classids_are_unique() { + let reg = registry(); + let mut seen = std::collections::BTreeMap::new(); + for r in ®.regs { + assert_eq!(seen.insert(r.classid, r.method), None, "{:#06x}", r.classid); + } + assert!(seen.len() > 1); +} + +const BIND_VARIANTS: &[&str] = &[ + "base", + "div3", + "erf", + "erf_sweep", + "gen2", + "i64_arith", + "i64_data", + "lstm_full", + "lstm_skip", + "matmul", + "rope", + "shape_fold", + "shape_i64", + "shape_sym", + "trig_sweep", +]; + +/// The ONNX registry splits into a route (position → classid, by signature, +/// once) and a bind-store resolve (classid → registration) that gives back +/// exactly what the one-step signature lookup gives, for every node of every +/// fixture. An unregistered node has no route and resolves as `Route`. +#[test] +fn route_then_resolve_equals_signature_lookup() { + use tract_harvest_probe::bind::{resolve, route, Unresolved}; + let reg = registry(); + let (mut routed, mut unrouted) = (0, 0); + for v in BIND_VARIANTS { + let b = bytes(v); + let src = SourceInventory::from_bytes(&b).unwrap(); + let facts = + tract_harvest_probe::fold::with_initializer_facts(&src, &tract_facts(&b, &[]).unwrap()); + let routes = route(&src, &facts, ®); + for (n, node) in src.nodes.iter().enumerate() { + let args: Vec<_> = node.inputs.iter().map(|i| facts.get(i)).collect(); + match (reg.lookup(node, &args), resolve(&routes, ®, n)) { + (Some(a), Ok(b)) => { + assert!(std::ptr::eq(a, b), "{v} node {n}"); + routed += 1; + } + (None, Err(Unresolved::Route { node })) => { + assert_eq!(node, n); + unrouted += 1; + } + (a, b) => panic!( + "{v} node {n}: lookup {:?} vs resolve {:?}", + a.map(|r| r.method), + b.map(|r| r.method) + ), + } + } + } + assert!( + routed > 50 && unrouted > 0, + "routed {routed}, unrouted {unrouted}" + ); +} + +/// A classid the bind store does not hold is its own failure, and a +/// different registry (or any re-route) is a different route version. +#[test] +fn bind_store_names_the_failed_link_and_versions_routes() { + use tract_harvest_probe::bind::{resolve, route, Unresolved}; + let reg = registry(); + let b = bytes("rope"); + let src = SourceInventory::from_bytes(&b).unwrap(); + let facts = tract_facts(&b, &[]).unwrap(); + let routes = route(&src, &facts, ®); + let (&n, &cid) = routes.map.iter().next().unwrap(); + let mut without = registry(); + without.regs.retain(|r| r.classid != cid); + assert_eq!( + resolve(&routes, &without, n).map(|r| r.classid), + Err(Unresolved::Class { + node: n, + classid: cid + }) + ); + // Re-routing against the smaller registry drops that node's route and + // moves the version. + let rerouted = route(&src, &facts, &without); + assert!(!rerouted.map.contains_key(&n)); + assert_ne!(rerouted.version, routes.version); + // Same routes, other registry version: another version too. + let mut bumped = registry(); + bumped.version += 1; + assert_ne!(route(&src, &facts, &bumped).version, routes.version); + assert_eq!(route(&src, &facts, ®), routes); +} From 96c528f5fc0be7d3b5c042ffcae680442954df24 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:21:43 +0000 Subject: [PATCH 2/2] =?UTF-8?q?board:=20D-TRACT-SHAPE-1=20bind-store=20con?= =?UTF-8?q?sumer=20(plan=20=C2=A70.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMMjKnig6MXTd41Lf2v --- .claude/board/STATUS_BOARD.md | 2 +- ...0-30-d-tract-shape-1-cold-shape-folding.md | 2 ++ ...ract-harvest-0-native-algebra-bridge-v1.md | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 6be9e5e0c..a99d88e35 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -78,7 +78,7 @@ | **D-ONNX-INTAKE-4** | S4 numeric/quantization parity for real model blockers | Queued (conditional; not authorized) | IntegerExact / bounded FP / SourceParity / CertifiedMath kept distinct | | **D-ONNX-INTAKE-5** | S5 whole-model forward pass and cost closure | Goal, not authorized | outputs vs ORT under declared tolerance; every cost stage reported | | **D-TRACT-HARVEST-0** | ONNX graph as a DR#3 graph (nodes Identity, values Property, dep = do_ ∘ read_by) resolved through a DT64 ON×WHEN×DO registry (op signature → classid → native DO); Tract cold facts; ORT (optimizer off) oracle; `crates/tract-harvest-probe` | Lab probe implemented (this PR, redesigned to the graph); plan [d-tract-harvest-0-native-algebra-bridge-v1.md](../plans/d-tract-harvest-0-native-algebra-bridge-v1.md) §0 | 26 tests, 13 disable runs red; full model 4,146/7,620 nodes WHEN-true with inference-stage facts (§0.1), 0 refused for missing facts, >64 positions untiled, first unregistered groups Unsqueeze/Shape/Gather/Concat/MatMul | -| **D-TRACT-SHAPE-1** | Cold shape-subgraph folding (Const/Shape/Symbolic/Data classes; fold through the node's own WHEN/DO; stale-input refusal), shape-op registrations (Shape, Gather i64, Unsqueeze, Concat, Reshape, Cast i64, Erf via `vserf`, scalar-Y Where via `blend_i32_scalar`), producer-checked Tract fact admission | **Shipped** #1853 + follow-up In PR (lab probe); plan [d-tract-harvest-0-native-algebra-bridge-v1.md](../plans/d-tract-harvest-0-native-algebra-bridge-v1.md) §0.2–§0.4 | #1853 merged (40 tests, 9 disable runs red; coverage 4,146/0/3,474 → 6,404/56/1,160). Follow-up **In PR**: registry v3 (int64 compares/Where, ConstantOfShape, Expand), optional-output producer test; coverage 6,581/114/925. Checked int64 follow-up **In PR** (plan §0.4): registry v4, int64 Add/Sub/Mul/Div/Range on ndarray #401; `i64_arith` folds and matches ORT; Jina 6,871/1/748, all shape-dependent Reshapes fold; remaining shape-only blockers are the rotary-embedding chain, And, one broadcast LessOrEqual. Rotary follow-up **In PR** (plan §0.5): registry v5 (Cast i64→f32/bool, MatMul f32, Transpose, Cos/Sin, And, any-broadcast compares), pinned external inv_freq; Jina rotary fold matches ORT ≤ 1 ULP; Jina 7,274/0/346, 0 shape-only nodes blocked | +| **D-TRACT-SHAPE-1** | Cold shape-subgraph folding (Const/Shape/Symbolic/Data classes; fold through the node's own WHEN/DO; stale-input refusal), shape-op registrations (Shape, Gather i64, Unsqueeze, Concat, Reshape, Cast i64, Erf via `vserf`, scalar-Y Where via `blend_i32_scalar`), producer-checked Tract fact admission | **Shipped** #1853 + follow-up In PR (lab probe); plan [d-tract-harvest-0-native-algebra-bridge-v1.md](../plans/d-tract-harvest-0-native-algebra-bridge-v1.md) §0.2–§0.4 | #1853 merged (40 tests, 9 disable runs red; coverage 4,146/0/3,474 → 6,404/56/1,160). Follow-up **In PR**: registry v3 (int64 compares/Where, ConstantOfShape, Expand), optional-output producer test; coverage 6,581/114/925. Checked int64 follow-up **In PR** (plan §0.4): registry v4, int64 Add/Sub/Mul/Div/Range on ndarray #401; `i64_arith` folds and matches ORT; Jina 6,871/1/748, all shape-dependent Reshapes fold; remaining shape-only blockers are the rotary-embedding chain, And, one broadcast LessOrEqual. Rotary follow-up **In PR** (plan §0.5): registry v5 (Cast i64→f32/bool, MatMul f32, Transpose, Cos/Sin, And, any-broadcast compares), pinned external inv_freq; Jina rotary fold matches ORT ≤ 1 ULP; Jina 7,274/0/346, 0 shape-only nodes blocked (#1858 merged). Bind-store consumer follow-up **In PR** (plan §0.6): `bind::route` / `bind::resolve` split the registry into DR64 route and DT64 bind store; equal to `lookup` on all tiny fixtures; Jina 7,274 routed+resolved / 346 unrouted | ## D-TFLP — TFLite models on the native polyfill: int8 codes as Palette256 (2026-10-25) diff --git a/.claude/board/entries/2026-10-30-d-tract-shape-1-cold-shape-folding.md b/.claude/board/entries/2026-10-30-d-tract-shape-1-cold-shape-folding.md index e07121347..00d3e9d95 100644 --- a/.claude/board/entries/2026-10-30-d-tract-shape-1-cold-shape-folding.md +++ b/.claude/board/entries/2026-10-30-d-tract-shape-1-cold-shape-folding.md @@ -19,3 +19,5 @@ Fixtures (ORT 1.29.0 `ORT_DISABLE_ALL`): shape chain bit-exact; Erf ≤ 2 ULP vs **Checked int64 (ndarray #401, plan §0.4):** registry v4 adds int64 Add/Sub/Mul/Div on `checked_binary_i64` and Range on `range_i64` (capacity 65,536); fold DO refusals now refuse the plan. The `i64_arith` chain folds and matches ORT exactly; overflow and unadmitted broadcasts refuse (zero divisor and zero Range step already refused by Tract). Initializers without an admitted fact get their protobuf dtype/dims in fold and coverage. Jina 6,871 / 1 / 748, 5,139 folded; all 58 shape-dependent Reshapes fold; 20 shape-only nodes still blocked, nearly all on the rotary-embedding chain (Cos, Sin, MatMul, Transpose, Cast; its `inv_freq` lives in the external weights), plus And and one broadcast LessOrEqual. 60 tests, 7 more disable runs red. **Rotary chain (plan §0.5):** registry v5 adds Cast int64→f32 and bool→bool, f32 MatMul (`matmul_f32_batched`), Transpose, Cos/Sin (`vscos`/`vssin`), bool And (`mask_and`), and any-broadcast int64 Equal/LessOrEqual. The rotary `inv_freq` is fetched as a SHA-256-pinned range (data gitignored, digests committed) and inlined into its Constant. The real Jina rotary fold matches ORT `ORT_DISABLE_ALL` within 1 ULP; trig ≤ 1 ULP from f64. Jina 7,274 / 0 / 346, 5,159 folded, **0 shape-only nodes blocked**; the 346 unregistered are run-time ops. 70 tests, 6 more disable runs red. OPEN: ndarray's `matmul_f32` doc claims BF16 on AMX but the word is exact f32; `amx_matmul` is x86_64-only. + +**Bind-store consumer (plan §0.6):** `bind::route` (position → classid, sparse, versioned) and `bind::resolve` (classid → registration, failed link named) split `Registry::lookup` into the DR64 route and the DT64 bind store with no registry change; classids are unique. Equal to `lookup` on every node of 15 tiny fixtures; Jina 7,274 routed and resolved, 346 unrouted (= census). The one-tile `OnnxGraph` refuses Jina's 7,620 nodes, so the binder reads facts, not the tile. 2 more disable runs red. OPEN: DT64 crate, DR route table and tile coordinates are other owners'. diff --git a/.claude/plans/d-tract-harvest-0-native-algebra-bridge-v1.md b/.claude/plans/d-tract-harvest-0-native-algebra-bridge-v1.md index c0620200f..f41fb3de2 100644 --- a/.claude/plans/d-tract-harvest-0-native-algebra-bridge-v1.md +++ b/.claude/plans/d-tract-harvest-0-native-algebra-bridge-v1.md @@ -214,6 +214,25 @@ MatMul admits a rank-2 right operand (the left's leading dims flatten into rows) Disable runs (each red): Transpose `perm` ignored; Cos computed with sin; compare broadcast removed; And broadcast ignored; manifest digest check removed; MatMul weight-row flattening dropped. The last one stayed green until the weight case was given two batches (with one batch the flattening is a no-op). 70 tests green (23 lib, 35 tiny, 9 Jina; the Jina tests need the fetch scripts). +### 0.6 ONNX as a consumer of the DR64 route / DT64 bind store (2026-10-30) + +D-DT64-6 §1 records DR64 as direct routing (position → bound target) and DT64 as the DTO bind store (classid → behaviour). The DT64 drafts on EXO-50 (-06 to -09) resolve `address → classid → method`. `Registry::lookup` does both halves in one step. `src/bind.rs` separates them without changing the registry: + +- `bind::route(src, facts, registry)` is the binder: the signature match runs once per node and only `node position → classid` is kept, sparse. `Routes::version` digests the registry version and every entry, so a re-route is a new version. +- `bind::resolve(routes, registry, node)` is the bind-store side: the registration is found by classid alone (`Registry::by_classid`), and the failed link is named (`Unresolved::Route` for an unrouted position, which is UNKNOWN; `Unresolved::Class` for a classid the store does not hold). + +This works because every registration carries its own classid and none is registered twice (`classids_are_unique`). + +**Acceptance.** +- `route_then_resolve_equals_signature_lookup`: for every node of 15 tiny fixtures, resolve returns the same registration (pointer-equal) as `lookup`; unregistered nodes resolve as `Route`. +- `bind_store_names_the_failed_link_and_versions_routes`: a removed classid resolves as `Class`; re-routing against it drops the route and moves the version; a registry version bump moves the version. +- `full_graph_facts_and_coverage` (Jina, [1,10], inv_freq inlined): **7,274 routed and resolved, 346 unrouted**, equal to the coverage census. +- Disable runs (each red): classid match loosened; registry version dropped from the route digest. + +**Finding.** `OnnxGraph::build` refuses a graph over one 64-node tile (`TileOverflow`), and Jina has 7,620 nodes. The binder therefore reads per-value facts, not the one-tile graph; a node position is the source node index. Mapping that index onto DR64 tile coordinates (and multi-tile execution) is DR's, not Tract's. + +**Not done here:** the DT64 crate, the DR route table, `K3` re-homing, OGAR classids. When those exist, `route` writes DR's table and `resolve` becomes DT64's; the equality test is the migration gate (G1 in the -06 draft, with the census now 7,274 / 0 / 346). + ## 1. The acceptance chain, demonstrated The chain under test: a source-attested ONNX subgraph is imported by Tract, admitted, lowered onto existing native methods, executed without Tract, and verified against ONNX Runtime (ORT). The chain holds for both fixtures: fixture A (tiny, synthetic) is bit-exact, and fixture B (a real model slice) is bounded. The numbers are in §4. Nothing here is certified, production or merged.