Skip to content

v2.9.0 - #587

Merged
singaraiona merged 28 commits into
masterfrom
dev
Sep 19, 2026
Merged

v2.9.0#587
singaraiona merged 28 commits into
masterfrom
dev

Conversation

@singaraiona

Copy link
Copy Markdown
Collaborator

What & why

Checklist

  • PR targets dev (not master)
  • Commits follow Conventional Commits (feat: / fix: / perf: / docs: / …)
  • make builds cleanly (no new warnings)
  • make test passes; tests added/updated for behaviour changes

ser-vasilich and others added 28 commits September 19, 2026 02:10
An `if` whose branch scans a SYM column with a FILE-domain vocabulary
translated its cells into runtime ids through the domain's whole-
vocabulary LUT.  The first request interns every vocabulary entry into
the global symbol table (with the dotted split), permanently: on a
100M-row table with a 19.7M-entry column that is about 45 GB for a
branch that reads 1.7M distinct values.

The branch now carries its own translation table: slots start
unresolved and a position is interned the first time the branch reads
it, from the raw vocabulary snapshot.  The serial scatter path resolves
on the fly; the parallel eager fill pre-resolves the positions it will
read before dispatching, so workers only read (sym.c frozen-table
rule).  The ids produced are the ones the LUT would have given.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ary bytes

The distinct-symbol evaluation of a computed group key fed the DAG a SYM
column whose ids live in a FILE domain, so every string step of the
expression became a permanent cost: substr and str-find intern each
distinct result into the global symbol table (with the dotted split,
about 1 KB per URL-shaped value), and an `if` that keeps the column
itself translates its cells into runtime ids as well.  On a 100M-row
table with a 19.7M-entry column the key expression left about 65 GB in
the symbol table that never came back.

When the expression's result over the SYM column is SYM and the
column's vocabulary is readable through the raw snapshot, the column is
now presented to the same DAG as a STR column built from the vocabulary
bytes, two million distinct values at a time.  Every intermediate is a
chunk-sized STR vector that dies with its chunk; only the final key
strings are interned, once per distinct value, giving the ids the SYM
evaluation would have produced.  Other shapes keep the one-shot SYM
evaluation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Trivial branches through the pooled fill (past the parallel threshold),
a computed branch through the selected path, the column on both sides
under a row selection, the expression as a group key, and the empty
symbol on either side — each against the same expression over the
in-memory table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The existing key expressions over a splayed copy of the table against
the in-memory oracle, with the default chunk and with a 100-value chunk
so the vocabulary crosses several chunks (RAY_DERIVED_KEY_CHUNK
overrides the chunk length for this), a non-symbol key result that
keeps the one-shot evaluation, and the null-bearing column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pre-resolution pass covered the cells the mask selects, but the SYM
fill read both sides of every row before choosing, so workers met
unresolved positions and interned them inside the pool.  The fill now
reads only the chosen side, through a read-only lookup once the sides
are prepared.  Both side tables are initialised before either result is
checked, so the OOM path frees no uninitialised side.  The table holds
32-bit ids (the symbol table counts them in 32 bits), half the scratch
per vocabulary position.

Test: a FILE-domain column on both sides of the `if` without a row
selection, through the pooled fill.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…probe

ray_table_add_col retains the column itself, so the retain before it
left one reference on the distinct-symbol vector after every derived-key
evaluation over a file-backed column, and with it the column's domain.
The fallback block is re-indented as the else branch it is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… routing, composite key compaction

- bound replicated dense slabs by the last-level cache (ray_cache_llc_bytes)
- keep multi-key arithmetic-over-aggregates on the DAG engine; admit binary aggs into hidden slots
- run desc/asc take: N group queries on v2 when a dense plan exists, trim afterwards
- parallelize the filtered dense-plan prescan
- compact interleaved symbol codes so composite symbol keys pack densely
Radix partitions finalize the emit filter's aggregate into one double per
group in parallel, keep a bounded candidate heap each, take the threshold
from the union, and emit only the kept superset in first-row order.  v2 now
reads the emit filter itself; every shape it admits runs there (the ladder
keeps only shapes v2 declines).

10M rows, three-key count, desc take 10: 1 core 988 -> 287 ms; 28 cores
154 -> 43 ms (legacy ladder: 47 ms).
Dense task-local, partitioned and shared finishes select the kept groups
over the occupied slot list (parallel value fill, bounded candidate heaps,
one threshold) before any output column is built.  Single-key count with
desc take at 28 threads: 6.0 -> 5.4 ms; at one thread 28 -> 17 ms.
…-outs

The emit filter no longer rejects a shape from the parallel engine.  Routes
that cannot select natively return through one wrapper that trims the full
result with the shared keep decision; the legacy trim helper and the
wide-domain carve-out in exec_group_run are gone.
The scatter, chunk count and compaction were dispatched by element grain
over 128 partitions / 77 chunks, which yields one task, so the ordering ran
serially.  Dispatch by task count and compact out of place into an
ng-sized buffer (the input-sized map is freed right after).  10M groups on
28 threads: ordering 63 -> 13.5 ms, whole query 194 -> 143 ms.  Also lifts
the rfl driver's file cap to 1024.
A bounded group emit no longer excludes the task-local dense strategy: its
finish selects the N groups with the smallest first rows (an N-sized heap
over the occupied slots) and emits them in first-seen order, exactly the
prefix the radix bounded emit produced.  10M rows, 100k-key count take 10:
1 core 106 -> 17 ms, 28 cores 17 -> 5.3 ms.
Adds a --census PATH flag to the test driver that records, per evaluated
.rfl line, grouped selects that took the legacy route with their admission
reason; the document tallies the corpus (340 hits in five classes) and what
v2 needs to close each.
fix(if): translate only the FILE-domain cells an if branch reads
…nstead of an env knob

The chunk of the per-distinct-symbol key evaluation is 256 dispatch
rounds of morsels (RAY_MORSEL_ELEMS * RAY_DISPATCH_MORSELS * 256, 2M
values) rather than a bare power of two; the comment says why.  The
RAY_DERIVED_KEY_CHUNK environment variable is gone: a DEBUG-only
ray_derived_key_chunk_set_for_test replaces it, so release builds carry
no knob and no getenv per call.

The several-chunks case moves to test_lang.c (lang/select/
derived_key_file_chunks): a 414-value vocabulary through 100-value and
7-value chunks against the in-memory oracle, the seam reset around each
evaluation.  The rfl test keeps the default-chunk FILE-domain checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e ranges on compaction failure

- top-N value view maps nulls/NaN to -INFINITY in both directions, matching
  sort_nulls_first (first ascending, last descending); the trimmed route
  applies the same mapping, so every v2 route returns the same set
- agg_dense_plan_compact publishes shrunken key ranges only when every
  remap table exists; an allocation failure restores the raw ranges
- drop the unused dense-plan probe and its thread-local cache
… bytes

The asc:/desc: with take: path resolved a SYM sort key's two strings
through ray_sym_domain_str on every compare.  On a FILE domain that
materialises an atom per vocabulary entry under the domain lock, so the
first such query in a process paid for the whole vocabulary before it
compared anything — seconds on a column with millions of distinct
values, while the ordering itself takes milliseconds.

A FILE-domain key now pins the raw vocabulary snapshot for the duration
of the query and compares the entries' bytes in ray_str_cmp order
(common prefix, then length); runtime-domain keys keep the borrowed
string snapshot.  The rows taken are identical.

Test: test/rfl/symbol/file_domain_topk.rfl — one key ascending and
descending, ties broken by a second key in both directions, the symbol
as the second key behind an integer, the empty symbol taking part, a
take past the row count, all against the same ordering over the strings
and past the parallel threshold; the in-memory column as a control.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… below the parallel threshold

- agg_indexed_run frees the dense plan after the key build (tables leaked
  on every buffered aggregate over a compacted composite key)
- the candidate-union threshold handles a single selection task, whose
  union holds exactly N values: the worst candidate is the threshold, so
  small dense top-N queries are selected natively instead of emitting every
  group; route stats record the kept count and the contract tests pin it
- the partition-vs-task-local comparison uses the uncapped replication so
  the choice no longer moves with the machine's cache size (macOS runner)
The filter is thread-local and stays armed across every grouped select
nested in the matched select's from: expression; v2 now requires the slot
to hold the operation the filter names (count when unset).  Also: a bounded
emit that cannot allocate its selection fails instead of emitting the wrong
prefix; groups within the limit are emitted in first-seen order too; the
compaction prescan bounds its transient bitmap footprint.
…t select

The filter is thread-local and stays armed while the arming select's from:
evaluates, so a grouped select nested two levels down was filtered by its
grandparent's having threshold (pre-existing: count-of-count under
where (> n N) returned nothing when the inner counts fell below N).  Each
arming site now records the target depth (the direct child for the having
shape, itself for take/desc and the count-distinct rewrite) and both
engines read the filter through ray_group_emit_filter_active(), which
returns it only at that depth.  The nested-select test now exercises data
an erroneous trim would empty.
perf(group): evaluate a derived symbol key in chunks over the vocabulary bytes
perf(sort): bounded-heap ordering compares FILE-domain symbols by raw bytes
perf(group): scale grouping with cores; v2 owns top-N and bounded emit
@singaraiona
singaraiona merged commit 2b10670 into master Sep 19, 2026
12 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