Skip to content

[memory] layout: exceptions take 6 bytes, with 16-bit feature-relative slots and a 32-bit fallback - #67

Merged
victor-ab merged 1 commit into
victor-ab/fill-restorefrom
victor-ab/exception-slots
Nov 7, 2026
Merged

victor-ab merged 1 commit into
victor-ab/fill-restorefrom
victor-ab/exception-slots

Conversation

@victor-ab

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

Copy link
Copy Markdown
Collaborator

Motivation

After the stack's first PR, the rank tables' exceptions are the largest part of a categorical model's loaded heap: 8 bytes each, [slot, word] with an absolute 32-bit slot. A feature's slots are its own contiguous range, almost always far fewer than 65,536, and the fill's restore loop reads only the slots.

Design

layout: separate exception words from feature-relative slots

  • BinMap keeps the exceptions' words and their slots in separate arrays, the slots relative to their feature's first slot.
  • The slots are 16 bits wide while every categorical feature's slots fit (a relative slot below 65,536). The first feature that doesn't converts the slots so far to 32 bits once, model-wide, and the build continues in 32 bits, so no model that loads today fails to load; there is no transient double copy in the common case.
  • fill_with dispatches on the width once per feature, then runs a monomorphic loop; the restore loop reads 2 bytes per exception rather than 8.
  • rank_table_bytes counts 6 bytes per exception (8 in the 32-bit fallback), and the cap is checked on the same count, including a widening, before the tables grow; so LoadOptions::max_rank_table_bytes counts what is held, as the cap table in docs/treelite-loading.md says. A model near the cap that failed before can now load; none that loaded fails.

Evidence

On the private model, the loaded model's heap is 14% lower than with #66 (41% of 3.2.2's), and outputs are bitwise equal to 3.2.2's on synthetic rows and on the generated requests.

Latency, predict_fixed per request on the generated requests, one thread, Apple M4 Pro: p50 change from #66, median of four paired rounds (A B B A, 1 s per cell), with the rounds' range:

1 candidate 32 512
this PR vs #66 +1.3% (+0.8 to +1.9) within noise (−7.3 to +4.1) within noise (−13.0 to +5.7)

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 #66 −5.5% (−5.9 to −5.1) −5.7% (−8.6 to −4.5) −6.4% (−8.0 to −5.4) −1.2% (−1.6 to −0.4) −1.5% (−2.2 to −1.3) −3.8% (−5.1 to −3.3)

The M4's 1-candidate cost doesn't carry over: on AVX-512 the narrower slots are faster at every size.

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

  • Tests: the width boundary (65,536 slots stays narrow, 65,537 widens) and a widening keeping the earlier entries; fills after a widening keep the earlier features' words; a model whose unpacked chains need 32-bit slots predicts as its packed layout does, with constant and varying features. Expectations in tests/import.rs, tests/limits.rs and tests/research.rs are recomputed for 6-byte entries.
  • cargo test --release --features research passes; clippy (workspace, research, library alone) and fmt.

@victor-ab
victor-ab added this pull request to stack #70 November 6, 2026 13:20
@victor-ab victor-ab changed the title layout: separate exception words from feature-relative slots [memory] layout: exceptions take 6 bytes, with 16-bit feature-relative slots and a 32-bit fallback Nov 6, 2026
@victor-ab
victor-ab force-pushed the victor-ab/exception-slots branch from 9183b84 to ec3b280 Compare November 6, 2026 14:26
@victor-ab
victor-ab removed this pull request from stack #70 November 6, 2026 23:40
@victor-ab
victor-ab force-pushed the victor-ab/exception-slots branch from ec3b280 to 70ef0dd Compare November 6, 2026 23:40
@victor-ab
victor-ab added this pull request to stack #76 November 6, 2026 23:41
@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.

Approving. I checked the width boundary (65,536 slots stay 16-bit, 65,537 widen) and that the cap counts a widening before the tables grow. The +1.3% at 1 candidate shows up in every round, so it's a real cost. That's fine for 14% of the loaded heap, but it adds to the x86 question on #74.

@victor-ab

Copy link
Copy Markdown
Collaborator Author

On AVX-512 this PR is faster at every size: −5.5%, −5.7% and −6.4% at 1, 32 and 512 candidates, and −1.2% to −3.8% on groups of 128 to 1,024 rows (GCP c4, six paired rounds). Details are on #74.

The rank tables kept each exception's absolute slot beside its word. Keep
parallel arrays instead, with slots relative to their feature, so that
restores read only the slots and the usual entries take six bytes rather
than eight. This reduces the loaded memory of a large model with many
categorical splits without changing the chains, packed fields or outputs.

Slots start as u16. The first feature with more than 65,536 categorical
slots widens the preceding slots to u32 once, and later features keep that
width. Offsets and words do not change. A model-wide width avoids a format
per feature and lets the fill choose its width once before both loops,
rather than branch per exception. Wide tables still take eight bytes per
entry, so the narrower format adds no load limit.

Count the selected width before growing the tables, including the cost of
widening earlier entries. Update the cap's documentation and byte checks.
Tests cover the width boundary, widening after a narrow feature, repeated
fills across the boundary, and wide single-row and grouped predictions,
with constant and varying features, against the packed model and oracle.
@victor-ab
victor-ab force-pushed the victor-ab/exception-slots branch from 99ab9b1 to da5d340 Compare November 7, 2026 20:49
@victor-ab
victor-ab merged commit e50042f into main Nov 7, 2026
6 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