From 920473e47c38fb03a4d345f560aea0d26eb2fdc6 Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Thu, 3 Sep 2026 09:54:08 +0000 Subject: [PATCH 1/4] Move benchmark runners to c7gd.metal (Graviton3) Switch every benchmark workflow that ran on c6id off x86 and onto c7gd: the label-triggered PR benchmarks (SQL presets, compress, random access, string), the post-merge develop benchmarks, and the nightly SQL matrix. The bench job runs on c7gd.metal and the binary is built on c7gd.8xlarge so `-C target-cpu=native` matches the benchmark host. Both jobs pin the arm64 image since `bench-dedicated` defaults to x64. `sql-bench-matrix.yml` gains `build_machine_type` and `machine_image` inputs alongside `machine_type` so a caller can still pick another architecture consistently; the nightly matrix passes all three. The setup-duckdb action now picks the DuckDB CLI archive by `uname -m` instead of hard-coding amd64. Signed-off-by: "Joe Isaacs" Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FUSFWCFtHuDq5uCqDgGm9i --- .github/actions/setup-duckdb/action.yml | 7 ++++++- .github/workflows/develop-bench.yml | 7 +++++-- .github/workflows/nightly-bench.yml | 8 ++++++-- .github/workflows/pr-bench-runner.yml | 7 +++++-- .github/workflows/sql-bench-matrix.yml | 17 ++++++++++++++--- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.github/actions/setup-duckdb/action.yml b/.github/actions/setup-duckdb/action.yml index 58964dda51d..38b97a26521 100644 --- a/.github/actions/setup-duckdb/action.yml +++ b/.github/actions/setup-duckdb/action.yml @@ -10,8 +10,13 @@ runs: - name: Download DuckDB shell: bash run: | + case "$(uname -m)" in + x86_64) arch=amd64 ;; + aarch64) arch=arm64 ;; + *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; + esac wget -qO- \ - "https://github.com/duckdb/duckdb/releases/download/v${{ inputs.duckdb_version }}/duckdb_cli-linux-amd64.zip" \ + "https://github.com/duckdb/duckdb/releases/download/v${{ inputs.duckdb_version }}/duckdb_cli-linux-${arch}.zip" \ | funzip > duckdb chmod +x duckdb echo "$PWD" >> "$GITHUB_PATH" diff --git a/.github/workflows/develop-bench.yml b/.github/workflows/develop-bench.yml index 203d34011d3..7d2dcc1ea70 100644 --- a/.github/workflows/develop-bench.yml +++ b/.github/workflows/develop-bench.yml @@ -31,11 +31,14 @@ jobs: bash scripts/commit-json.sh > new-commit.json bash scripts/cat-s3.sh vortex-ci-benchmark-results commits.json new-commit.json + # Benchmarks run on Graviton3 (c7gd.metal). The binary is built on the same CPU family so + # `-C target-cpu=native` matches, and both jobs pin the arm64 image because the + # `bench-dedicated` runner defaults to x64. build: timeout-minutes: 60 runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/runner=bench-dedicated/family=c6id.8xlarge/extras=s3-cache/tag=build-{1}', github.run_id, matrix.benchmark.id) + && format('runs-on={0}/runner=bench-dedicated/family=c7gd.8xlarge/image=ubuntu24-full-arm64-pre-v2/extras=s3-cache/tag=build-{1}', github.run_id, matrix.benchmark.id) || 'ubuntu-latest' }} strategy: fail-fast: false @@ -83,7 +86,7 @@ jobs: timeout-minutes: 120 runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/runner=bench-dedicated/family=c6id.metal/extras=s3-cache/tag={1}', github.run_id, matrix.benchmark.id) + && format('runs-on={0}/runner=bench-dedicated/family=c7gd.metal/image=ubuntu24-full-arm64-pre-v2/extras=s3-cache/tag={1}', github.run_id, matrix.benchmark.id) || 'ubuntu-latest' }} strategy: fail-fast: false diff --git a/.github/workflows/nightly-bench.yml b/.github/workflows/nightly-bench.yml index cc90e07316f..61b9b346d96 100644 --- a/.github/workflows/nightly-bench.yml +++ b/.github/workflows/nightly-bench.yml @@ -23,11 +23,15 @@ jobs: with: mode: "develop" machine_type: ${{ matrix.machine_type.instance_name }} + build_machine_type: ${{ matrix.machine_type.build_instance_name }} + machine_image: ${{ matrix.machine_type.image }} matrix_preset: "nightly" strategy: # A single run not should kill the others fail-fast: false matrix: machine_type: - - id: x86 - instance_name: c6id.metal + - id: arm64 + instance_name: c7gd.metal + build_instance_name: c7gd.8xlarge + image: ubuntu24-full-arm64-pre-v2 diff --git a/.github/workflows/pr-bench-runner.yml b/.github/workflows/pr-bench-runner.yml index e1fe46ef502..7828d3bbfdd 100644 --- a/.github/workflows/pr-bench-runner.yml +++ b/.github/workflows/pr-bench-runner.yml @@ -28,11 +28,14 @@ permissions: id-token: write # enables AWS-GitHub OIDC jobs: + # Benchmarks run on Graviton3 (c7gd.metal). The binary is built on the same CPU family so + # `-C target-cpu=native` matches, and both jobs pin the arm64 image because the + # `bench-dedicated` runner defaults to x64. build: timeout-minutes: 60 runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/runner=bench-dedicated/family=c6id.8xlarge/tag=build-{1}{2}', github.run_id, inputs.benchmark_id, github.event.pull_request.head.repo.fork == false && '/extras=s3-cache' || '') + && format('runs-on={0}/runner=bench-dedicated/family=c7gd.8xlarge/image=ubuntu24-full-arm64-pre-v2/tag=build-{1}{2}', github.run_id, inputs.benchmark_id, github.event.pull_request.head.repo.fork == false && '/extras=s3-cache' || '') || 'ubuntu-latest' }} steps: - uses: runs-on/action@v2 @@ -75,7 +78,7 @@ jobs: timeout-minutes: 120 runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/runner=bench-dedicated/family=c6id.metal/tag={1}{2}', github.run_id, inputs.benchmark_id, github.event.pull_request.head.repo.fork == false && '/extras=s3-cache' || '') + && format('runs-on={0}/runner=bench-dedicated/family=c7gd.metal/image=ubuntu24-full-arm64-pre-v2/tag={1}{2}', github.run_id, inputs.benchmark_id, github.event.pull_request.head.repo.fork == false && '/extras=s3-cache' || '') || 'ubuntu-latest' }} steps: # `extras=s3-cache` points ACTIONS_RESULTS_URL at the RunsOn artifact proxy, but only diff --git a/.github/workflows/sql-bench-matrix.yml b/.github/workflows/sql-bench-matrix.yml index cae3630d6b8..76a7c221933 100644 --- a/.github/workflows/sql-bench-matrix.yml +++ b/.github/workflows/sql-bench-matrix.yml @@ -10,10 +10,21 @@ on: required: true type: string description: "Named benchmark matrix to run" + # The three machine inputs must agree on CPU architecture: the build job's binaries + # run on `machine_type`, and `bench-dedicated` defaults to an x64 image, so an arm64 + # family needs the arm64 image spelled out. machine_type: required: false type: string - default: c6id.metal + default: c7gd.metal + build_machine_type: + required: false + type: string + default: c7gd.8xlarge + machine_image: + required: false + type: string + default: ubuntu24-full-arm64-pre-v2 jobs: resolve-matrix: @@ -50,7 +61,7 @@ jobs: FLAT_LAYOUT_INLINE_ARRAY_NODE: "1" runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/runner=bench-dedicated/family=c6id.8xlarge/tag=build{1}', github.run_id, (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && '/extras=s3-cache' || '') + && format('runs-on={0}/runner=bench-dedicated/family={1}/image={2}/tag=build{3}', github.run_id, inputs.build_machine_type, inputs.machine_image, (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && '/extras=s3-cache' || '') || 'ubuntu-latest' }} steps: - uses: runs-on/action@v2 @@ -112,7 +123,7 @@ jobs: runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/runner=bench-dedicated/family={1}/tag={2}{3}', github.run_id, inputs.machine_type, matrix.id, (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && '/extras=s3-cache' || '') + && format('runs-on={0}/runner=bench-dedicated/family={1}/image={2}/tag={3}{4}', github.run_id, inputs.machine_type, inputs.machine_image, matrix.id, (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && '/extras=s3-cache' || '') || 'ubuntu-latest' }} steps: - uses: runs-on/action@v2 From 7e613e8d3b7fd5ca06ef80e6463d1044be140565 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 11:41:47 +0000 Subject: [PATCH 2/4] Add Graviton3 SVE miscompile repros to string-bench Two examples reproduce the Vortex file read failures seen once the benchmarks moved to c7gd.metal (OnPair/FSST decoded bytes disagree with uncompressed_lengths): * `sve_widening_sum`: dependency-free. With `-C target-cpu=neoverse-v1` (what `target-cpu=native` resolves to on Graviton3), rustc 1.98 / LLVM 22 miscompiles a widening `u8 -> usize` sum at an SVE vector length of 256 bits or more; the same binary is correct at 128 bits, and u16/u32/i32 sums are correct everywhere. Both codecs size their decode buffer with exactly that sum over the file's `u8` string lengths, so the buffer comes out about half the needed size. * `arm64_repro`: the same fault through the real path, on the ClickBench URL column, as five independent stages (in-memory OnPair and FSST, then file write/read with OnPair, FSST and the default compressor), with a `--dump` mode that writes every decoded child and its widening sum so two runs can be diffed. Signed-off-by: Claude Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FUSFWCFtHuDq5uCqDgGm9i --- Cargo.lock | 1 + benchmarks/string-bench/Cargo.toml | 1 + .../string-bench/examples/arm64_repro.rs | 340 ++++++++++++++++++ .../string-bench/examples/sve_widening_sum.rs | 100 ++++++ 4 files changed, 442 insertions(+) create mode 100644 benchmarks/string-bench/examples/arm64_repro.rs create mode 100644 benchmarks/string-bench/examples/sve_widening_sum.rs diff --git a/Cargo.lock b/Cargo.lock index 0a367df1534..4818fa0d15f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9492,6 +9492,7 @@ dependencies = [ "clap", "futures", "indicatif", + "num-traits", "parquet 59.2.0", "regex", "tabled", diff --git a/benchmarks/string-bench/Cargo.toml b/benchmarks/string-bench/Cargo.toml index eae7d10f3aa..64ae93e4fe0 100644 --- a/benchmarks/string-bench/Cargo.toml +++ b/benchmarks/string-bench/Cargo.toml @@ -20,6 +20,7 @@ bytes = { workspace = true } clap = { workspace = true, features = ["derive"] } futures = { workspace = true } indicatif = { workspace = true } +num-traits = { workspace = true } parquet = { workspace = true } regex = { workspace = true } tabled = { workspace = true, features = ["std"] } diff --git a/benchmarks/string-bench/examples/arm64_repro.rs b/benchmarks/string-bench/examples/arm64_repro.rs new file mode 100644 index 00000000000..feddcd8f0b9 --- /dev/null +++ b/benchmarks/string-bench/examples/arm64_repro.rs @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Reproduction of the string-codec length mismatch seen when the benchmarks moved to +//! `c7gd.metal` (Graviton3): reading a Vortex file back fails with +//! `OnPair codes decode to more bytes than uncompressed_lengths records` or +//! `FSST decoded N bytes, expected M` / `output buffer sized too small`. +//! +//! Root cause (see the dependency-free `sve_widening_sum` example for the 40-line version): +//! with `-C target-cpu=native` on Graviton3, i.e. `neoverse-v1`, rustc 1.98 / LLVM 22 miscompiles +//! a widening `u8 -> usize` sum whenever the SVE vector length is 256 bits or more. Both +//! `OnPairDecodePlan::new` and `FsstDecodePlan::new` size their output buffer with exactly that +//! sum over the `u8` `uncompressed_lengths` child of arrays read back from a file, so the buffer +//! comes out about half the needed size. In-memory arrays keep `i32` lengths and are unaffected, +//! which is why only the file stages below fail. Building with `-C target-feature=-sve,-sve2` +//! avoids it. +//! +//! The same column is pushed through five independent stages so the failing layer is +//! visible at a glance: +//! +//! 1. `onpair-memory`: `onpair_compress` then canonicalize, no file involved. +//! 2. `fsst-memory`: `fsst_compress` then canonicalize, no file involved. +//! 3. `onpair-file`: write an in-memory Vortex file forcing the OnPair scheme, read it back, +//! canonicalize every row split. +//! 4. `fsst-file`: same with the FSST scheme forced. +//! 5. `default-file`: same with the default btrblocks compressor (whatever it picks). +//! +//! Every stage runs even if an earlier one fails. A failing file stage prints the encoding +//! tree of the first row split that does not decode. `--dump DIR` additionally writes every +//! decoded integer child of each row split, with its widening sum on the first line, so two +//! runs can be diffed. The process exits non-zero if any stage fails. +//! +//! Build exactly as the benchmark jobs do and run against the ClickBench `URL` column: +//! +//! ```text +//! RUSTFLAGS="-C target-cpu=native" cargo build --profile release_debug \ +//! -p string-bench --example arm64_repro --features unstable_encodings +//! target/release_debug/examples/arm64_repro # full shard 0 (~1M rows) +//! target/release_debug/examples/arm64_repro --rows 100000 # first 100k rows only +//! target/release_debug/examples/arm64_repro --shard 3 +//! ``` +//! +//! Off Graviton hardware, cross-compile with `-C target-cpu=neoverse-v1` and run under +//! `qemu-aarch64 -cpu max,sve-default-vector-length=32` (256-bit SVE, Graviton3's width); the +//! same binary passes with `sve-default-vector-length=16`. + +use std::fmt::Write as _; +use std::io::Cursor; +use std::path::PathBuf; +use std::process::ExitCode; +use std::sync::Arc; + +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use bytes::Bytes; +use clap::Parser; +use futures::TryStreamExt; +use num_traits::AsPrimitive; +use string_bench::SESSION; +use string_bench::load_clickbench_url; +use vortex::array::ArrayRef; +use vortex::array::ExecutionCtx; +use vortex::array::IntoArray; +use vortex::array::VortexSessionExecute; +use vortex::array::arrays::PrimitiveArray; +use vortex::array::arrays::VarBinViewArray; +use vortex::array::builtins::ArrayBuiltins; +use vortex::array::dtype::DType; +use vortex::array::dtype::PType; +use vortex::array::match_each_integer_ptype; +use vortex::compressor::BtrBlocksCompressorBuilder; +use vortex::file::OpenOptionsSessionExt; +use vortex::file::WriteOptionsSessionExt; +use vortex::file::WriteStrategyBuilder; +use vortex::layout::LayoutStrategy; +use vortex_bench::benchmark_write_options; +use vortex_btrblocks::SchemeExt; +use vortex_btrblocks::SchemeId; +use vortex_btrblocks::schemes::string::FSSTScheme; +use vortex_btrblocks::schemes::string::NullDominatedSparseScheme; +use vortex_btrblocks::schemes::string::OnPairScheme; +use vortex_btrblocks::schemes::string::StringDictScheme; +use vortex_fsst::FSST; +use vortex_fsst::FSSTArraySlotsExt; +use vortex_fsst::fsst_compress; +use vortex_fsst::fsst_train_compressor; +use vortex_onpair::DEFAULT_CONFIG; +use vortex_onpair::OnPair; +use vortex_onpair::OnPairArraySlotsExt; +use vortex_onpair::onpair_compress; + +#[derive(Parser, Debug)] +struct Args { + /// ClickBench `hits` shard whose `URL` column is used as input. + #[arg(long, default_value_t = 0)] + shard: u32, + /// Only use the first N rows of the column. Useful to bisect a failure down. + #[arg(long)] + rows: Option, + /// Directory to write the decoded integer children (`codes`, `codes_offsets`, + /// `dict_offsets`, `uncompressed_lengths`) of every OnPair row split read back from the + /// file, one value per line. Diff two runs (e.g. different SVE vector lengths) to find + /// the child that decodes differently. + #[arg(long)] + dump: Option, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result { + let args = Args::parse(); + let session = &*SESSION; + let mut ctx = session.create_execution_ctx(); + + let column = load_clickbench_url(args.shard, &mut ctx).await?; + let mut expected = column.array.clone().execute::(&mut ctx)?; + if let Some(rows) = args.rows { + let rows = rows.min(expected.len()); + expected = expected + .into_array() + .slice(0..rows)? + .execute::(&mut ctx)?; + } + println!( + "input: {} rows={} dtype={}", + column.name, + expected.len(), + expected.dtype() + ); + let input = expected.clone().into_array(); + + let mut failed = false; + let mut report = |name: &str, result: Result<()>| match result { + Ok(()) => println!("PASS {name}"), + Err(e) => { + failed = true; + println!("FAIL {name}: {e:#}"); + } + }; + + report("onpair-memory", { + let mut ctx = session.create_execution_ctx(); + onpair_compress(&input, DEFAULT_CONFIG, &mut ctx) + .context("onpair_compress") + .and_then(|encoded| check_chunks(&expected, &[encoded], &mut ctx)) + }); + + report("fsst-memory", { + let mut ctx = session.create_execution_ctx(); + fsst_train_compressor(&input, &mut ctx) + .context("fsst_train_compressor") + .and_then(|compressor| { + fsst_compress(&input, &compressor, &mut ctx).context("fsst_compress") + }) + .and_then(|encoded| check_chunks(&expected, &[encoded.into_array()], &mut ctx)) + }); + + for (name, strategy) in [ + ("onpair-file", forced_string_scheme(Some(OnPairScheme.id()))), + ("fsst-file", forced_string_scheme(Some(FSSTScheme.id()))), + ("default-file", forced_string_scheme(None)), + ] { + let mut ctx = session.create_execution_ctx(); + let dump = args.dump.as_ref(); + let result = async { + let data = write_file(&input, &strategy).await?; + let chunks = read_file(data).await?; + if let Some(dir) = dump { + dump_onpair_children(&dir.join(name), &chunks, &mut ctx)?; + } + check_chunks(&expected, &chunks, &mut ctx) + } + .await; + report(name, result); + } + + Ok(if failed { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + }) +} + +/// The btrblocks string schemes the default compressor chooses between. Forcing one keeps +/// only that scheme selectable; `None` leaves the default set untouched. +fn forced_string_scheme(forced: Option) -> Arc { + let all = [ + StringDictScheme.id(), + FSSTScheme.id(), + OnPairScheme.id(), + NullDominatedSparseScheme.id(), + ]; + let mut compressor = BtrBlocksCompressorBuilder::default(); + if let Some(forced) = forced { + compressor = compressor.exclude_schemes(all.into_iter().filter(|&id| id != forced)); + } + WriteStrategyBuilder::default() + .with_btrblocks_builder(compressor) + .build() +} + +/// Write `input` to an in-memory Vortex file with the benchmark's write options. +async fn write_file(input: &ArrayRef, strategy: &Arc) -> Result { + let session = &*SESSION; + let mut buf = Vec::new(); + { + let mut cursor = Cursor::new(&mut buf); + benchmark_write_options(session.write_options()) + .with_strategy(Arc::clone(strategy)) + .write(&mut cursor, input.to_array_stream()) + .await + .context("write")?; + } + println!(" wrote {} bytes", buf.len()); + Ok(Bytes::from(buf)) +} + +/// Scan the whole file back as one array per row split, without canonicalizing. +async fn read_file(data: Bytes) -> Result> { + let session = &*SESSION; + let file = session.open_options().open_buffer(data).context("open")?; + let chunks: Vec = file + .scan() + .context("scan")? + .into_array_stream() + .context("stream")? + .try_collect() + .await + .context("collect")?; + println!(" read {} row splits", chunks.len()); + Ok(chunks) +} + +/// Canonicalize each chunk on its own and compare it byte-for-byte against the matching +/// rows of `expected`. Reports the first chunk that fails, with its encoding tree. +fn check_chunks( + expected: &VarBinViewArray, + chunks: &[ArrayRef], + ctx: &mut ExecutionCtx, +) -> Result<()> { + let mut offset = 0usize; + for (i, chunk) in chunks.iter().enumerate() { + let len = chunk.len(); + let canonical = match chunk.clone().execute::(ctx) { + Ok(canonical) => canonical, + Err(e) => { + bail!( + "row split {i} (rows {offset}..{}) failed to canonicalize: {e}\n{}", + offset + len, + chunk.display_tree() + ); + } + }; + if canonical.len() != len { + bail!( + "row split {i}: canonical len {} != chunk len {len}", + canonical.len() + ); + } + for row in 0..len { + let want = expected.bytes_at(offset + row); + let got = canonical.bytes_at(row); + if want.as_slice() != got.as_slice() { + bail!( + "row split {i}: row {} differs (want {} bytes, got {} bytes)\n{}", + offset + row, + want.len(), + got.len(), + chunk.display_tree() + ); + } + } + offset += len; + } + if offset != expected.len() { + bail!("chunks cover {offset} rows, expected {}", expected.len()); + } + Ok(()) +} + +/// Decode the integer children of every OnPair and FSST row split independently and write +/// each as one value per line under `dir`, so two runs can be diffed child by child. The +/// `*-cast-*` files go through the same widening `cast` the decoders use before executing. +fn dump_onpair_children(dir: &PathBuf, chunks: &[ArrayRef], ctx: &mut ExecutionCtx) -> Result<()> { + std::fs::create_dir_all(dir)?; + for (i, chunk) in chunks.iter().enumerate() { + let mut children: Vec<(String, ArrayRef)> = Vec::new(); + if let Some(onpair) = chunk.as_opt::() { + for (name, child, widen) in [ + ("codes", onpair.codes(), PType::U16), + ("codes_offsets", onpair.codes_offsets(), PType::U64), + ("dict_offsets", onpair.dict_offsets(), PType::U32), + ( + "uncompressed_lengths", + onpair.uncompressed_lengths(), + PType::U64, + ), + ] { + children.push((name.to_string(), child.clone())); + let dtype = DType::Primitive(widen, child.dtype().nullability()); + children.push((format!("{name}-cast-{widen}"), child.cast(dtype)?)); + } + } else if let Some(fsst) = chunk.as_opt::() { + let lengths = fsst.uncompressed_lengths(); + children.push(("uncompressed_lengths".to_string(), lengths.clone())); + let dtype = DType::Primitive(PType::U64, lengths.dtype().nullability()); + children.push(( + "uncompressed_lengths-cast-u64".to_string(), + lengths.cast(dtype)?, + )); + } else { + continue; + } + for (child_name, child) in children { + let path = dir.join(format!("split-{i:02}-{child_name}.txt")); + let text = match child.execute::(ctx) { + Ok(prim) => { + let mut out = String::new(); + match_each_integer_ptype!(prim.ptype(), |P| { + // The same widening sum the decoders use to size their output buffer. + let total: usize = prim + .as_slice::

