Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
158 changes: 11 additions & 147 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -1,68 +1,25 @@
name: Codspeed Benchmarking
name: Codspeed RowFn Take Filter

# Concurrency control:
# - PRs: new commits on a feature branch will cancel in-progress (outdated) runs.
# - Push to develop: every commit gets its own group, so baseline runs never cancel and never
# queue behind each other. Serialising them meant a burst of merges left later commits without
# a finished baseline, so CodSpeed fell back to an older comparison base and reported changes
# unrelated to the PR being tested.
# - `workflow_dispatch`: groups by branch and queues if run on develop.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'push' && github.sha || '' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/develop' }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [develop]
pull_request: { }
workflow_dispatch: { }

permissions:
contents: read

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
NIGHTLY_TOOLCHAIN: nightly-2026-02-05

jobs:
changes:
name: "Detect CUDA changes"
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
pull-requests: read
outputs:
run-cuda-benchmarks: ${{ github.event_name != 'pull_request' || steps.filter.outputs.cuda == 'true' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
id: filter
if: github.event_name == 'pull_request'
with:
filters: |
cuda:
- "vortex-cuda/**"
# Only this workflow defines the CUDA benchmark jobs.
- ".github/workflows/codspeed.yml"

bench-codspeed:
strategy:
matrix:
include:
- { shard: 1, name: "Core foundation", packages: "vortex-buffer vortex-error vortex-mask vortex-compute vortex-file" }
- { shard: 2, name: "Arrays", packages: "vortex-array", features: "--features _test-harness" }
- { shard: 3, name: "Main library", packages: "vortex" }
- { shard: 4, name: "Encodings 1", packages: "vortex-alp vortex-bytebool vortex-datetime-parts" }
- { shard: 5, name: "Encodings 2", packages: "vortex-decimal-byte-parts vortex-fastlanes vortex-fsst", features: "--features _test-harness" }
- { shard: 6, name: "Encodings 3", packages: "vortex-pco vortex-runend vortex-sequence" }
- { shard: 7, name: "Encodings 4", packages: "vortex-sparse vortex-zigzag vortex-zstd" }
- { shard: 8, name: "Storage formats & row encoding", packages: "vortex-flatbuffers vortex-proto vortex-btrblocks vortex-row" }
- { shard: 9, name: "Tensor & spatial", packages: "vortex-tensor vortex-spatial" }
name: "Benchmark with Codspeed (Shard #${{ matrix.shard }})"
name: "Benchmark take_filter with Codspeed"
timeout-minutes: 30
runs-on: >-
${{ github.repository == 'vortex-data/vortex'
&& format('runs-on={0}/runner=amd64-medium/image=ubuntu24-full-x64-pre-v2/extras=s3-cache/tag=bench-codspeed-{1}', github.run_id, matrix.shard)
&& format('runs-on={0}/runner=amd64-medium/image=ubuntu24-full-x64-pre-v2/extras=s3-cache/tag=bench-codspeed-take-filter', github.run_id)
|| 'ubuntu-latest' }}
steps:
- uses: runs-on/action@v2
Expand All @@ -78,108 +35,15 @@ jobs:
uses: taiki-e/cache-cargo-install-action@66c9585ef5ca780ee69399975a5e911f47905995
with:
tool: cargo-codspeed
- name: Build benchmarks
- name: Build take_filter
env:
RUSTFLAGS: "-C target-feature=+avx2"
run: cargo codspeed build ${{ matrix.features }} $(printf -- '-p %s ' ${{ matrix.packages }}) --profile bench
- name: Run benchmarks
run: >-
cargo codspeed build --features _test-harness -p vortex-array
--bench take_filter --profile bench
- name: Run take_filter
uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5
with:
run: cargo codspeed run
run: cargo codspeed run --bench take_filter
token: ${{ secrets.CODSPEED_TOKEN }}
mode: "simulation"

# Getting a GPU box is slow, in the future we can build on a box without one and only run
# on GPU machines.
bench-codspeed-cuda-build:
needs: [changes]
if: >-
always() && github.repository == 'vortex-data/vortex' &&
needs.changes.outputs.run-cuda-benchmarks == 'true'
name: "Build Codspeed CUDA benchmarks"
timeout-minutes: 30
runs-on: >-
runs-on=${{ github.run_id }}/family=g5/cpu=8/image=ubuntu24-gpu-x64/extras=s3-cache/tag=bench-codspeed-cuda-build
steps:
- uses: runs-on/action@v2
with:
sccache: s3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-rust
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
enable-sccache: "true"
- name: Install Codspeed
uses: taiki-e/cache-cargo-install-action@66c9585ef5ca780ee69399975a5e911f47905995
with:
tool: cargo-codspeed
- name: Build benchmarks
run: |
cargo codspeed build -m walltime \
--bench bitpacked_cuda \
--bench dynamic_dispatch_cuda \
--bench alp_cuda \
--bench date_time_parts_cuda \
--bench dict_cuda \
--bench fsst_cuda \
--bench runend_cuda \
--profile bench
- name: Package CUB shared library
run: |
find target/release/build -path '*/out/libvortex_cub.so' \
-exec cp {} target/codspeed/walltime/vortex-cuda/libvortex_cub.so \;
test -f target/codspeed/walltime/vortex-cuda/libvortex_cub.so
- name: Upload benchmark executables
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: codspeed-cuda-benchmarks
path: target/codspeed/
retention-days: 1
if-no-files-found: error

bench-codspeed-cuda:
if: github.repository == 'vortex-data/vortex'
needs: [bench-codspeed-cuda-build]
strategy:
matrix:
include:
- { shard: 1, name: "Bitpacked", benches: "bitpacked_cuda" }
- { shard: 2, name: "Dynamic dispatch", benches: "dynamic_dispatch_cuda" }
- { shard: 3, name: "Standalone kernels", benches: "alp_cuda date_time_parts_cuda dict_cuda fsst_cuda runend_cuda" }
name: "Benchmark with Codspeed (CUDA Shard #${{ matrix.shard }} - ${{ matrix.name }})"
timeout-minutes: 30
runs-on: >-
runs-on=${{ github.run_id }}/family=g5/cpu=8/image=ubuntu24-gpu-x64/extras=s3-cache/tag=bench-codspeed-cuda-${{ matrix.shard }}
steps:
- uses: runs-on/action@v2
with:
sccache: s3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: ./.github/actions/setup-rust
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
enable-sccache: "true"
- name: Display NVIDIA SMI details
run: |
nvidia-smi
nvidia-smi -L
nvidia-smi -q -d Memory
- name: Install Codspeed
uses: taiki-e/cache-cargo-install-action@66c9585ef5ca780ee69399975a5e911f47905995
with:
tool: cargo-codspeed
- name: Download benchmark executables
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: codspeed-cuda-benchmarks
path: target/codspeed
- name: Restore executable permissions
run: find target/codspeed -type f -exec chmod +x {} +
- name: Run benchmarks
uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5
env:
CARGO_MANIFEST_DIR: ${{ github.workspace }}/vortex-cuda
with:
run: cargo codspeed run $(printf -- '--bench %s ' ${{ matrix.benches }})
token: ${{ secrets.CODSPEED_TOKEN }}
mode: "walltime"
8 changes: 8 additions & 0 deletions vortex-array/benches/binary_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ fn div_i64_nonnull(bencher: Bencher) {
bench_primitive(bencher, lhs, rhs, Operator::Div);
}

#[divan::bench]
fn div_i64_nullable(bencher: Bencher) {
let lhs = primitive_nullable(1_000_000, 7).into_array();
let rhs = primitive_nullable(17, 5).into_array();

bench_primitive(bencher, lhs, rhs, Operator::Div);
}

#[divan::bench]
fn sub_i64_constant(bencher: Bencher) {
let lhs = primitive_nonnull(0).into_array();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use vortex_error::vortex_err;

use crate::scalar_fn::fns::operators::Operator;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Binary element-wise operations.
pub enum NumericOperator {
/// Binary element-wise addition of two arrays or of two scalars.
Expand Down
88 changes: 11 additions & 77 deletions vortex-array/src/scalar_fn/fns/binary/numeric/checked.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

//! Checked-lane execution for numeric kernels, driven by the shared
//! Checked-lane execution for the decimal kernels, driven by the shared
//! `vortex-compute` lane kernels.

use std::ops::BitOrAssign;
//!
//! The primitive widths do not come through here: they are computed one row at a time by
//! [`row`](super::row), which writes a value for every row and reduces failure evidence without
//! scanning the finished output.

use vortex_buffer::Buffer;
use vortex_buffer::BufferMut;
Expand All @@ -13,33 +15,18 @@ use vortex_compute::lane_kernels::IndexedSourceExt;
use vortex_mask::AllOr;
use vortex_mask::Mask;

/// Evidence that a lane failed, anything other than [`Default`] meaning failure.
///
/// `bool` is the ordinary choice; [`map_checked_into`] explains why the wider members exist and
/// asserts the width bound that membership here does **not** imply.
///
/// [`map_checked_into`]: IndexedSourceExt::map_checked_into
pub(super) trait Failure: Copy + Default + PartialEq + BitOrAssign {}

impl Failure for bool {}
impl Failure for u8 {}
impl Failure for u16 {}
impl Failure for u32 {}
impl Failure for u64 {}

/// Apply the fallible `apply` over every lane of `source`, returning
/// `Err(first_failing_valid_lane)` only when it returns `None` on a _valid_ lane.
/// `Err(first_failing_valid_lane)` only when it returns `None` on a valid lane.
///
/// `apply` also runs on invalid lanes, whose failures are masked out and whose values are
/// unspecified, so it must be total: no panics or side effects on any stored lane value.
///
/// This drives the one-pass early-exit kernels, which abort at the end of the enclosing 64-lane
/// chunk. Use it when per-lane failure handling is cheap relative to the operation, as in integer
/// division. Prefer [`checked_apply_lanes`] when the failure check itself vectorizes.
/// This drives the one-pass early-exit kernels: failures abort at the end of the enclosing
/// 64-lane chunk. It suits an operation whose per-lane failure handling is cheap relative to the
/// operation itself, which is what the decimal kernels and their per-lane casts are.
///
/// `#[inline]`: this must inline into the caller that builds the closure, so that a captured
/// constant operand flattens into a register rather than living behind a pointer the loop reloads
/// on every lane, which blocks vectorization.
/// Keep this wrapper inlineable so captured constants can become loop invariants in the caller.
/// The lane kernels retain their own inlining decisions.
#[inline]
pub(super) fn checked_lanes<S, T, Apply>(
source: S,
Expand All @@ -61,7 +48,6 @@ where
};

let mut values = BufferMut::<T>::with_capacity(len);

let out = &mut values.spare_capacity_mut()[..len];
match valid_bits {
None => source.try_map_into(out, apply)?,
Expand All @@ -73,55 +59,3 @@ where

Ok(values.freeze())
}

/// Apply the split value/failure `apply` over every lane of `source`, returning
/// `Err(first_failing_valid_lane)` only when it flags a _valid_ lane.
///
/// The hot pass writes every value unconditionally and OR-reduces one piece of evidence, leaving
/// the loop free of per-lane selects and per-chunk exit branches. Only if a lane flagged does a
/// cold second pass re-run `apply` through the early-exit kernels, which drop null-lane failures
/// and attribute the first valid one.
///
/// Like [`checked_lanes`], `apply` runs on invalid lanes and must be total.
///
/// `#[inline]`: see [`checked_lanes`].
#[inline]
pub(super) fn checked_apply_lanes<S, T, Fail, Apply>(
source: S,
valid_rows: &Mask,
mut apply: Apply,
) -> Result<Buffer<T>, usize>
where
S: IndexedSource + Copy,
T: Copy + Default,
Fail: Failure,
Apply: FnMut(S::Item) -> (T, Fail),
{
let len = source.len();
debug_assert_eq!(len, valid_rows.len());

let valid_bits = match valid_rows.bit_buffer() {
AllOr::All => None,
AllOr::None => return Ok(Buffer::zeroed(len)),
AllOr::Some(valid_bits) => Some(valid_bits),
};

let mut values = BufferMut::<T>::with_capacity(len);

let out = &mut values.spare_capacity_mut()[..len];
if source.map_checked_into(out, &mut apply) != Fail::default() {
let mut checked = |item: S::Item| {
let (value, failure) = apply(item);
(failure == Fail::default()).then_some(value)
};
match valid_bits {
None => source.try_map_into(out, &mut checked)?,
Some(valid_bits) => source.try_map_masked_into(valid_bits, out, &mut checked)?,
}
}

// SAFETY: the kernels initialize every lane in `out`.
unsafe { values.set_len(len) };

Ok(values.freeze())
}
12 changes: 9 additions & 3 deletions vortex-array/src/scalar_fn/fns/binary/numeric/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
//! Native execution of the arithmetic operators (Add/Sub/Mul/Div) of the [`Binary`] scalar
//! function. There is no Arrow fallback.
//!
//! The primitive widths are computed by a [`RowFn`](crate::scalar_fn::RowFn), which owns null
//! handling, constants, and validity for them; see [`row`]. Decimal keeps its own columnar
//! implementation in [`decimal`].
//!
//! [`Binary`]: super::Binary

mod checked;
mod decimal;
mod primitive;
#[cfg(test)]
mod tests;
mod row;

use decimal::execute_numeric_decimal;
use primitive::execute_numeric_primitive;
use row::execute_numeric_primitive;
use vortex_error::VortexResult;
use vortex_error::vortex_ensure;

Expand Down Expand Up @@ -81,3 +84,6 @@ fn build_empty_result(

Ok(Canonical::empty(&result_dtype).into_array())
}

#[cfg(test)]
mod tests;
Loading
Loading