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
36 changes: 35 additions & 1 deletion .claude/board/ISSUES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Issues Log — Open + Resolved (double-entry, append-only)

## ISS-LGJ-HOP-SWEEPS-FULL-POPULATION (2026-08-27) — RESOLVED IN PART, same day
## ISS-LGJ-HOP-SWEEPS-FULL-POPULATION (2026-08-27) — RESOLVED

**Found.** By running bench Component G — the F-PARITY harness — for the
first time. `lgj_hop` is the slowest of three arms at every configuration in
Expand Down Expand Up @@ -68,6 +68,40 @@ point in the measured space. The gather rewrite is none of those things.
Landing them together would have made a regression in either impossible to
attribute.

**RESOLVED, same day, second pass.** The remaining structural half is gone:
`lgj_hop` no longer sweeps at all. It gathers — touching only the rows `src`
names, reading each one's participating facets in place out of that row's own
512 bytes.

**The crossover this issue predicted does not exist.**
`examples/hop_gather_vs_sweep.rs` measured both shapes over five populations
(1 024 … 262 144) × twelve densities — 60 configurations, byte-identical
output asserted at every one, plus an anti-vacuity empty-hop guard. **Gather
wins all 60**, from 2 754× at 0.01 % density to **1.73× at 100 %**.

The prediction was that a dense frontier would favour the sweep's sequential
vectorised access. Wrong, and the mechanism is the correction: a sweep
MATERIALISES an `n`-element per-row intermediate that each row reads exactly
once, so it is never amortised — at full density it does everything the gather
does PLUS allocate, zero, write and re-read `n` u32s. Strictly more work at
every density. The reasoning had been about access PATTERN and missed that one
shape simply does MORE.

Consequence for the design: **no threshold, no dispatch, no heuristic gate.**
A crossover would have required one, with the two-sided evidence such a gate
demands; its absence makes the change unconditional and much simpler.

End to end through Component G, the independent instrument: native_hop
24 798 → 34.4 µs at 1 %/65 536 (**720×**), and the ordering inverted — native
is now FASTEST at every configuration, 2.0×–13× ahead of the best scalar arm,
having started this arc slowest at every configuration.

**Still open, and named rather than hidden:** the one shape that could favour
a precomputed per-row mask is REUSE — memoising it across many hops on the
same `(store, classid)`. That is a caching design with its own invalidation
questions and is deliberately not this function's.

Raw probe output: `.claude/board/hop-gather-vs-sweep-crossover.txt`.
Data: `bench/results/jmh-results-G.csv`; narrative: `bench/RESULTS.md` § G.


Expand Down
77 changes: 77 additions & 0 deletions .claude/board/hop-gather-vs-sweep-crossover.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
gather-vs-sweep crossover — spread frontier, edge_classid=0, gate=0x0, radius=25, reps=7 (median)

== n_rows = 1024 ==
frontier density sweep_us gather_us ratio faster
1 0.01% 92.8 0.3 273.86x gather
1 0.05% 100.2 0.3 319.18x gather
1 0.10% 101.2 0.3 314.24x gather
2 0.25% 106.5 0.4 273.70x gather
5 0.50% 103.9 0.6 166.76x gather
10 1.00% 104.0 0.9 111.50x gather
20 2.00% 99.9 1.4 71.81x gather
51 5.00% 101.7 3.3 31.09x gather
102 10.00% 89.8 6.2 14.45x gather
256 25.00% 94.1 14.1 6.68x gather
512 50.00% 88.8 27.0 3.29x gather
1024 100.00% 92.0 61.9 1.49x gather

== n_rows = 4096 ==
frontier density sweep_us gather_us ratio faster
1 0.01% 368.7 0.3 1237.12x gather
2 0.05% 361.6 0.4 1018.66x gather
4 0.10% 378.5 0.4 860.28x gather
10 0.25% 420.9 0.9 487.12x gather
20 0.50% 393.6 1.5 257.74x gather
40 1.00% 396.0 2.6 150.56x gather
81 2.00% 429.4 5.2 82.60x gather
204 5.00% 367.0 12.3 29.76x gather
409 10.00% 377.8 24.4 15.50x gather
1024 25.00% 385.1 80.1 4.81x gather
2048 50.00% 371.7 114.1 3.26x gather
4096 100.00% 416.5 225.9 1.84x gather

