Skip to content

[memory] layout: hold fixed leaf values in their leaf nodes - #74

Merged
victor-ab merged 1 commit into
victor-ab/leaf-i64from
victor-ab/leaf-inline
Nov 7, 2026
Merged

victor-ab merged 1 commit into
victor-ab/leaf-i64from
victor-ab/leaf-inline

Conversation

@victor-ab

@victor-ab victor-ab commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

After #68, a fixed-point leaf's value is an eight-byte entry in a table beside the nodes, though the leaf's node already spends 16 bytes on two words, most of whose bits a leaf doesn't use. Holding the value in the node removes the table and its index.

Design

layout: hold fixed leaf values in their leaf nodes

  • Under a Fixed plan, a leaf's packed value ([memory] layout: fixed-point leaf values take 8 bytes #68's signed odd significand and shift) is in its node's word 1, with a gap at the leaf flag; removing the gap restores the packed value exactly. Word 0 stays zero, slot included, because lanes evaluate the bin test before checking the flag. A Fixed leaf's mask-field bits hold significand bits, so readers check the leaf flag first.
  • The walks and their split steps are unchanged. Constant passes decode the word the walk returns; grouped Fixed entries carry the stop link, and the leaf pass reads the node through it. Resolve selects the all-rows mask before reading a Fixed leaf as a mask field.
  • Wide plans keep their table of limbs, one vector that is empty under Fixed. Resolve stays instantiated separately for Fixed and Wide: folding them adds instructions to Fixed's x86-64 hot paths (resolve 41 to 43, the leaf pass 552 to 554).

Evidence

1 candidate 32 512
this PR vs #68 +2.4% (+2.3 to +2.8) within noise (−12.6 to +5.3) within noise (−13.7 to +6.2)

On AVX-512 (GCP c4-standard-8, Intel Xeon Platinum 8581C, SMT off, all-core turbo, one pinned core; six paired rounds), for the served requests and for groups of 128 to 1,024 rows (max_group_width 1024): like any group of 65 to 1,024 rows, these take Handle carriers and reach list_avx512:

1 candidate 32 512 128 rows 512 rows 1,024 rows
this PR vs #68 +2.0% (+0.8 to +2.5) +2.0% (+0.5 to +2.2) −1.4% (−1.6 to −0.5) +1.1% (+0.4 to +2.6) −1.7% (−2.1 to −0.0) −1.7% (−2.6 to −1.3)
#67 to this PR vs #66 −2.7% (−3.3 to −1.9) −1.2% (−4.5 to −0.7) −6.7% (−8.6 to −5.2) within noise (−0.3 to +2.2) −2.1% (−3.2 to −1.5) −4.7% (−5.2 to −3.9)

A served request's 14-row group is W32, with u32 carriers, so resolve takes its cursor loop there; only Handle carriers (65 to 1,024 rows) go through resolve_listed and list_avx512. On those groups, this PR costs 1.1% at 128 rows and is 1.7% faster at 512 and 1,024. With #67 and #68, the stack is faster than #66 on AVX-512 at every served size.

Where the rounds disagree in sign, only their range is given.

  • New test: odd significands from 1 to 2^53 − 1, both signs, at every shift that fits, and zero, round-trip through a leaf node whose word 0 stays zero and which reads as a leaf. The existing layout, oracle and reference-walk tests check both prediction paths for Fixed and Wide models.
  • cargo test --release --features research passes; clippy (workspace, research, library alone) and fmt.

@victor-ab
victor-ab added this pull request to stack #76 November 6, 2026 23:41
@victor-ab victor-ab changed the title victor ab/leaf inline [memory] layout: hold fixed leaf values in their leaf nodes Nov 6, 2026
@victor-ab
victor-ab marked this pull request as ready for review November 6, 2026 23:42
@victor-ab
victor-ab requested a review from ukaratay November 7, 2026 01:18

@ukaratay ukaratay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The encoding is right: closing the gap and the arithmetic shift restore the packed value, sign included.

Before this merges I want one paired A/B on an AVX-512 Google Cloud machine. Every latency number in the stack is from an M4 Pro, and this PR changes list_avx512, which the M4 never runs: a masked load and a widening per 8 items on the hot path. Across #67, #68 and this PR, 1 candidate pays +1.3%, +0.7% and +2.4%, consistent in every round. 9.4% of the loaded heap is worth it for users who load into their own memory. But every user pays the latency and can't opt out, so measure it where production runs.

@victor-ab
victor-ab force-pushed the victor-ab/leaf-inline branch from bd7b0e0 to 6f5c3f0 Compare November 7, 2026 18:29
@victor-ab

Copy link
Copy Markdown
Collaborator Author

Measured on a GCP c4-standard-8 (Intel Xeon Platinum 8581C, AVX-512, SMT off, all-core turbo, one pinned core), on the private model, with six paired rounds (A B B A) per pair. Every build gives bitwise equal outputs there, equal to the M4's.

list_avx512 doesn't run for the served requests. A 14-row group is W32 with u32 carriers, so resolve takes its cursor loop; only Handle carriers (65 to 1,024 rows) reach resolve_listed, and that model's configuration caps groups at 14 rows. So I also timed groups of 128, 512 and 1,024 rows, with max_group_width 1024. p50 change:

1 candidate 32 512 128 rows 512 rows 1,024 rows
#67 vs #66 −5.5% −5.7% −6.4% −1.2% −1.5% −3.8%
#68 vs #67 +1.7% +2.0% noise noise +1.6% noise
this PR vs #68 +2.0% +2.0% −1.4% +1.1% −1.7% −1.7%
all three vs #66 −2.7% −1.2% −6.7% noise −2.1% −4.7%

"noise" marks a cell whose rounds disagree in sign; in every other cell, all six rounds agree. On AVX-512, #67 is faster at every size. So the three together are faster than #66 at every served size and at 512 and 1,024 rows, and within noise at 128. The description has the ranges.

@victor-ab
victor-ab requested a review from ukaratay November 7, 2026 18:30

@ukaratay ukaratay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the c4 numbers. I was wrong about list_avx512: the 14-row cap keeps served requests on the cursor loop. +2% at 1 and 32 candidates for 9.4% of the loaded heap, with the three PRs together faster than #66 at every served size, is a trade I'll take.

Hold each Fixed leaf in its node instead of an eight-byte entry in a
separate table. Its packed signed significand and shift go in word 1,
with a gap at the leaf flag; removing the gap restores the packed value
exactly. Word 0 stays zero, slot included, because lanes evaluate the
bin test before checking the flag.

Keep the walks and their split steps unchanged. Constant passes decode
the word the walk returns; grouped Fixed entries carry the stop link and
the leaf pass reads the node through it. Resolve selects the all-rows
mask before reading a Fixed leaf as a mask field. Keep separate Fixed
and Wide resolve instantiations, so Wide retains its table indices and
Fixed keeps its generated hot paths.

The plan selects the leaf source: Wide plans keep one vector of values,
empty under Fixed. Introspection counts no Fixed leaf table. Tests
round-trip the encoding through leaf nodes; the existing split and
reference checks cover both prediction paths.
@victor-ab
victor-ab force-pushed the victor-ab/leaf-inline branch from 6f5c3f0 to 0d79175 Compare November 7, 2026 20:49
@victor-ab
victor-ab merged commit e50042f into main Nov 7, 2026
6 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants