Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
bfc4d80
fix(store): order integer partitions by value; reject names past int64
belowzeroff Sep 15, 2026
22384c1
fix(store): reject integer partition names past int64 as corrupt
belowzeroff Sep 15, 2026
d45be6d
perf(engine): put per-query scratch on the buddy heap, gate the filte…
singaraiona Sep 15, 2026
2f894be
Merge pull request #530 from belowzeroff/fix/part-int-dir-order
singaraiona Sep 15, 2026
70a539e
Merge pull request #532 from RayforceDB/perf/engine-scratch-on-heap
singaraiona Sep 15, 2026
ac1b229
fix(aggr): establish type contracts and repair narrow group outputs
singaraiona Sep 15, 2026
86164e3
perf(expr): drop the per-query SYM null scan for null-safe comparison…
singaraiona Sep 15, 2026
acac21b
perf(aggr): complete typed query fast paths
singaraiona Sep 15, 2026
e00bdef
fix(query): preserve dense grouping with full default worker pool
singaraiona Sep 15, 2026
b851eca
fix(expr): chunk-zone all-fail arms must keep null rows for NE / LT / LE
singaraiona Sep 15, 2026
b1654be
Merge remote-tracking branch 'origin/dev' into perf/sym-null-proof-533
singaraiona Sep 15, 2026
0ad23e9
perf(mem): replace stale direct-cache blocks within the byte budget
singaraiona Sep 15, 2026
3018812
test(expr): drive fused_pred zone arms per row now that dev admits nu…
singaraiona Sep 15, 2026
bb81e62
Merge pull request #534 from RayforceDB/perf/sym-null-proof-533
singaraiona Sep 15, 2026
c9a54bf
fix(eval): reset call_lambda's cached `self` sym ID with the runtime
ser-vasilich Sep 15, 2026
bf324a3
perf(query): write a keyed upsert of a vector payload column by colum…
ser-vasilich Sep 16, 2026
21dba3f
perf(query): carry the key hash in the upsert map slot and grow from …
ser-vasilich Sep 16, 2026
ac880d9
Merge pull request #537 from RayforceDB/perf/upsert-vector-535
singaraiona Sep 16, 2026
28850e1
Merge pull request #536 from RayforceDB/fix/lambda-self-sym-stale
singaraiona Sep 16, 2026
917d667
fix(query): re-place the upsert key map from a chain start when it grows
ser-vasilich Sep 16, 2026
cb6b037
Merge pull request #538 from RayforceDB/fix/ukey-grow-dup-order
singaraiona Sep 16, 2026
a6b6d7d
perf(aggr): share grouping work across aggregate families
singaraiona Sep 15, 2026
06dc246
perf(aggr): parallelize dominant-group consumers
singaraiona Sep 15, 2026
2988a5d
perf(aggr): balance grouped consumers and reduce scratch work
singaraiona Sep 15, 2026
5395e35
perf(aggr): reduce shared directory allocation and first-touch conten…
singaraiona Sep 15, 2026
306f803
perf(aggr): specialize native validity and balance small dense tasks
singaraiona Sep 16, 2026
1a069f5
perf(aggr): emit native results across streaming aggregates
singaraiona Sep 16, 2026
591421e
perf(aggr): complete mixed scatter records in bounded chunks
singaraiona Sep 16, 2026
7dc5d2a
perf(aggr): split medium rank groups at the parallel grain
singaraiona Sep 16, 2026
f61a4ed
docs(aggr): record grouping acceptance evidence
singaraiona Sep 16, 2026
0f19f38
test(journal): cover snapshot function restart
belowzeroff Sep 16, 2026
7f1865a
docs(aggr): finalize grouping scaling validation
singaraiona Sep 16, 2026
2491704
fix(aggr): address grouped query audit regressions
singaraiona Sep 16, 2026
fdf365f
test(aggr): verify dense keys without assuming output order
singaraiona Sep 16, 2026
ffe4ad8
docs(aggr): record audit remediation validation
singaraiona Sep 16, 2026
8ab7c40
fix(sort): synchronize parallel sortedness detection
singaraiona Sep 16, 2026
8efdaeb
Merge pull request #539 from belowzeroff/fix/snapshot-function-restar…
singaraiona Sep 16, 2026
1915461
fix(aggr): clarify atomic pointer declarations for cppcheck
singaraiona Sep 16, 2026
262a876
perf(ipc): batch-intern SYM decode, frame buffers on the buddy heap
singaraiona Sep 16, 2026
fa21e3d
perf(ipc): never compress loopback and UNIX-domain links
singaraiona Sep 16, 2026
61c0d6b
feat(ipc): .ipc.open options dict with a compress threshold
singaraiona Sep 16, 2026
1a441e4
test(ipc): wire-level characterization before the server refactor
singaraiona Sep 16, 2026
886bc14
refactor(ipc): delete the second server implementation
singaraiona Sep 16, 2026
f051617
fix(ipc): validate all option key forms, restore compressed-frame cov…
singaraiona Sep 16, 2026
bd0fcfe
Merge pull request #543 from RayforceDB/perf/ipc-decode-542
singaraiona Sep 16, 2026
007a7b6
Merge pull request #544 from RayforceDB/feat/ipc-compress-541
singaraiona Sep 16, 2026
fc2cbaf
fix(sort): derive the radix key width against keys[0] in every task
ser-vasilich Sep 17, 2026
fdb8e5a
fix(query): keep every aggregate column when a grouped select matches…
ser-vasilich Sep 17, 2026
6466251
Merge pull request #540 from RayforceDB/perf/grouping-engine-scaling
singaraiona Sep 17, 2026
7176e03
Merge pull request #545 from RayforceDB/fix/group-sort-followups
singaraiona Sep 17, 2026
5956d2a
fix(pool): run dispatch_n past the task ring in rounds instead of dro…
ser-vasilich Sep 17, 2026
d020eed
fix(query): keep literal projection columns when a grouped select mat…
singaraiona Sep 17, 2026
1096b0c
Merge remote-tracking branch 'origin/perf/ipc-decode-542' into feat/i…
singaraiona Sep 17, 2026
fd2e61d
Merge pull request #549 from RayforceDB/feat/ipc-compress-541-to-dev
singaraiona Sep 17, 2026
000eeeb
Merge pull request #548 from RayforceDB/fix/zero-group-literal-projec…
singaraiona Sep 17, 2026
257c5e4
fix(query): don't deref a declined upsert key map; correct agg-v2 doc
singaraiona Sep 17, 2026
7ebb04e
fix(sort): keep equal keys in source order on every sort path
ser-vasilich Sep 17, 2026
94702a2
test(group): drive the fused count-distinct kernel with nulls and floats
singaraiona Sep 17, 2026
df301d4
fix(pivot): emit index rows and pivot columns in first-seen order
ser-vasilich Sep 17, 2026
6719bc5
perf(sort): stable bucket scatter for STR keys instead of the in-plac…
ser-vasilich Sep 17, 2026
6188a60
feat(mcast): one framing per distinct subscriber compression policy
singaraiona Sep 17, 2026
3e1a97b
fix(agg): pearson_corr must be null when a side has no variance
singaraiona Sep 17, 2026
c03d6ba
fix(agg): keep the first-seen order compaction serial rather than rel…
singaraiona Sep 17, 2026
f3cac07
fix(agg): numerically stable grouped variance and stddev
singaraiona Sep 17, 2026
43be3b4
fix(mcast): a framing failure must not drop subscribers or report suc…
singaraiona Sep 17, 2026
8a14efc
Merge pull request #550 from RayforceDB/fix/audit-followups-547
singaraiona Sep 17, 2026
fb25783
Merge remote-tracking branch 'origin/dev' into fix/compact-serial-556
singaraiona Sep 17, 2026
fcbdaae
Merge pull request #559 from RayforceDB/fix/compact-serial-556
singaraiona Sep 17, 2026
7175db4
Merge remote-tracking branch 'origin/dev' into fix/pearson-degenerate…
singaraiona Sep 17, 2026
4649990
Merge pull request #558 from RayforceDB/fix/pearson-degenerate-555
singaraiona Sep 17, 2026
63f75ea
Merge remote-tracking branch 'origin/dev' into fix/stable-variance-554
singaraiona Sep 17, 2026
5f4f524
Merge pull request #552 from RayforceDB/fix/pool-dispatch-rounds
singaraiona Sep 17, 2026
8a2b0ea
Merge pull request #557 from RayforceDB/feat/mcast-compress-buckets-551
singaraiona Sep 17, 2026
107591a
Merge pull request #553 from RayforceDB/fix/sort-stability-pivot-order
singaraiona Sep 17, 2026
27cd398
Merge pull request #560 from RayforceDB/fix/stable-variance-554
singaraiona Sep 17, 2026
21d36b4
perf(serde): cache SYM id -> (bytes, len) per thread when encoding
singaraiona Sep 17, 2026
1388b33
Merge pull request #562 from RayforceDB/perf/sym-encode-batch-561
singaraiona Sep 17, 2026
e45226f
fix(query): admit a symbol-vector by: to the count-distinct rewrite
ser-vasilich Sep 17, 2026
75bd546
test: count-distinct vector by — build the probe index list with (lis…
ser-vasilich Sep 17, 2026
c279e4f
perf(select): evaluate a single-SYM-column group key once per distinc…
ser-vasilich Sep 17, 2026
a61b0be
perf(select): bail out of the per-symbol key path after one block on …
ser-vasilich Sep 17, 2026
b3166f3
fix(select): admit only elementwise key expressions to the per-symbol…
ser-vasilich Sep 17, 2026
380d785
fix(select): probe before allocating, reject quoted column names, kee…
ser-vasilich Sep 17, 2026
766759e
fix(select): rename only an expression key — a bare column symbol kee…
ser-vasilich Sep 17, 2026
2ff2db1
fix(select): per-symbol key path — shadow-aware allowlist, route coun…
ser-vasilich Sep 17, 2026
fee4982
fix(select): mask the probe's first slot; cap the slot count at int32
ser-vasilich Sep 17, 2026
9caaf19
Merge pull request #564 from RayforceDB/perf/derived-key-sym-domain
singaraiona Sep 17, 2026
c27fdc3
Merge pull request #563 from RayforceDB/fix/count-distinct-vector-by
singaraiona Sep 17, 2026
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
230 changes: 230 additions & 0 deletions bench/groupby_shapes/grouping_scaling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""Portable grouping-family scaling matrix, with typed baseline comparisons."""
import argparse
import csv
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import statistics
import subprocess
import tempfile

from scaling_common import compare_csv, profile_phases

# (preparation, select fields, keys, optional selection)
CASES = {}
for kind in ("BOOL", "U8", "I16", "I32", "I64", "F32", "F64", "TIME"):
source = "(> v 0)" if kind == "BOOL" else "(as 'U8 (+ v 50))" if kind == "U8" else f"(as '{kind} v)"
# F32 conversion is admitted by the expression materializer.
prep = f"(set t (select {{from:t i:i k:k j:j v:{source} f:f w:w}}))"
CASES[f"sum-{kind.lower()}"] = (prep, "s:(sum v)", "k", "")
CASES[f"extrema-{kind.lower()}"] = (prep, "s:(min v) hi:(max v)", "k", "")
CASES.update({
"count": ("", "s:(count v)", "k", ""),
"statistics": ("", "s:(avg f) v:(var f) sd:(stddev f)", "k", ""),
"binary": ("", "s:(wsum f w) a:(wavg f w) p:(pearson_corr f w) c:(cov f w)", "k", ""),
"truth": ("", "s:(all v) a:(any v)", "k", ""),
"product": ("(set t (update {from:t v:(+ (% i 2) -1)}))", "s:(prod v)", "k", ""),
"median": ("", "s:(med v)", "k", ""),
"quantile": ("", "s:(quantile v 0.25)", "k", ""),
"mode": ("", "s:(mode v)", "k", ""),
"top-bottom": ("", "s:(top v 3) b:(bot v 3)", "k", ""),
"first-last": ("", "s:(first v) z:(last v)", "k", ""),
"mixed": ("", "s:(sum v) m:(med v) a:(avg f)", "k", ""),
"distinct": ("", "s:(count (distinct v))", "k", ""),
"composite": ("", "s:(sum v) m:(min v)", "[k j]", ""),
"selected": ("", "s:(sum v) m:(min v)", "k", "where:(> v 25)"),
"selected-indexed": ("", "s:(sum v) m:(med v)", "[k j]", "where:(> v 25)"),
"sparse": ("(set t (select {from:t i:i k:(* (as 'I64 k) 1000000007) j:j v:v f:f w:w}))", "s:(sum v)", "k", ""),
"clustered": ("(set t (update {from:t k:(as 'I32 (/ i 8))}))", "s:(sum v)", "k", ""),
"skew": ("(set t (update {from:t k:(as 'I32 0N) where:(< (% i 10) 4)}))", "s:(sum v)", "k", ""),
"skew-indexed": ("(set t (update {from:t k:(as 'I32 0N) where:(< (% i 10) 4)}))", "s:(sum v) m:(med v)", "k", ""),
"float-key": ("(set t (select {from:t i:i k:(as 'F64 k) j:j v:v f:f w:w}))", "s:(sum v)", "k", ""),
"float-indexed": ("(set t (select {from:t i:i k:(as 'F64 k) j:j v:v f:f w:w}))", "s:(sum v) m:(med v)", "k", ""),
"symbol-key": ("(set t (table [i k j v f w] (list i (at ['a 'b 'c 'd] (% i 4)) (at t 'j) (at t 'v) (at t 'f) (at t 'w))))", "s:(sum v)", "k", ""),
"string-key": ('(set t (table [i k j v f w] (list i (at ["pooled long alpha value" "beta" "gamma" "delta"] (% i 4)) (at t \'j) (at t \'v) (at t \'f) (at t \'w))))', "s:(sum v)", "k", ""),
"string-indexed": ('(set t (table [i k j v f w] (list i (at ["pooled long alpha value" "beta" "gamma" "delta"] (% i 4)) (at t \'j) (at t \'v) (at t \'f) (at t \'w))))', "s:(sum v) m:(med v)", "k", ""),
"wide-extrema": ('(set t (table [i k j v f w text] (list i (at t \'k) (at t \'j) (at t \'v) (at t \'f) (at t \'w) (at ["pooled long alpha value" "beta" "gamma" "delta"] (% i 4)))))', "s:(min text) z:(max text)", "k", ""),
})
for kind in ("DATE", "TIMESTAMP"):
CASES[f"extrema-{kind.lower()}"] = (
f"(set t (select {{from:t i:i k:k j:j v:(as '{kind} v) f:f w:w}}))",
"s:(min v) hi:(max v)", "k", "")
for name, fields in (("wide-mode", "s:(mode text)"),
("wide-top-bottom", "s:(top text 3) b:(bot text 3)")):
CASES[name] = (CASES["wide-extrema"][0], fields, "k", "")
for name, value in (("extrema-ascending", "i"), ("extrema-descending", "(- 0 i)")):
CASES[name] = (f"(set t (select {{from:t i:i k:k j:j v:(as 'I32 {value}) f:f w:w}}))",
"s:(min v) hi:(max v)", "k", "")

CASES["few-groups-indexed"] = (
"(set t (update {from:t k:(as 'I32 (% i 7))}))",
"n:(count v) f:(first v) l:(last v)", "k", "")

CASES["hot-top-ascending"] = (
"(set t (select {from:t i:i k:(as 'I32 (* i 0)) j:j v:i f:f w:w}))",
"s:(top v 3) b:(bot v 3)", "k", "")

CASES["guid-key"] = (
"(set g (as 'GUID (list \"00000000-0000-0000-0000-000000000001\" \"00000000-0000-0000-0000-000000000002\" \"00000000-0000-0000-0000-000000000003\" \"\")))\n"
"(set t (table [i k j v f w] (list i (at g (% i 4)) (at t 'j) (at t 'v) (at t 'f) (at t 'w))))",
"s:(sum v) m:(med v)", "k", "")
CASES["list-key"] = (
"(set g (list [1 2] [3 4] ['a 'b] [\"pooled list string\" \"x\"]))\n"
"(set t (table [i k j v f w] (list i (at g (% i 4)) (at t 'j) (at t 'v) (at t 'f) (at t 'w))))",
"s:(sum v) m:(med v)", "k", "")
CASES["unique-keys"] = ("(set t (update {from:t k:(as 'I32 i)}))", "s:(sum v)", "k", "")
CASES["nonnull"] = ("(set t (update {from:t v:(- (% i 101) 50)}))", "s:(sum v) a:(avg v)", "k", "")

for direction, value in (("ascending", "i"), ("descending", "(- 0 i)")):
CASES[f"hot-min-{direction}"] = (
f"(set t (select {{from:t i:i k:k j:j v:(as 'I32 {value}) f:f w:w}}))\n"
"(set t (update {from:t k:(as 'I32 0) where:(== (% i 2) 0)}))",
"s:(min v)", "k", "")
CASES["all-null-key"] = ("(set t (update {from:t k:(as 'I32 0N)}))", "s:(sum v) a:(avg v)", "k", "")

CASES["distinct-skew"] = (
"(set t (update {from:t k:(as 'I32 0N) v:0 where:(< (% i 10) 4)}))",
"s:(count (distinct v))", "k", "")
CASES["distinct-correlated"] = ("", "s:(count (distinct k))", "k", "")
CASES["symbol-extrema"] = (CASES["wide-extrema"][0].replace('["pooled long alpha value" "beta" "gamma" "delta"]', "['alpha 'beta 'gamma 'delta]"), "s:(min text) z:(max text)", "k", "")
CASES["symbol-top-bottom"] = (CASES["symbol-extrema"][0], "s:(top text 3) z:(bot text 3)", "k", "")
CASES["distinct-string-values"] = (CASES["wide-extrema"][0], "s:(count (distinct text))", "k", "")
for kind in ("string", "guid", "list"):
CASES[f"distinct-{kind}-keys"] = (CASES[f"{kind}-key"][0], "s:(count (distinct v))", "k", "")
CASES[f"mixed-distinct-{kind}-keys"] = (
CASES[f"{kind}-key"][0], "s:(count (distinct v)) total:(sum v)", "k", "")
CASES["mixed-distinct-string-values"] = (
CASES["wide-extrema"][0], "s:(count (distinct text)) total:(sum v)", "k", "")

# A single large group must share work across workers too.
for family in ("first-last", "symbol-extrema", "wide-extrema", "mode",
"wide-mode", "top-bottom", "wide-top-bottom", "symbol-top-bottom"):
prep, fields, keys, selection = CASES[family]
prep += "\n(set t (update {from:t k:(as 'I32 0)}))"
if family == "first-last":
prep += "\n(set t (update {from:t v:0N}))"
CASES[f"hot-{family}"] = (prep, fields, keys, selection)

CASES["hot-max-k"] = ("(set t (update {from:t k:(as 'I32 0)}))",
"s:(top v 1024) b:(bot v 1024)", "k", "")


def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--binary", type=Path, required=True)
ap.add_argument("--baseline", type=Path)
ap.add_argument("--baseline-skip-cases", default="",
help="cases with a recorded baseline failure; current results still checked across workers")
ap.add_argument("--workers", default="1,2,4,8,16,default")
ap.add_argument("--rounds", type=int, default=3)
ap.add_argument("--rows", type=int, default=1000000)
ap.add_argument("--cases", default=",".join(CASES))
ap.add_argument("--profile", action="store_true")
ap.add_argument("--stop-file", type=Path, help="stop between processes when this file exists")
ap.add_argument("--output", type=Path, required=True)
args = ap.parse_args()
cases, workers = args.cases.split(","), args.workers.split(",")
baseline_skip = set(filter(None, args.baseline_skip_cases.split(",")))
if any(c not in CASES for c in cases) or args.rounds < 1 or args.rows < 1:
ap.error("invalid cases, rounds or rows")
if any(w != "default" and (not w.isdecimal() or int(w) < 1) for w in workers):
ap.error("worker counts must be positive or default")
binaries = [("current", args.binary.resolve())]
if args.baseline:
binaries.insert(0, ("baseline", args.baseline.resolve()))
hashes = {name: hashlib.sha256(binary.read_bytes()).hexdigest() for name, binary in binaries}
env = os.environ.copy()
env.pop("RAYFORCE_CORES", None)
log_dir = args.output.with_suffix(".runs")
log_dir.mkdir(parents=True, exist_ok=True)
if baseline_skip - CASES.keys():
ap.error("unknown baseline skip case")
matrix = [(case, worker, name, binary) for case in cases for worker in workers for name, binary in binaries
if not (name == "baseline" and case in baseline_skip)]
records = []
with tempfile.TemporaryDirectory(prefix="rayforce-grouping-") as temp:
directory = Path(temp)
script, result, schema, rss, input_schema = (directory / f for f in ("query.rfl", "result.csv", "schema.csv", "rss", "input-schema.csv"))
warm_result, warm_schema = directory / "warm-result.csv", directory / "warm-schema.csv"
references = set()
for round_number in range(1, args.rounds + 1):
for case, worker, name, binary in matrix if round_number % 2 else reversed(matrix):
if args.stop_file and args.stop_file.exists():
print(f"Stopped between processes; {len(records)} verified runs retained in {args.output}")
return
prep, fields, keys, selection = CASES[case]
key_list = "['k 'j]" if keys == "[k j]" else "['k]"
query = f"(select {{from:t by:{keys} {fields} {selection}}})"
setup = f"""(set i (til {args.rows}))
(set v (- (% i 101) 50))
(set t (table [i k j v f w] (list i (as 'I32 (% (* i 17) 65536)) (as 'I32 (% i 7)) v (as 'F64 v) (as 'F64 (+ (% i 31) 1)))))
(set t (update {{from:t v:0N where:(== (% i 97) 0)}}))
{prep}
"""
text = setup + f"(println (count t))\n(println (timeit (set r {query})))\n(println (count r))\n"
text += (f"(println (timeit {query}))\n" * 5)
text += f"(set ordered (xasc r {key_list}))\n(.csv.write ordered {json.dumps(str(result))})\n"
# Untimed re-execution checks that warming does not change results.
text += f"(set warm_ordered (xasc {query} {key_list}))\n(.csv.write warm_ordered {json.dumps(str(warm_result))})\n"
text += "(.csv.write (table [column kind] (list (key warm_ordered) (map (fn [c] (at (meta (at warm_ordered c)) 'type)) (key warm_ordered)))) " + json.dumps(str(warm_schema)) + ")\n"
text += "(.csv.write (table [column kind] (list (key ordered) (map (fn [c] (at (meta (at ordered c)) 'type)) (key ordered)))) " + json.dumps(str(schema)) + ")\n"
text += "(.csv.write (table [column kind] (list (key t) (map (fn [c] (at (meta (at t c)) 'type)) (key t)))) " + json.dumps(str(input_schema)) + ")\n"
script.write_text(text)
for p in (result, schema, warm_result, warm_schema, input_schema, rss):
p.unlink(missing_ok=True)
command = ["/usr/bin/time", "-f", "%M", "-o", str(rss), str(binary), str(script)]
if worker != "default":
command += ["-c", worker]
if args.profile:
command += ["-t", "1"]
run = subprocess.run(command, env=env, capture_output=True, text=True)
(log_dir / f"{case}-{worker}-{name}-{round_number}.log").write_text(run.stdout + run.stderr)
if run.returncode:
raise RuntimeError(f"{case} failed: {run.stdout}\n{run.stderr}")
values = [float(line) for line in run.stdout.splitlines() if re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", line.strip())]
if len(values) < 8 or not all(p.is_file() for p in (result, schema, warm_result, warm_schema, input_schema)):
raise RuntimeError(f"Incomplete {case}: {run.stdout}\n{run.stderr}")
reference, ref_schema = directory / f"{case}.csv", directory / f"{case}-schema.csv"
if case not in references:
shutil.copyfile(result, reference)
shutil.copyfile(schema, ref_schema)
references.add(case)
if schema.read_bytes() != warm_schema.read_bytes():
raise RuntimeError(f"Cold/warm result types differ for {case}")
compare_csv(result, warm_result, schema, key_names=("k", "j"))
if schema.read_bytes() != ref_schema.read_bytes():
raise RuntimeError(f"Result types differ for {case}")
try:
compare_csv(reference, result, schema, key_names=("k", "j"))
except RuntimeError:
shutil.copyfile(reference, log_dir / "mismatch-reference.csv")
shutil.copyfile(result, log_dir / "mismatch-current.csv")
shutil.copyfile(schema, log_dir / "mismatch-schema.csv")
raise
with input_schema.open(newline="") as stream:
input_types = {r["column"]: r["kind"] for r in csv.DictReader(stream)}
if case.startswith("string-") and input_types["k"] != "STR":
raise RuntimeError("String fixture did not produce STR keys")
if case == "symbol-key" and input_types["k"] != "SYM":
raise RuntimeError("Symbol fixture did not produce SYM keys")
if case in ("guid-key", "list-key") and input_types["k"] != case.split("-")[0].upper():
raise RuntimeError("Wide key fixture has wrong input type")
if case in ("symbol-extrema", "symbol-top-bottom") and input_types["text"] != "SYM":
raise RuntimeError("Symbol fixture did not produce SYM values")
if case.startswith("wide-") and input_types["text"] != "STR":
raise RuntimeError("Wide fixture did not produce STR values")
records.append(dict(input_types=input_types, phases=profile_phases(run.stdout + run.stderr),
case=case, workers=worker, binary=name, path=str(binary), sha256=hashes[name], round=round_number,
rows=int(values[0]), groups=int(values[2]), cold_ms=values[1], warm_ms=values[3:8],
peak_rss_kib=int(rss.read_text()), result_sha256=hashlib.sha256(result.read_bytes()).hexdigest()))
args.output.write_text(json.dumps(records, indent=2) + "\n")
print(f"{case} {name} workers={worker} round={round_number}: cold={values[1]:.3f}, warm={statistics.median(values[3:8]):.3f} ms", flush=True)
print(f"Verified typed results for all {len(records)} runs.")


if __name__ == "__main__":
main()
8 changes: 8 additions & 0 deletions bench/groupby_shapes/nullable_time.rfl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
;; Synthetic nullable I32 keys with TIME values.
(set n 16000000)
(set group_count 1000000)
(set i (til n))
(set g (% (* i 17) group_count))
(set t (table [k tm] (list (as 'I32 (+ (* g 2) (div g 4))) (as 'TIME (% (* i 13) 86400000)))))
(set t (update {from:t k:(as 'I32 0N) where:(== k 0)}))
(map (fn [_] (println (timeit (select {from:t by:k s:(min tm)})))) (til 8))
24 changes: 24 additions & 0 deletions bench/groupby_shapes/results/2026-09-15/baseline-failures.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Baseline failures reproduced on bb81e621

Use the baseline binary identified in environment.json:
python3 bench/groupby_shapes/grouping_scaling.py --binary /path/to/baseline --workers 1 --rounds 1 --cases CASE --output /tmp/CASE.json

distinct-string-values:
=== rayforce fatal SIGSEGV at fault addr 0x0000000000000008 ===

mixed-distinct-string-values:
=== rayforce fatal SIGSEGV at fault addr 0x0000000000000000 ===

distinct-list-keys:
error: type: vec_new: type must be a positive concrete vector type, got LIST

mixed-distinct-list-keys:
error: type: vec_new: type must be a positive concrete vector type, got LIST

Direct indexed top/bottom-K kernel, nullable I64, K at group size:
Build topk_consumer.c against the baseline static library, then run:
RAYFORCE_CORES=1 /path/to/baseline-topk-consumer 10000 10000
Result: top/bottom K differs from histogram oracle
The native serial heap was not heapified when nulls left fewer than K values.
No baseline speedup is reported for this failing shape; candidate worker runs
are checked against the histogram oracle.
77 changes: 77 additions & 0 deletions bench/groupby_shapes/results/2026-09-15/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"kernel": "Linux-6.17.9-76061709-generic-x86_64-with-glibc2.35",
"cpu": {
"Architecture": "x86_64",
"CPU(s)": "28",
"On-line CPU(s) list": "0-27",
"Model name": "Intel(R) Core(TM) i7-14700",
"Thread(s) per core": "2",
"Core(s) per socket": "20",
"Socket(s)": "1",
"L2 cache": "28 MiB (11 instances)",
"L3 cache": "33 MiB (1 instance)"
},
"compiler": "cc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0",
"workers": [
"1",
"2",
"4",
"8",
"16",
"default"
],
"fresh_processes_per_case": 3,
"warm_executions_per_process": 5,
"baseline_revision": "bb81e6215321a7cf2cdd83d7223d62909804dee8",
"governor": "powersave",
"scaling_driver": "intel_pstate",
"candidate_revision": "f61a4eda",
"baseline_sha256": "f9ed90d41e7bff92707a796e4677772351a23e53a6e148cf831a3feb92ae880e",
"candidate_sha256": "1003a83830e8b12ae1cb86bdec98af3d649ae54404b59a1466ec31548b149353",
"build_flags": "-O3 -march=native -funroll-loops -fomit-frame-pointer -fno-math-errno -falign-functions=64 -fassociative-math -ffp-contract=fast -fno-signed-zeros -fno-trapping-math",
"physical_memory_kib": 65633828,
"allowed_cpus": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27
],
"kernel_baseline_sha256": "e174e083c2a474b6da59888401b742d784df685db28d8a0c63bf9e4ad424fe73",
"kernel_candidate_sha256": "96944ec28f56bdc442fbfc86b9578e9d553d97122976d3a2db6512fac1447825",
"acceptance_status": "complete",
"sanitizer_tests": "3828/3828",
"thread_sanitizer_tests": "24/24",
"kernel_oracle_runs": 90,
"thread_sanitizer_command": "RAYFORCE_CORES=3 setarch x86_64 -R ./rayforce.test.tsan -f agg_contract",
"synthetic_acceptance_status": "complete",
"synthetic_query_runs": 2700,
"synthetic_cases": 77,
"profile_runs": 68,
"integration_base_revision": "cb6b0371",
"kernel_candidate_revision": "f61a4eda",
"regression_repeat_runs": 130
}
Loading
Loading