== n_rows = 16384 ==
frontier density sweep_us gather_us ratio faster
1 0.01% 1609.2 0.7 2305.40x gather
8 0.05% 1525.2 1.2 1268.87x gather
16 0.10% 1509.6 1.4 1105.94x gather
40 0.25% 1607.6 3.2 501.89x gather
81 0.50% 1570.5 6.7 234.80x gather
163 1.00% 1535.6 13.5 113.57x gather
327 2.00% 1572.7 20.7 76.04x gather
819 5.00% 1496.2 66.6 22.46x gather
1638 10.00% 1597.0 146.2 10.92x gather
4096 25.00% 1552.9 312.3 4.97x gather
8192 50.00% 1758.5 532.3 3.30x gather
16384 100.00% 1785.4 946.3 1.89x gather

== n_rows = 65536 ==
frontier density sweep_us gather_us ratio faster
6 0.01% 7193.7 2.7 2666.31x gather
32 0.05% 7306.4 3.6 2035.21x gather
65 0.10% 6993.4 5.3 1325.51x gather
163 0.25% 7670.4 13.6 563.25x gather
327 0.50% 8246.3 30.2 272.82x gather
655 1.00% 8117.5 69.5 116.83x gather
1310 2.00% 7080.6 92.4 76.59x gather
3276 5.00% 7128.7 321.2 22.19x gather
6553 10.00% 7590.4 678.7 11.18x gather
16384 25.00% 8158.6 3283.4 2.48x gather
32768 50.00% 8853.5 4514.4 1.96x gather
65536 100.00% 10083.2 5678.4 1.78x gather

== n_rows = 262144 ==
frontier density sweep_us gather_us ratio faster
26 0.01% 27524.6 10.0 2753.83x gather
131 0.05% 27511.3 13.8 1994.15x gather
262 0.10% 27735.1 24.3 1139.16x gather
655 0.25% 27779.5 78.9 351.93x gather
1310 0.50% 27641.5 137.3 201.30x gather
2621 1.00% 28604.5 318.2 89.89x gather
5242 2.00% 28562.8 612.0 46.67x gather
13107 5.00% 30100.1 2842.1 10.59x gather
26214 10.00% 29586.3 5968.4 4.96x gather
65536 25.00% 31583.4 12368.0 2.55x gather
131072 50.00% 33729.6 16287.8 2.07x gather
262144 100.00% 37821.5 21811.6 1.73x gather

58 changes: 58 additions & 0 deletions bench/RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,64 @@ Behaviour is unchanged, not merely believed to be: lgj-abi 134/134, AllTests
304, GraphHopTest 66 **including G3 at the identical 384-byte floor**,
TradesParity 12, TradesAllocation 3, BricksAuth 62.

### FIXED AGAIN — and the crossover I predicted does not exist

The section below was written when the one-pass rewrite still left native 43×
the best scalar arm, and it named the next rung as a *gather* whose merit
depended on a **density crossover**: sparse frontiers should favour a gather,
dense ones the sweep's sequential vectorised access. It said to measure rather
than assume.

Measured — `native/lgj-abi/examples/hop_gather_vs_sweep.rs`, both shapes over
**five populations (1 024 … 262 144) × twelve densities**, with byte-identical
output asserted at every one of the 60 configurations plus an anti-vacuity
guard against an empty hop:

**There is no crossover. Gather wins all 60.**

| n_rows | 0.01 % | 1 % | 25 % | 100 % |
|---|---|---|---|---|
| 1 024 | 361× | 113× | 7.3× | 1.73× |
| 4 096 | 1 295× | 143× | 5.9× | 1.75× |
| 65 536 | 2 666× | 117× | 2.5× | **1.78×** |
| 262 144 | 2 754× | 90× | 2.6× | **1.73×** |

