Skip to content

Commit 024005e

Browse files
committed
ci(bench): compare V1 and V2 random access on a dedicated branch
Run the six-dataset, 48-case full-Arrow regression comparison on one c8gd.8xlarge runner. Build current V2 and the pre-splitting V2 checkpoint with the same Rust flags, then measure V1/current V2 from the same binary and previous V2 from its frozen binary against shared input files. Generate flat-list controls plus ListLayout variants with only element chunks targeted at 1 MiB or 64 KiB. Verify full Arrow parity of each rewrite. Measure 100 selected rows at 1 and 30 CPUs, clustered/uniform, cached/reopen, seeds 17/42/73, with alternating version order. Rerun borderline previous-V2 regressions with twice the queries and rounds. Upload raw samples, logs, input and binary hashes, JSON/CSV aggregates, layout metadata, and a job-summary performance table. The workflow runs only on pushes to ji/scan-v2-regression-ci or by manual dispatch; it does not change existing PR workflows or publish baseline benchmark results. GitHub API copies preserve each local checkpoint's exact source tree and signed-off message, but receive new commit IDs. The pinned CI baseline 1249ce4 has the same tree as 519f6ba46; the candidate runtime cce5661 has the same tree as eab30f057. Original checkpoints remain on the local ji/decompression-v2-scan branch. Validation: Ruff format/check, strict repository yamllint, whitespace check, and 18 real benchmark process smoke runs validating mode, selected indices, instrumentation, hashes, and JSON/CSV/Markdown output. No Rust code changes in this commit; full timing comparison runs in Actions. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent cce5661 commit 024005e

2 files changed

