Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'.
19 changes: 19 additions & 0 deletions .claude/plans/d-tract-harvest-0-native-algebra-bridge-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions crates/tract-harvest-probe/src/bind.rs
Original file line number Diff line number Diff line change
@@ -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<usize, ClassId>,
}

/// 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<String, Fact>, 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<Option<&Fact>> = 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<u8>> = 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 })
}
1 change: 1 addition & 0 deletions crates/tract-harvest-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion crates/tract-harvest-probe/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
16 changes: 16 additions & 0 deletions crates/tract-harvest-probe/tests/jina.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(), &reg);
let resolved = (0..src.nodes.len())
.filter(|&n| tract_harvest_probe::bind::resolve(&routes, &reg, 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
Expand Down
102 changes: 102 additions & 0 deletions crates/tract-harvest-probe/tests/tiny.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 &reg.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, &reg);
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, &reg, 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, &reg);
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, &reg), routes);
}
Loading