**Why it holds even at 100 % density** — the part I had wrong. A sweep
MATERIALISES an `n`-element per-row intermediate that each row reads exactly
once, so its cost is never amortised. At full density the sweep does
everything the gather does *plus* allocate, zero, write and re-read `n` u32s.
It is strictly more work at every density, not merely at sparse ones. I
reasoned about access *pattern* and missed that one shape simply does *more*.

**A scalar gather beats a vectorised `ndarray::simd` sweep.** Same lesson the
object-model ladder taught in the Component-C/F family: the win is in not
doing the work, not in the vector width.

### End-to-end through the independent instrument

Re-running Component G with the gather in place, same command:

| `native_hop` | 1 % / 4096 | 1 % / 65536 | 25 % / 4096 | 25 % / 65536 |
|---|---|---|---|---|
| original (32 sweeps) | 479.0 µs | 24 798.3 µs | 521.2 µs | 23 633.9 µs |
| one-pass | 374.7 µs | 7 120.3 µs | 375.8 µs | 8 076.9 µs |
| **gather** | **1.9 µs** | **34.4 µs** | **61.0 µs** | **3 534.1 µs** |
| vs original | **246×** | **720×** | 8.5× | 6.7× |

And the ordering the whole component was built to test has inverted — native
is now **fastest at every configuration**, 2.0×–13× ahead of the best scalar
arm, where it began this arc slowest at every configuration by 2.6×–165×.

Two honesty notes. The Rust probe and the JMH harness are independent
instruments and they agree: sweep at 1 %/65 536 measures 6 754–8 118 µs in
Rust against JMH's 7 120 µs. And this JMH run was noisier than the previous
one — `classidScan` at 1 %/4096 reports 25.6 ± 61.9 µs, an error bar larger
than the score — so the *scalar* absolutes here are weak. Native's own errors
are tight (1.9 ± 0.3, 34.4 ± 8.1), and a 720× change is far outside any noise
in this container.

Raw probe output: `.claude/board/hop-gather-vs-sweep-crossover.txt`.

### Still slower than the best scalar arm — the honest remaining gap

At 1 % / 65 536, native is 7 120 µs against `classidScan`'s 164 µs: **43×**.
Expand Down
24 changes: 12 additions & 12 deletions bench/results/jmh-results-G.csv
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"Benchmark","Mode","Threads","Samples","Score","Score Error (99.9%)","Unit","Param: frontierPct","Param: rows"
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,10.902422,18.771611,"us/op",1,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,164.242494,12.784405,"us/op",1,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,238.702506,41.554658,"us/op",25,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,7042.946461,2272.034075,"us/op",25,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,392.556226,9.559705,"us/op",1,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,7698.823135,456.583748,"us/op",1,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,487.562356,57.601186,"us/op",25,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,10864.031411,2497.132997,"us/op",25,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,374.663354,21.906457,"us/op",1,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,7120.259949,1037.574620,"us/op",1,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,375.800408,25.683170,"us/op",25,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,8076.916011,1556.374964,"us/op",25,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,25.639505,61.918845,"us/op",1,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,180.563246,70.114359,"us/op",1,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,269.635512,229.450931,"us/op",25,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_classidScan","avgt",1,5,6943.313461,1755.024200,"us/op",25,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,500.512270,170.638166,"us/op",1,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,9215.489321,3935.414475,"us/op",1,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,570.724283,526.047273,"us/op",25,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.java_scalar_facetMatches","avgt",1,5,10674.616011,2590.280567,"us/op",25,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,1.945347,0.344690,"us/op",1,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,34.425630,8.069149,"us/op",1,65536
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,61.030395,6.963998,"us/op",25,4096
"com.adaworldapi.lancegraph.bench.G_HopExecutionBoundary.native_hop","avgt",1,5,3534.089985,841.958023,"us/op",25,65536
Loading