Lines changed: 324 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
name: Scan V2 Regression Comparison
5+
run-name: Scan V2 regression comparison on ${{ github.ref_name }}
6+
7+
on:
8+
push:
9+
branches: ["ji/scan-v2-regression-ci"]
10+
workflow_dispatch: { }
11+
12+
permissions:
13+
contents: read
14+
15+
concurrency:
16+
group: scan-v2-regression-${{ github.ref }}
17+
cancel-in-progress: false
18+
19+
jobs:
20+
compare:
21+
if: github.repository == 'vortex-data/vortex'
22+
timeout-minutes: 180
23+
runs-on: >-
24+
runs-on=${{ github.run_id }}/runner=bench-dedicated/family=c8gd.8xlarge/image=ubuntu24-full-arm64-pre-v2/extras=s3-cache/tag=scan-v2-regression
25+
env:
26+
# Same source tree as local checkpoint 519f6ba46, before the two list-splitting changes.
27+
BASELINE_COMMIT: "1249ce425d7371431ca201ac12f3369bfb0f1c56"
28+
CARGO_BUILD_JOBS: "8"
29+
CARGO_TARGET_DIR: ${{ github.workspace }}/target
30+
RUSTFLAGS: "-C target-cpu=native -C force-frame-pointers=yes"
31+
RUST_LOG: warn
32+
steps:
33+
- uses: runs-on/action@v2
34+
with:
35+
sccache: s3
36+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
37+
with:
38+
fetch-depth: 0
39+
- uses: ./.github/actions/setup-prebuild
40+
with:
41+
repo-token: ${{ secrets.GITHUB_TOKEN }}
42+
enable-sccache: "true"
43+
- uses: ./.github/actions/system-info
44+
45+
- name: Build current scan and data preparation binaries
46+
shell: bash
47+
run: |
48+
set -euo pipefail
49+
mkdir -p scan-v2-binaries scan-v2-results
50+
env -u VORTEX_SELECTION_METRICS cargo build --locked --profile release_debug \
51+
-p vortex --example selection_scan --example rewrite_list_layout
52+
cp target/release_debug/examples/selection_scan scan-v2-binaries/current
53+
cp target/release_debug/examples/rewrite_list_layout scan-v2-binaries/rewrite
54+
env -u VORTEX_SELECTION_METRICS cargo build --locked --profile release_debug \
55+
-p random-access-bench
56+
57+
- name: Build previous V2 from the pinned checkpoint
58+
shell: bash
59+
run: |
60+
set -euo pipefail
61+
git worktree add --detach "$RUNNER_TEMP/scan-v2-baseline" "$BASELINE_COMMIT"
62+
(
63+
cd "$RUNNER_TEMP/scan-v2-baseline"
64+
env -u VORTEX_SELECTION_METRICS cargo build --locked --profile release_debug \
65+
-p vortex --example selection_scan
66+
)
67+
cp target/release_debug/examples/selection_scan scan-v2-binaries/baseline
68+
sha256sum scan-v2-binaries/* > scan-v2-results/binaries.sha256
69+
70+
- name: Prepare shared datasets and verify ListLayout rewrites
71+
shell: bash
72+
env:
73+
VORTEX_SCAN_V2: "0"
74+
run: |
75+
set -euo pipefail
76+
target/release_debug/random-access-bench --prepare-data --formats vortex
77+
mkdir -p scan-v2-data
78+
source_file=vortex-bench/data/random_access/nested_lists/nested_lists.vortex
79+
scan-v2-binaries/rewrite "$source_file" scan-v2-data/flat-control.vortex flat \
80+
> scan-v2-results/layout-flat.json
81+
scan-v2-binaries/rewrite "$source_file" scan-v2-data/elements-default.vortex list-elements 1048576 \
82+
> scan-v2-results/layout-1MiB.json
83+
scan-v2-binaries/rewrite "$source_file" scan-v2-data/elements-64k.vortex list-elements 65536 \
84+
> scan-v2-results/layout-64KiB.json
85+
86+
- name: Isolate benchmark CPUs
87+
run: sudo bash scripts/setup-benchmark.sh
88+
89+
- name: Compare all 48 cases and repeat borderline regressions
90+
shell: bash
91+
env:
92+
CANDIDATE_COMMIT: ${{ github.sha }}
93+
run: |
94+
set -euo pipefail
95+
bash scripts/bench-taskset.sh python3 scripts/scan-v2-regression.py \
96+
--baseline scan-v2-binaries/baseline \
97+
--candidate scan-v2-binaries/current \
98+
--baseline-commit "$BASELINE_COMMIT" \
99+
--candidate-commit "$CANDIDATE_COMMIT" \
100+
| tee scan-v2-results/run.log
101+
102+
- name: Upload results, raw samples, and layout metadata
103+
if: always()
104+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
105+
with:
106+
name: scan-v2-regression-${{ github.sha }}
107+
path: scan-v2-results/
108+
if-no-files-found: warn

‎scripts/scan-v2-regression.py‎

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
"""Compare V1, previous V2, and current V2 with full Arrow materialization."""
5+
6+
import argparse
7+
import csv
8+
import hashlib
9+
import itertools
10+
import json
11+
import os
12+
import platform
13+
import statistics
14+
import subprocess
15+
from pathlib import Path
16+
17+
18+
def digest(path: Path) -> str:
19+
checksum = hashlib.sha256()
20+
with path.open("rb") as source:
21+
for chunk in iter(lambda: source.read(1024 * 1024), b""):
22+
checksum.update(chunk)
23+
return checksum.hexdigest()
24+
25+
26+
def change(current: float, baseline: float) -> float:
27+
return 100 * (current / baseline - 1)
28+
29+
30+
def measure(
31+
case: dict,
32+
versions: dict[str, tuple[Path, str]],
33+
files: dict[str, Path],
34+
cpus: list[int],
35+
output: Path,
36+
iterations: int,
37+
rounds: int,
38+
) -> dict:
39+
env = {k: v for k, v in os.environ.items() if not k.startswith(("VORTEX_", "TOKIO_"))}
40+
env["RUST_LOG"] = "warn"
41+
seeds = []
42+
for seed in [17, 42, 73]:
43+
medians = {v: [] for v in versions}
44+
expected_indices = None
45+
for repeat in range(rounds):
46+
order = list(versions)[:: 1 if repeat % 2 == 0 else -1]
47+
for version in order:
48+
binary, flag = versions[version]
49+
stem = "-".join(str(v) for v in (*case.values(), seed, repeat, version))
50+
command = [
51+
"taskset",
52+
"-c",
53+
",".join(map(str, cpus[: case["cores"]])),
54+
str(binary),
55+
str(files[case["dataset"]]),
56+
str(iterations),
57+
"100",
58+
case["pattern"],
59+
case["mode"],
60+
"disk",
61+
"decode",
62+
str(seed),
63+
]
64+
with (output / f"{stem}.json").open("w") as target, (output / f"{stem}.log").open("w") as log:
65+
subprocess.run(
66+
command, env=dict(env, VORTEX_SCAN_V2=flag), stdout=target, stderr=log, check=True, timeout=180
67+
)
68+
result = json.loads((output / f"{stem}.json").read_text())
69+
if result["v2"] != flag or len(result["runs"]) != iterations:
70+
raise RuntimeError(f"Wrong scan mode or sample count: {stem}")
71+
if any(run["instrumented"] for run in result["runs"]):
72+
raise RuntimeError(f"Instrumented binary used for timing: {stem}")
73+
if expected_indices is None:
74+
expected_indices = result["indices"]
75+
if result["indices"] != expected_indices:
76+
raise RuntimeError(f"Versions selected different rows: {stem}")
77+
samples = result["runs"][iterations // 8 :]
78+
medians[version].append(statistics.median(x["elapsed_ns"] for x in samples) / 1e6)
79+
times = {v: statistics.median(values) for v, values in medians.items()}
80+
seeds.append(dict(seed=seed, **times, rounds=medians))
81+
row = dict(case, **{v + "_ms": statistics.median(s[v] for s in seeds) for v in versions}, seeds=seeds)
82+
row["vs_old_v2_pct"] = change(row["new_v2_ms"], row["old_v2_ms"])
83+
if "v1" in versions:
84+
row["vs_v1_pct"] = change(row["new_v2_ms"], row["v1_ms"])
85+
print(json.dumps({k: v for k, v in row.items() if k != "seeds"}), flush=True)
86+
return row
87+
88+
89+
def save(output: Path, metadata: dict, summary: list[dict], confirmations: list[dict]):
90+
(output / "results.json").write_text(
91+
json.dumps(dict(metadata, summary=summary, confirmations=confirmations), indent=2) + "\n"
92+
)
93+
with (output / "results.csv").open("w") as target:
94+
writer = csv.DictWriter(target, fieldnames=[k for k in summary[0] if k != "seeds"])
95+
writer.writeheader()
96+
writer.writerows({k: v for k, v in row.items() if k != "seeds"} for row in summary)
97+
98+
99+
def report(output: Path, metadata: dict, summary: list[dict], confirmations: list[dict]):
100+
lines = [
101+
"# Scan V2 regression comparison",
102+
"",
103+
f"Candidate: `{metadata['candidate_commit']}`; previous V2: `{metadata['baseline_commit']}`.",
104+
"",
105+
"100 selected rows; full Arrow decode/drop; warm local disk. "
106+
"Cached reuses file preparation; reopen includes opening/metadata.",
107+
"Three seeds, two alternating rounds of 64 queries, first 8 discarded. Positive percentages mean slower.",
108+
"",
109+
f"Cases above +3% vs previous V2: {sum(r['vs_old_v2_pct'] > 3 for r in summary)}/{len(summary)}; "
110+
f"above +5% vs V1: {sum(r['vs_v1_pct'] > 5 for r in summary)}/{len(summary)}.",
111+
"",
112+
"| Dataset | Cores | Selection | Open | V1 ms | Previous V2 ms | Current V2 ms | vs V1 | vs previous V2 |",
113+
"|---|---:|---|---|---:|---:|---:|---:|---:|",
114+
]
115+
for r in summary:
116+
lines.append(
117+
f"| {r['dataset']} | {r['cores']} | {r['pattern']} | {r['mode']} | {r['v1_ms']:.4f} | "
118+
f"{r['old_v2_ms']:.4f} | {r['new_v2_ms']:.4f} | {r['vs_v1_pct']:+.1f}% | {r['vs_old_v2_pct']:+.1f}% |"
119+
)
120+
if confirmations:
121+
lines += [
122+
"",
123+
"## Longer reruns",
124+
"",
125+
"Rerun when the first-pass aggregate exceeds +2%, or any seed exceeds +3%, versus previous V2.",
126+
"Four alternating rounds of 128 queries, first 16 discarded, same three seeds.",
127+
"",
128+
"| Dataset | Cores | Selection | Open | Previous V2 ms | Current V2 ms | Change |",
129+
"|---|---:|---|---|---:|---:|---:|",
130+
]
131+
for r in confirmations:
132+
lines.append(
133+
f"| {r['dataset']} | {r['cores']} | {r['pattern']} | {r['mode']} | "
134+
f"{r['old_v2_ms']:.4f} | {r['new_v2_ms']:.4f} | {r['vs_old_v2_pct']:+.1f}% |"
135+
)
136+
text = "\n".join(lines) + "\n"
137+
(output / "summary.md").write_text(text)
138+
if step_summary := os.environ.get("GITHUB_STEP_SUMMARY"):
139+
with Path(step_summary).open("a") as target:
140+
target.write(text)
141+
142+
143+
def main():
144+
parser = argparse.ArgumentParser(description=__doc__)
145+
parser.add_argument("--baseline", type=Path, required=True)
146+
parser.add_argument("--candidate", type=Path, required=True)
147+
parser.add_argument("--baseline-commit", required=True)
148+
parser.add_argument("--candidate-commit", required=True)
149+
parser.add_argument("--data-dir", type=Path, default=Path("vortex-bench/data"))
150+
parser.add_argument("--list-dir", type=Path, default=Path("scan-v2-data"))
151+
parser.add_argument("--output", type=Path, default=Path("scan-v2-results"))
152+
args = parser.parse_args()
153+
args.output.mkdir(parents=True, exist_ok=True)
154+
raw = args.output / "raw"
155+
raw.mkdir(exist_ok=True)
156+
cpus = sorted(os.sched_getaffinity(0))
157+
if len(cpus) < 30:
158+
raise RuntimeError(f"This comparison requires 30 available CPUs; found {len(cpus)}")
159+
files = {
160+
"taxi": args.data_dir / "taxi/taxi.vortex",
161+
"feature-vectors": args.data_dir / "random_access/feature_vectors/feature_vectors.vortex",
162+
"nested-structs": args.data_dir / "random_access/nested_structs/nested_structs.vortex",
163+
"flat-lists": args.list_dir / "flat-control.vortex",
164+
"list-1MiB": args.list_dir / "elements-default.vortex",
165+
"list-64KiB": args.list_dir / "elements-64k.vortex",
166+
}
167+
versions = {
168+
"v1": (args.candidate.resolve(), "0"),
169+
"old_v2": (args.baseline.resolve(), "1"),
170+
"new_v2": (args.candidate.resolve(), "1"),
171+
}
172+
metadata = dict(
173+
baseline_commit=args.baseline_commit,
174+
candidate_commit=args.candidate_commit,
175+
platform=platform.platform(),
176+
cpus=cpus[:30],
177+
method=dict(
178+
rows=100,
179+
seeds=[17, 42, 73],
180+
iterations=64,
181+
warmups=8,
182+
rounds=2,
183+
output="full Arrow decode and drop",
184+
storage="warm local disk",
185+
aggregation="median of round medians, then median across seeds",
186+
confirmation_iterations=128,
187+
confirmation_warmups=16,
188+
confirmation_rounds=4,
189+
),
190+
files={k: dict(path=str(p), sha256=digest(p), bytes=p.stat().st_size) for k, p in files.items()},
191+
versions={k: dict(binary=str(p), sha256=digest(p), scan_v2=f) for k, (p, f) in versions.items()},
192+
)
193+
(args.output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")
194+
summary = []
195+
confirmations = []
196+
for cores, dataset, pattern, mode in itertools.product(
197+
[1, 30], files, ["clustered", "uniform"], ["cached", "reopen"]
198+
):
199+
case = dict(dataset=dataset, cores=cores, pattern=pattern, mode=mode)
200+
summary.append(measure(case, versions, files, cpus, raw, 64, 2))
201+
save(args.output, metadata, summary, confirmations)
202+
reruns = args.output / "reruns"
203+
reruns.mkdir(exist_ok=True)
204+
for row in summary:
205+
if row["vs_old_v2_pct"] <= 2 and all(change(s["new_v2"], s["old_v2"]) <= 3 for s in row["seeds"]):
206+
continue
207+
case = {k: row[k] for k in ["dataset", "cores", "pattern", "mode"]}
208+
confirmations.append(
209+
measure(case, {k: v for k, v in versions.items() if k != "v1"}, files, cpus, reruns, 128, 4)
210+
)
211+
save(args.output, metadata, summary, confirmations)
212+
report(args.output, metadata, summary, confirmations)
213+
214+
215+
if __name__ == "__main__":
216+
main()

0 commit comments

Comments
 (0)