() + .iter() + .map(|&v| AsPrimitive::::as_(v)) + .sum(); + writeln!(out, "sum={total} ptype={}", prim.ptype())?; + for v in prim.as_slice::

() { + writeln!(out, "{v}")?; + } + }); + out + } + Err(e) => format!("ERROR: {e}\n"), + }; + std::fs::write(&path, text)?; + } + } + println!(" dumped children to {}", dir.display()); + Ok(()) +} diff --git a/benchmarks/string-bench/examples/sve_widening_sum.rs b/benchmarks/string-bench/examples/sve_widening_sum.rs new file mode 100644 index 00000000000..c56bbec4ce5 --- /dev/null +++ b/benchmarks/string-bench/examples/sve_widening_sum.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Minimal, dependency-free reproduction of the miscompile behind the `c7gd.metal` (Graviton3) +//! benchmark failures: a widening sum of `u8` values into `usize`/`u64` returns the wrong +//! total when the binary is built with `-C target-cpu=neoverse-v1` (what `-C target-cpu=native` +//! resolves to on Graviton3) and runs with an SVE vector length of 256 bits or more. The same +//! binary is correct at a 128-bit vector length, and `u16`/`u32`/`i32` sums are correct at +//! every length. +//! +//! Vortex hits this in `OnPairDecodePlan::new` and `FsstDecodePlan::new`, which size their +//! output buffer as the sum of the `u8` `uncompressed_lengths` child of arrays read back from a +//! file; the undersized buffer surfaces as `OnPair codes decode to more bytes than +//! uncompressed_lengths records`, `FSST decoded N bytes, expected M`, and the `fsst-rs` +//! `output buffer sized too small` panic. +//! +//! ```text +//! RUSTFLAGS="-C target-cpu=neoverse-v1" cargo build --release \ +//! -p string-bench --example sve_widening_sum --features unstable_encodings +//! target/release/examples/sve_widening_sum # on Graviton3, or under +//! qemu-aarch64 -cpu max,sve-default-vector-length=32 target/.../sve_widening_sum +//! ``` +//! +//! Exits non-zero and prints every mismatching input length. Building with +//! `-C target-feature=-sve,-sve2` makes it pass. + +use std::hint::black_box; +use std::process::ExitCode; + +#[inline(never)] +fn sum_u8_as_usize(xs: &[u8]) -> usize { + xs.iter().map(|&x| x as usize).sum() +} + +#[inline(never)] +fn sum_u8_as_u64(xs: &[u8]) -> u64 { + xs.iter().map(|&x| x as u64).sum() +} + +#[inline(never)] +fn sum_u16_as_usize(xs: &[u16]) -> usize { + xs.iter().map(|&x| x as usize).sum() +} + +#[inline(never)] +fn sum_u32_as_usize(xs: &[u32]) -> usize { + xs.iter().map(|&x| x as usize).sum() +} + +/// Reference total: the accumulator goes through `black_box` every step so the loop cannot be +/// vectorized. +#[inline(never)] +fn reference(xs: &[u8]) -> usize { + let mut acc = 0usize; + for &x in xs { + acc = black_box(acc + x as usize); + } + acc +} + +fn main() -> ExitCode { + // Deterministic pseudo-random lengths in 19..=250, the range of ClickBench URL lengths. + let mut seed = 0x9E37_79B9_7F4A_7C15_u64; + let mut next = move || { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + #[allow(clippy::cast_possible_truncation)] + let byte = (seed % 232) as u8; + 19 + byte + }; + + let mut mismatches = 0; + for n in [ + 1usize, 7, 8, 15, 16, 31, 32, 33, 63, 64, 65, 100, 127, 128, 129, 255, 256, 257, 1000, + 1023, 1024, 4096, 8191, 8192, 8193, 100_000, + ] { + let xs: Vec = (0..n).map(|_| next()).collect(); + let xs = black_box(xs); + let want = reference(&xs); + let u8_usize = sum_u8_as_usize(&xs); + let u8_u64 = usize::try_from(sum_u8_as_u64(&xs)).unwrap_or(usize::MAX); + let u16_usize = sum_u16_as_usize(&xs.iter().map(|&x| u16::from(x)).collect::>()); + let u32_usize = sum_u32_as_usize(&xs.iter().map(|&x| u32::from(x)).collect::>()); + let ok = u8_usize == want && u8_u64 == want && u16_usize == want && u32_usize == want; + if !ok { + mismatches += 1; + println!( + "n={n}: reference={want} u8->usize={u8_usize} u8->u64={u8_u64} \ + u16->usize={u16_usize} u32->usize={u32_usize}" + ); + } + } + println!("mismatches: {mismatches}"); + if mismatches == 0 { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE + } +} From f59b1ec9d1974e184ea72dcf5395636547fa4eba Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 12:06:55 +0000 Subject: [PATCH 3/4] Disable SVE codegen for Graviton3 benchmark builds `-C target-cpu=native` on c7gd.metal resolves to neoverse-v1, and rustc 1.98 / LLVM 22 then miscompiles widening `u8 -> usize` sums at 256-bit SVE. OnPair and FSST size their decode buffers with that sum over a file's `u8` string lengths, so every SQL, compression and string benchmark that read a Vortex file failed on the new runners. Add `-C target-feature=-sve,-sve2` to the three benchmark build steps until the compiler is fixed; the standalone repro is `benchmarks/string-bench/examples/sve_widening_sum.rs`. Signed-off-by: Claude Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FUSFWCFtHuDq5uCqDgGm9i --- .github/workflows/develop-bench.yml | 6 +++++- .github/workflows/pr-bench-runner.yml | 6 +++++- .github/workflows/sql-bench-matrix.yml | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/develop-bench.yml b/.github/workflows/develop-bench.yml index 7d2dcc1ea70..58b45725367 100644 --- a/.github/workflows/develop-bench.yml +++ b/.github/workflows/develop-bench.yml @@ -62,8 +62,12 @@ jobs: enable-sccache: ${{ github.repository == 'vortex-data/vortex' && 'true' || 'false' }} - name: Build binary shell: bash + # `-C target-feature=-sve,-sve2`: on Graviton3, `target-cpu=native` (neoverse-v1) makes rustc + # 1.98 / LLVM 22 miscompile widening `u8 -> usize` sums at 256-bit SVE, which undersizes + # the OnPair and FSST decode buffers for every Vortex file with `u8` string lengths. See + # `benchmarks/string-bench/examples/sve_widening_sum.rs`. env: - RUSTFLAGS: "-C target-cpu=native -C force-frame-pointers=yes" + RUSTFLAGS: "-C target-cpu=native -C target-feature=-sve,-sve2 -C force-frame-pointers=yes" run: | cargo build --bin ${{ matrix.benchmark.id }} --profile release_debug ${{ matrix.benchmark.build_args }} --features unstable_encodings - name: Pre-upload benchmark debuginfo to Polar Signals diff --git a/.github/workflows/pr-bench-runner.yml b/.github/workflows/pr-bench-runner.yml index 7828d3bbfdd..36080d6ae5e 100644 --- a/.github/workflows/pr-bench-runner.yml +++ b/.github/workflows/pr-bench-runner.yml @@ -52,8 +52,12 @@ jobs: enable-sccache: ${{ github.event.pull_request.head.repo.fork == false && 'true' || 'false' }} - name: Build binary shell: bash + # `-C target-feature=-sve,-sve2`: on Graviton3, `target-cpu=native` (neoverse-v1) makes rustc + # 1.98 / LLVM 22 miscompile widening `u8 -> usize` sums at 256-bit SVE, which undersizes + # the OnPair and FSST decode buffers for every Vortex file with `u8` string lengths. See + # `benchmarks/string-bench/examples/sve_widening_sum.rs`. env: - RUSTFLAGS: "-C target-cpu=native -C force-frame-pointers=yes" + RUSTFLAGS: "-C target-cpu=native -C target-feature=-sve,-sve2 -C force-frame-pointers=yes" run: | cargo build --package ${{ inputs.benchmark_id }} --profile release_debug \ --features ${{ inputs.with_lance && 'lance,' || '' }}unstable_encodings diff --git a/.github/workflows/sql-bench-matrix.yml b/.github/workflows/sql-bench-matrix.yml index 76a7c221933..444bd7a4d0a 100644 --- a/.github/workflows/sql-bench-matrix.yml +++ b/.github/workflows/sql-bench-matrix.yml @@ -77,8 +77,12 @@ jobs: enable-sccache: ${{ (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && 'true' || 'false' }} - name: Build binaries shell: bash + # `-C target-feature=-sve,-sve2`: on Graviton3, `target-cpu=native` (neoverse-v1) makes rustc + # 1.98 / LLVM 22 miscompile widening `u8 -> usize` sums at 256-bit SVE, which undersizes + # the OnPair and FSST decode buffers for every Vortex file with `u8` string lengths. See + # `benchmarks/string-bench/examples/sve_widening_sum.rs`. env: - RUSTFLAGS: "-C target-cpu=native -C force-frame-pointers=yes" + RUSTFLAGS: "-C target-cpu=native -C target-feature=-sve,-sve2 -C force-frame-pointers=yes" run: | packages=(--bin data-gen --bin datafusion-bench --bin duckdb-bench) if [ "${{ inputs.mode }}" != "pr" ]; then From dd8b594869554ea8e28d01e1544617ad4a49e0e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 12:14:09 +0000 Subject: [PATCH 4/4] Gate the arm64_repro example on unstable_encodings The string-bench library is compiled out without the feature, so the example must declare it as required, like the binary already does; workspace builds without the feature (the musl test job) otherwise fail to compile it. Signed-off-by: Claude Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FUSFWCFtHuDq5uCqDgGm9i --- benchmarks/string-bench/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/benchmarks/string-bench/Cargo.toml b/benchmarks/string-bench/Cargo.toml index 64ae93e4fe0..d6cd0ece9f9 100644 --- a/benchmarks/string-bench/Cargo.toml +++ b/benchmarks/string-bench/Cargo.toml @@ -44,5 +44,9 @@ unstable_encodings = [ name = "string-bench" required-features = ["unstable_encodings"] +[[example]] +name = "arm64_repro" +required-features = ["unstable_encodings"] + [lints] workspace = true