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
3 changes: 3 additions & 0 deletions .claude/board/INTEGRATION_PLANS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 2026-10-23 — d-onnx-native-v1 — ONNX vocabulary over existing native functions → `.claude/plans/d-onnx-native-v1.md`

**Status:** D-ONNX-NATIVE-0 IMPLEMENTED as a draft PR (`crates/lance-graph-onnx`), not merged. The path is ONNX name → existing native function: ALIAS where the contract is identical, a small INLINE adapter where it is not, a typed `Refusal` otherwise. No graph, dispatcher or initialization. R2IL routing (D-MPF-4, #1752) is separate and is not imported.
## 2026-10-23 — d-native-repr-contract-v1 + d-native-first-dependency-correction-v1 — the native polyfill is the root → `.claude/plans/d-native-repr-contract-v1.md`, `.claude/plans/d-native-first-dependency-correction-v1.md`

**Status:** D-NFR-0 / D-NFC-0 documentation (DECISION 2026-10-23: corrections to #1746–#1751 and #1750 approved). D-NFR-1..5 PROPOSED, not authorized. ONNX is an external compatibility specification; the R2IL polyfill is the native root; Quack binding + DR64³ + Palette256³ is the intended native representation, with its shipped, lab and missing seams separated. The smallest missing seam is that a Quack-bound query does not know its frame. The #1744 / #1745 / #1750 S0 dispute is not decided.
Expand Down
9 changes: 9 additions & 0 deletions .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## D-ONNX-NATIVE — ONNX vocabulary over existing native functions (2026-10-23)

Plan: `.claude/plans/d-onnx-native-v1.md`. Crate: `crates/lance-graph-onnx` (ALIAS / INLINE only; no dispatch).

| D-id | scope | status | gate / falsifier |
|---|---|---|---|
| **D-ONNX-NATIVE-0** | crosswalk + minimal facade: elementwise float arithmetic, int32 comparisons, Boolean ops, Where, ReduceSum, ReduceSum(Where(Greater)) | IMPLEMENTED, draft PR (`entries/2026-10-23-d-onnx-native-0-facade.md`) | 11 conformance tests + doctest; 4 disable runs red on their own test; alias = native symbol; asm diff |
| **D-ONNX-NATIVE-1** | the NATIVE rows: approximate transcendentals, float ReduceSum tolerance, empty-population min/max | PROPOSAL | each row names its open decision (plan §2) |

## D-NFR / D-NFC — native representation contract + dependency correction (2026-10-23)

Docs only. Plans `d-native-repr-contract-v1.md` (D-NFR) and `d-native-first-dependency-correction-v1.md` (D-NFC). Nothing implemented; no ABI or execution contract changed.
Expand Down
2 changes: 1 addition & 1 deletion .claude/board/TECH_DEBT.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

## TD-BGZ-TENSOR-INTEGRATION-TESTS-UNGATED-1 (2026-10-23) — OPEN

CI runs `bgz-tensor` with `--lib` only (`.github/workflows/rust-test.yml:274`),
CI runs `bgz-tensor` with `--lib` only (`.github/workflows/rust-test.yml`, step "Run bgz-tensor tests (workspace-excluded, ndarray sibling)"),
so nothing under `crates/bgz-tensor/tests/` runs in CI. The crate is
workspace-excluded, so no other step reaches it.

Expand Down
12 changes: 12 additions & 0 deletions .claude/board/entries/2026-10-23-d-onnx-native-0-facade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# D-ONNX-NATIVE-0 — ONNX names over existing native functions

**Date:** 2026-10-23 · **Status:** IMPLEMENTED (draft PR, not merged) · **Plan:** `.claude/plans/d-onnx-native-v1.md`

`crates/lance-graph-onnx` exposes ONNX operator names over existing native functions. It has no dispatcher, no graph and no initialization, and its only normal dependency is `ndarray`. No native crate depends on it.

- **ALIAS** (same symbol, pinned by `fn_addr_eq`): `Abs`, `Sqrt` (vml); `Greater`/`GreaterOrEqual`/`Less`/`LessOrEqual`/`Equal` on int32 vs a scalar; `And`/`Or`/`Xor`/`Not` on packed masks; `Where` int32.
- **INLINE**: float `Add`/`Sub`/`Mul`/`Div` (a broadcast check turns ndarray's panic into a refusal); `Neg`; int32 `ReduceSum` (attributes validated once, plus an overflow refusal); `ReduceSum(Where(Greater))`, the #1752 composite.
- **MEASURED** (ns per call, reported not gated): Greater direct = onnx (14.6 / 14.6 at 64 rows, 0 allocs). The composite is 38.6 direct vs 39.3 onnx vs 224 for the #1752 wrapper (3 allocs). In the asm, the composite's extra instructions are the int32 range check only.
- **FOUND:** `vml::vsneg` returns `+0` for `Neg(+0)` when n ≥ 16 (`0.0 - v` in the SIMD body), so `Neg` is INLINE, not ALIAS. `vml::vsexp` saturates +inf. Both are reported to the ndarray owner, not fixed here.
- **REFUSED:** integer arithmetic (overflow undefined), tensor-vs-tensor compares, MatMul, control flow, protobuf import, automatic Palette substitution.
- **OPEN:** independent review; the NATIVE rows in plan §2 (approximate transcendentals, float ReduceSum tolerance, empty-population min/max).
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.

459 entries, 2026-08-06 .. 2026-10-23.
460 entries, 2026-08-06 .. 2026-10-23.

| date | entry id | finding | file |
|---|---|---|---|
Expand All @@ -39,6 +39,7 @@ exactly one of them, never both.
| 2026-10-23 | `D-TGA-5` | | [2026-10-23-d-tga-5-squash-gate-preregistration.md](2026-10-23-d-tga-5-squash-gate-preregistration.md) |
| 2026-10-23 | `D-ONNX-RELALG-CAP` | S0 result: ONNX as import membrane, DR64³ plan not found | [2026-10-23-d-onnx-relalg-cap-s0.md](2026-10-23-d-onnx-relalg-cap-s0.md) |
| 2026-10-23 | `E-D-ONNX-RELALG-CAP-S0-1` | | [2026-10-23-d-onnx-relalg-cap-s0-capstone-1750.md](2026-10-23-d-onnx-relalg-cap-s0-capstone-1750.md) |
| 2026-10-23 | `D-ONNX-NATIVE-0` | | [2026-10-23-d-onnx-native-0-facade.md](2026-10-23-d-onnx-native-0-facade.md) |
| 2026-10-23 | `D-NFR-0` | | [2026-10-23-d-nfr-0-native-repr-contract.md](2026-10-23-d-nfr-0-native-repr-contract.md) |
| 2026-10-23 | `D-MPF-4` | | [2026-10-23-d-mpf-4-classid-route.md](2026-10-23-d-mpf-4-classid-route.md) |
| 2026-10-23 | `D-MPF-2` | | [2026-10-23-d-mpf-2-fixed-cost.md](2026-10-23-d-mpf-2-fixed-cost.md) |
Expand Down
134 changes: 134 additions & 0 deletions .claude/plans/d-onnx-native-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# D-ONNX-NATIVE — ONNX vocabulary over existing native functions

**Status:** D-ONNX-NATIVE-0 IMPLEMENTED as a draft (`crates/lance-graph-onnx`). It does not change the production ABI and has not been merged. Everything past §5 is a proposal.

**Rule:** ONNX is a vocabulary over our existing mathematics, not another execution system. The path is `ONNX-compatible Rust API → existing native function` and nothing else: no graph, AST, interpreter, VM, runtime dispatcher, `LazyLock`/`OnceLock`, ClassId routing or per-call backend choice.

**Scope boundaries:**
- R2IL routing (D-MPF-4, #1752) is a separate architecture, and this crate does not import it.
- `ndarray` owns SIMD and the kernels, and it does not depend on this crate.
- Quack, DR64³ and Palette256³ are not recreated here.

## 1. Categories

| category | meaning | cost contract |
|---|---|---|
| **ALIAS** | The native function already has the ONNX contract, so it is re-exported under the ONNX name. | The same symbol: zero added instructions, zero allocations. |
| **INLINE** | A small adapter does only the semantic work the native function lacks, then calls it. | Only that work is added (§4). |
| **NATIVE** | The mathematics exists natively, but it needs a composition or a decision that is not made yet. | Not exported. |
| **REFUSE** | The ONNX semantics differ from what we have, or would need an execution system. | Not exported. Runtime facts are refused as `Refusal` values. |

## 2. Crosswalk (source-backed)

Every ALIAS and INLINE row was checked against the native source. The checks covered dtype, rank, broadcasting, axes, overflow, rounding, empty tensors, output shape and the bool representation.

**Bool convention:** ONNX `tensor(bool)` is the native packed mask. Bit `i % 64` of word `i / 64` is element `i`, and the bits past the last element are zero. `mask_words(n) = n.div_ceil(64)`.

### ALIAS

| ONNX | native | checks |
|---|---|---|
| `Abs` (f32) | `ndarray::hpc::vml::vsabs` | IEEE exact on ±0, ±inf and NaN, for n = 0..70 (SIMD body and tail) |
| `Sqrt` (f32) | `ndarray::hpc::vml::vssqrt` | IEEE exact (correctly rounded sqrt); `sqrt(-x) = NaN`; `sqrt(-0) = -0` |
| `Greater`, `GreaterOrEqual`, `Less`, `LessOrEqual`, `Equal` (int32 tensor vs a scalar) | `ndarray::simd::{gt,ge,lt,le,eq}_i32_to_mask` | signed comparison; `i32::MIN`/`MAX` thresholds; n ∈ {0,1,15,16,17,63,64,65,130}; tail bits zero |
| `And`, `Or`, `Xor` (bool) | `ndarray::simd::mask_{and,or,xor}` | word-wise and exact; zero tails stay zero |
| `Not` (bool, n elements) | `ndarray::simd::mask_not(src, n, dst)` | clears the tail bits past n |
| `Where` (int32, equal shapes) | `ndarray::simd::blend_i32` | a set bit takes X, a clear bit takes Y; native asserts equal lengths |

`aliases_are_the_native_symbols` pins each alias by `fn_addr_eq`, so an alias cannot quietly become a wrapper. That was the D4 disable.

### INLINE

| ONNX | native call | added work and reason |
|---|---|---|
| `Add`/`Sub`/`Mul`/`Div` (f32, f64; multidirectional broadcast) | `ndarray` `&a op &b` | `broadcastable(shapes)`. ndarray broadcasts like numpy but **panics** on a mismatch (`broadcast_with(..).unwrap()`; `co_broadcast` is `pub(crate)`). The adapter turns that panic into `Refusal::Broadcast`. The arithmetic is IEEE in both. `simd::add_f32` and its siblings were **not** used: they truncate to `min(len)`, which is not ONNX. |
| `Neg` (f32, f64) | `ndarray` element map `-v` (the body of its `Neg` impl) | none. It is INLINE, not ALIAS, because the obvious alias `vml::vsneg` is wrong: its 16-lane body computes `0.0 - v`, so `Neg(+0) = +0` for n ≥ 16, while its scalar tail computes `-v`. Pinned by `vml_vsneg_is_not_onnx_neg_on_positive_zero`. |
| `ReduceSum` (int32, rank 1) | iterator `i64` sum | attribute validation at construction, once; an exact `i64` sum; an `int32` range check (`Refusal::OutOfDtypeRange`). ONNX does not define integer overflow, so the adapter refuses rather than wraps. There is no native unmasked `i32 → i64` sum (§6). |
| `ReduceSum(Where(Greater(X, T), X, 0))` (int32) | `gt_i32_to_mask` + `masked_sum_i32` | the range check only. Scratch comes from the caller, so there is no allocation. This is the #1752 composition. |

`ReduceSum` attributes are checked once in `ReduceSum::new(axes, keepdims, noop_with_empty_axes)`, never per call:
- `axes` ∈ {absent, `[0]`, `[-1]`} for rank 1;
- `keepdims` ∈ {0, 1};
- `noop_with_empty_axes = 1` with absent axes is refused, because it returns a tensor, not a scalar.

### NATIVE (exists, not exported yet)

| ONNX | native | why not exported |
|---|---|---|
| `Exp`, `Log`, `Tanh`, `Sigmoid` | `vml::vsexp` and the related vml functions | approximate (polynomial). On ndarray master `vsexp` returns 1.7e38 for +inf, halves every result above x ≈ 88.38 and returns ~1.2e-38 for −inf (fixed in AdaWorldAPI/ndarray#368, open). Exact and approximate stay separate, so these would need an explicitly named approximate surface with ULP bounds. |
| `ReduceSum` (float) | `ArrayBase::sum` | it reassociates (`unrolled_fold`). That is allowed under ONNX tolerance but is not bit-identical to sequential order, so the tolerance contract has to be stated first. |
| `ReduceMin`/`ReduceMax` of a masked population | `masked_min_i32`/`masked_max_i32` | the empty-population result is undecided (ONNX ReduceMin over an empty set is ±inf for floats and undefined for ints) |
| comparisons on u64 | `{lt,le,gt,ge}_u64_to_mask` | ONNX has no unsigned-64 compare-with-scalar distinct from int64; needs a dtype decision |

### REFUSE

| ONNX | reason |
|---|---|
| integer `Add`/`Sub`/`Mul` | overflow is undefined in ONNX. ndarray `+` wraps in release and panics in debug. Needs the G1 overflow decision. |
| tensor-vs-tensor comparisons; f32 comparisons to a mask | no native kernel. Writing one here would duplicate a SIMD kernel. |
| `MatMul`/`Gemm` | out of scope for D-ONNX-NATIVE-0. The CompiledLinear identity defect has to be resolved first. |
| `Gather`, `Scatter`, `If`, `Loop`, control flow | these need graph or execution semantics, which this crate does not have |
| ONNX protobuf import | out of scope. It would be a separate optional crate. |
| Palette substitution for any op | approximate. Never automatic. |

## 3. Cross-check against relalg and mask-risc

The composite answer is checked against four independent computations in one test (`composite_matches_the_oracle_mask_risc_r2il_and_relalg`):

1. a plain-iterator oracle;
2. a `lance-graph-mask-risc` program, `Pred::GtI32` + `Terminal::MaskedSumI32` via `execute_into`;
3. `lance-graph-r2il` `FoldDialect` (`LOAD`, `INT_S_LESS`, `procs::sum`);
4. `lance-graph-relalg` `run(Query{ rel: Atom::I32{ op: Gt, .. }, measure: Sum })`.

All four are dev-dependencies only. None is on the facade's call path.

## 4. Measurements and assembly (reported, not gated)

Setup: `examples/alias_cost.rs`, release, AVX-512 at compile time, load ≈ 0.1–0.4. Medians over 21 reps, in ns per call; 3 runs agree.

| rows | op | direct | onnx | #1752 wrapper (diagnostic) | allocs d/o/w |
|---:|---|---:|---:|---:|---|
| 64 | Greater (ALIAS) | 14.6 | 14.6 | — | 0/0 |
| 64 | ReduceSum(Where(Greater)) (INLINE) | 38.6 | 39.3 | 224.4 | 0/0/3 |
| 64 | Add f32 (INLINE) | 34.8 | 35.6 | — | 1/1 |
| 4096 | Greater | 311.1 | 310.7 | — | 0/0 |
| 4096 | ReduceSum(Where(Greater)) | 1807.9 | 1807.3 | 2047.1 | 0/0/3 |
| 4096 | Add f32 | 362.3 | 382.7 | — | 1/1 |
| 65536 | Greater | 6004.8 | 5796.6 | — | 0/0 |
| 65536 | ReduceSum(Where(Greater)) | 30531.1 | 28112.2 | 29773.5 | 0/0/3 |
| 65536 | Add f32 | 8003.4 | 8465.6 | — | 1/1 |

At 65536 rows the direct/onnx differences are run-to-run noise (other runs reverse them). Add's single allocation is the output array, the same as direct `&a + &b`.

The assembly is from the out-of-line `probe_*` functions (`cargo rustc --release --example alias_cost -- --emit=asm`):
- **Greater (ALIAS):** `fn_addr_eq(greater_i32, gt_i32_to_mask)` is true, so there is no wrapper symbol at all.
- **ReduceSum(Where(Greater)):** with `#[inline]`, the onnx probe is 163 instructions against direct's 147. Diffing the bodies, the extra instructions are the `int32` range check (`movslq`/`cmp`) and the `Result` write. Both probes carry the same `memset` (mask zero-fill in `pack`), `memcpy` and bounds-check panics. Without `#[inline]`, the onnx probe was a single tail `jmp` into the adapter.
- **Add:** direct is a 4-instruction tail jump into ndarray's `Add for &ArrayRef`. The onnx probe inlines `broadcastable` (a shape loop plus the cold `to_vec` allocation on the refusal path) and then makes the same ndarray call.
- **Scan for dispatch:** no `Lazy`, `Once`, `cpuid` or feature probe appears in any onnx probe or in `src/`.

The old 50 ns target does not apply to aliases. An alias's cost is the native function's cost.

## 5. Dependency audit

- `lance-graph-onnx`'s only normal dependency is `ndarray`, on the same git coordinate as `lance-graph-mask-risc`. The root `[patch]` unifies it with the sibling checkout, so the workspace holds one ndarray. `cargo tree -p lance-graph-onnx -e normal --depth 1` shows exactly that.
- Reverse: `cargo tree -i lance-graph-onnx -e normal` lists only the crate itself. No manifest under `crates/` names it. The root `Cargo.toml` names it only as a workspace member. The ndarray sibling has no reference to it.
- The relalg, mask-risc, r2il and ogar-loco crates appear only under `[dev-dependencies]`, for the cross-checks and the diagnostic baseline.

## 6. Proposed (not implemented)

- An ndarray owner fix for `vml::vsneg` (+0) and `vml::vsexp` (±inf, overflow halving): AdaWorldAPI/ndarray#368, open. After it lands, `Neg` could become an ALIAS.
- A native unmasked `i32 → i64` sum in `ndarray::simd`, so that `ReduceSum::sum_i32` becomes a direct call.
- The NATIVE rows in §2, each with its open decision named.
- `#1752` `onnx_compat` stays as the independent probe. This crate is the facade, and #1752's measurements are kept as the diagnostic baseline.

## 7. Gates

- [x] ALIAS = native symbol (`fn_addr_eq`), zero allocations (measured)
- [x] INLINE adds only its semantic work (asm diff)
- [x] no dispatch, no initialization, no per-loop decision (source grep + asm)
- [x] agreement with independent references (11 tests + 1 doctest)
- [x] explicit refusals (broadcast, axes, keepdims, noop, overflow)
- [x] 4 disable runs, each red on its own test only
- [x] clippy `-D warnings`, fmt
- [ ] independent review
- [ ] operator authorization before merge
8 changes: 8 additions & 0 deletions .github/workflows/rust-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ jobs:
run: cargo clippy --manifest-path crates/lance-graph-r2il/Cargo.toml --all-targets -- -D warnings
- name: Run r2il-mask-abi-probe mask-ABI differential (excluded tier, OGAR + ndarray siblings)
run: cargo test --manifest-path crates/r2il-mask-abi-probe/Cargo.toml
# lance-graph-onnx (D-ONNX-NATIVE-0): ONNX names over existing native
# functions. A workspace member, and `cargo build --workspace` does not
# run a member's tests. Its conformance tests cross-check against
# mask-risc, r2il (OGAR sibling) and relalg, so it runs here beside them.
- name: Run lance-graph-onnx conformance tests
run: cargo test --release --manifest-path crates/lance-graph-onnx/Cargo.toml
- name: Clippy lance-graph-onnx
run: cargo clippy --manifest-path crates/lance-graph-onnx/Cargo.toml --all-targets -- -D warnings
# deepnsm: standalone 0-dep codec crate, workspace-excluded, so the
# lance-graph test steps above never reached it. ~217 lib + integration +
# doctests, fast (no lance/datafusion/ndarray deps). Gating.
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ members = [
# `ogar-r2il`). The contract may never depend on OGAR (`ogar-loco` depends on
# the contract), but members other than the contract may.
"crates/lance-graph-r2il",
# ONNX mathematical compatibility facade over existing ndarray functions
# (D-ONNX-NATIVE-0). Depends on ndarray only; nothing native depends on it.
"crates/lance-graph-onnx",
# EXPERIMENTAL, routed nowhere: Cypher (upstream front end, read-only) →
# bound LogicalOperator → Quack, with typed refusals.
"crates/lance-graph-cypher-quack",
Expand Down
21 changes: 21 additions & 0 deletions crates/lance-graph-onnx/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "lance-graph-onnx"
version = "0.1.0"
edition = "2021"
publish = false
description = "ONNX mathematical compatibility facade: ONNX operator names and semantics over existing native ndarray functions. Aliases where the contract is identical, minimal adapters where it is not, typed refusals otherwise. No graph, interpreter, dispatcher or runtime initialization."

[dependencies]
# The one native dependency, on the coordinate lance-graph-mask-risc uses. The
# workspace root's `[patch."https://github.com/AdaWorldAPI/ndarray"]` points it
# at the sibling checkout, so the workspace holds one ndarray package.
ndarray = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master", default-features = false, features = ["std"] }

# Cross-checks and the diagnostic baseline only. The facade itself never calls
# them: the normal path is "ONNX-compatible Rust API -> existing native function".
[dev-dependencies]
lance-graph-mask-risc = { path = "../lance-graph-mask-risc" }
lance-graph-r2il = { path = "../lance-graph-r2il" }
lance-graph-relalg = { path = "../lance-graph-relalg" }
# The trait that carries `FoldDialect::call`, on the coordinate lance-graph-r2il uses.
ogar-loco = { git = "https://github.com/AdaWorldAPI/OGAR", branch = "main" }
Loading
Loading