-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add remaining NDS-H queries to libcudf with CI validation #23624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c4c4405
26f61ae
c351c0e
e6e50e4
93dedb3
061caf4
4990387
dc8bbca
cab2af8
31c04a1
0806f0b
7e7cd37
0f8d927
1ceb155
a089808
35f6a55
e61f618
a06beff
9e4319b
cb67c3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| #!/bin/bash | ||
| # SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. | ||
| # SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| repo_root="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/.." | ||
|
|
||
| # Support customizing the benchmarks' install location | ||
| # First, try the installed location (CI/conda environments) | ||
| installed_benchmark_location="${INSTALL_PREFIX:-${CONDA_PREFIX:-/usr}}/bin/benchmarks/libcudf/" | ||
|
|
@@ -22,12 +24,23 @@ else | |
| fi | ||
|
|
||
| EXITCODE=0 | ||
| validation_dir="$(mktemp -d)" | ||
| ndsh_scale_factor=1 | ||
| trap 'rm -rf "${validation_dir}"' EXIT | ||
| # Run all nvbench benchmarks with --profile and rmm_mode=cuda | ||
| for bench in *_NVBENCH; do | ||
| if [[ -x "$bench" && -f "$bench" ]]; then | ||
| start_time=$(date +%s) | ||
| echo "Running $bench with --profile..." | ||
| "./$bench" --profile --devices 0 -q --rmm_mode cuda | ||
| args=(--profile --devices 0 -q --rmm_mode cuda) | ||
| if [[ "$bench" == NDSH_* ]]; then | ||
| args+=(--axis "scale_factor=${ndsh_scale_factor}") | ||
| if [[ "$bench" =~ ^NDSH_Q([0-9]{2})_NVBENCH$ ]]; then | ||
| # Validate small-scale NDS-H benchmark outputs against DuckDB | ||
| args+=(--output_directory "${validation_dir}/q${BASH_REMATCH[1]}") | ||
| fi | ||
| fi | ||
| "./$bench" "${args[@]}" | ||
|
Comment on lines
+27
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Scale-factor-1 outputs in
🤖 Prompt for AI Agents |
||
| SUITEERROR=$? | ||
| end_time=$(date +%s) | ||
| duration=$((end_time - start_time)) | ||
|
|
@@ -40,5 +53,10 @@ for bench in *_NVBENCH; do | |
| fi | ||
| done | ||
|
|
||
| python "${repo_root}/ci/validate_ndsh_benchmarks.py" \ | ||
| --output-dir "${validation_dir}" \ | ||
| --sql-dir "${repo_root}/cpp/libcudf_streaming/benchmarks/streaming/ndsh/sql" \ | ||
| --scale-factor "${ndsh_scale_factor}" | ||
|
|
||
| echo "Test script exiting with value: $EXITCODE" | ||
| exit ${EXITCODE} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| #!/usr/bin/env python3 | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import argparse | ||
| import math | ||
| import numbers | ||
| from pathlib import Path | ||
|
|
||
| import duckdb | ||
|
|
||
| QUERIES = ( | ||
| "q01", | ||
| "q02", | ||
| "q03", | ||
| "q04", | ||
| "q05", | ||
| "q06", | ||
| "q07", | ||
| "q08", | ||
| "q09", | ||
| "q10", | ||
| "q11", | ||
| "q12", | ||
| "q13", | ||
| "q14", | ||
| "q15", | ||
| "q16", | ||
| "q17", | ||
| "q18", | ||
| "q19", | ||
| "q20", | ||
| "q21", | ||
| "q22", | ||
| ) | ||
| EXPECTED_NAMES = { | ||
| "q18": [ | ||
| "c_name", | ||
| "c_custkey", | ||
| "o_orderkey", | ||
| "o_orderdate", | ||
| "o_totalprice", | ||
| "sum(l_quantity)", | ||
| ] | ||
| } | ||
|
|
||
|
|
||
| def values_equal(actual, expected): | ||
| if actual is None or expected is None: | ||
| return actual is expected | ||
| if isinstance(actual, numbers.Number) and isinstance( | ||
| expected, numbers.Number | ||
| ): | ||
| return math.isclose( | ||
| float(actual), float(expected), rel_tol=0.0, abs_tol=0.01 | ||
| ) | ||
| return actual == expected | ||
|
|
||
|
|
||
| def validate_query(query_name, sql_dir, output_dir, scale_factor=0.01): | ||
| connection = duckdb.connect() | ||
| for path in (output_dir / query_name / "input").glob("*.parquet"): | ||
| table_name = path.stem.replace('"', '""') | ||
| parquet_path = str(path).replace("'", "''") | ||
| connection.execute( | ||
| f'CREATE VIEW "{table_name}" AS ' | ||
| f"SELECT * FROM read_parquet('{parquet_path}')" | ||
| ) | ||
|
|
||
| parameters = ( | ||
| {"scale_factor": scale_factor} if query_name == "q11" else None | ||
| ) | ||
| expected = connection.execute( | ||
| (sql_dir / f"{query_name}.sql").read_text(), parameters | ||
| ) | ||
| expected_names = [column[0] for column in expected.description] | ||
| expected_rows = expected.fetchall() | ||
|
|
||
| result_path = output_dir / query_name / "results" / f"{query_name}.parquet" | ||
| actual = connection.execute( | ||
| "SELECT * FROM read_parquet(?)", [str(result_path)] | ||
| ) | ||
| actual_names = [column[0] for column in actual.description] | ||
| actual_rows = actual.fetchall() | ||
|
|
||
| expected_names = EXPECTED_NAMES.get(query_name, expected_names) | ||
| if actual_names != expected_names: | ||
| return f"column names differ: {actual_names} != {expected_names}" | ||
| if len(actual_rows) != len(expected_rows): | ||
| return f"row counts differ: {len(actual_rows)} != {len(expected_rows)}" | ||
|
|
||
| for row_index, (actual_row, expected_row) in enumerate( | ||
| zip(actual_rows, expected_rows, strict=True) | ||
| ): | ||
| for column_name, actual_value, expected_value in zip( | ||
| actual_names, actual_row, expected_row, strict=True | ||
| ): | ||
| if not values_equal(actual_value, expected_value): | ||
| return ( | ||
| f"row {row_index}, column {column_name} differs: " | ||
| f"{actual_value!r} != {expected_value!r}" | ||
| ) | ||
| return None | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Validate NDS-H benchmark Parquet results against DuckDB" | ||
| ) | ||
| parser.add_argument("--output-dir", type=Path, required=True) | ||
| parser.add_argument("--sql-dir", type=Path, required=True) | ||
| parser.add_argument("--scale-factor", type=float, default=0.01) | ||
| args = parser.parse_args() | ||
|
|
||
| failed = False | ||
| for query_name in QUERIES: | ||
| error = validate_query( | ||
| query_name, args.sql_dir, args.output_dir, args.scale_factor | ||
| ) | ||
| if error is None: | ||
| print(f"{query_name}: PASSED") | ||
| else: | ||
| failed = True | ||
| print(f"{query_name}: FAILED: {error}") | ||
|
|
||
| raise SystemExit(failed) | ||
|
Comment on lines
+116
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Missing benchmark output produces an unhandled failure instead of a clear result. The shell script always calls the validator, and the validator has no per-query exception handling, so an absent query directory or result file aborts the run and hides the status of the other queries.
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,17 +23,22 @@ | |
| #include <cudf/strings/padding.hpp> | ||
| #include <cudf/transform.hpp> | ||
| #include <cudf/unary.hpp> | ||
| #include <cudf/utilities/error.hpp> | ||
|
|
||
| #include <rmm/cuda_stream_view.hpp> | ||
| #include <rmm/resource_ref.hpp> | ||
|
|
||
| #include <array> | ||
| #include <limits> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| namespace cudf::datagen { | ||
|
|
||
| namespace { | ||
| constexpr cudf::size_type orders_rows_per_scale_factor = 1'500'000; | ||
| constexpr cudf::size_type order_key_candidate_multiplier = 4; | ||
|
|
||
| constexpr std::array nations{ | ||
| "ALGERIA", "ARGENTINA", "BRAZIL", "CANADA", "EGYPT", "ETHIOPIA", "FRANCE", | ||
| "GERMANY", "INDIA", "INDONESIA", "IRAN", "IRAQ", "JAPAN", "JORDAN", | ||
|
|
@@ -146,12 +151,15 @@ std::unique_ptr<cudf::table> generate_orders_independent(double scale_factor, | |
| rmm::device_async_resource_ref mr) | ||
| { | ||
| CUDF_BENCHMARK_RANGE(); | ||
| cudf::size_type const o_num_rows = scale_factor * 1'500'000; | ||
| cudf::size_type const o_num_rows = scale_factor * orders_rows_per_scale_factor; | ||
|
|
||
| // Generate the `o_orderkey` column | ||
| auto o_orderkey = [&]() { | ||
| auto const o_orderkey_candidates = generate_primary_key_column( | ||
| cudf::numeric_scalar<cudf::size_type>(1), 4 * o_num_rows, stream, mr); | ||
| auto const o_orderkey_candidates = | ||
| generate_primary_key_column(cudf::numeric_scalar<cudf::size_type>(1), | ||
| order_key_candidate_multiplier * o_num_rows, | ||
| stream, | ||
| mr); | ||
| auto const o_orderkey_unsorted = cudf::sample(cudf::table_view({o_orderkey_candidates->view()}), | ||
| o_num_rows, | ||
| cudf::sample_with_replacement::FALSE, | ||
|
|
@@ -708,6 +716,9 @@ generate_orders_lineitem_part(double scale_factor, | |
| rmm::device_async_resource_ref mr) | ||
| { | ||
| CUDF_BENCHMARK_RANGE(); | ||
| CUDF_EXPECTS(scale_factor <= static_cast<double>(std::numeric_limits<cudf::size_type>::max()) / | ||
| (orders_rows_per_scale_factor * order_key_candidate_multiplier), | ||
| "Scale factor exceeds the libcudf row limit for orders and lineitem generation"); | ||
|
Comment on lines
+719
to
+721
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect every scale-factor entry point and its validation path.
rg -n -C 4 'double scale_factor|supplier_count|CUDF_EXPECTS' \
cpp/benchmarks/common/ndsh_data_generatorRepository: NVIDIA/cudf Length of output: 22379 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- generator validation and call paths ---'
sed -n '700,770p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
sed -n '530,585p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
sed -n '150,215p' cpp/benchmarks/common/ndsh_data_generator/table_helpers.cpp
sed -n '221,270p' cpp/benchmarks/common/ndsh_data_generator/table_helpers.cpp
printf '%s\n' '--- scale-factor tests and declarations ---'
sed -n '1,110p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator_test.cpp
sed -n '1,120p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.hpp
printf '%s\n' '--- all relevant helper call sites ---'
rg -n -C 5 'calculate_(l|ps)_suppkey|generate_(orders_lineitem_part|partsupp|supplier|customer|part)\(' \
cpp/benchmarks/common/ndsh_data_generatorRepository: NVIDIA/cudf Length of output: 40102 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
root = Path("cpp/benchmarks/common/ndsh_data_generator")
files = [
root / "ndsh_data_generator.cpp",
root / "table_helpers.cpp",
root / "ndsh_data_generator_test.cpp",
]
for path in files:
text = path.read_text()
print(f"--- {path} ---")
for needle in (
"scale_factor",
"supplier_count",
"calculate_l_suppkey",
"calculate_ps_suppkey",
"CUDF_EXPECTS",
"EXPECT_THROW",
"generate_orders_lineitem_part",
):
hits = [(i + 1, line) for i, line in enumerate(text.splitlines()) if needle in line]
for line_no, line in hits:
print(f"{line_no}: {line.strip()}")
print("--- deterministic scale-factor classification ---")
# Model only the arithmetic used by the cited code. This does not execute
# repository code or CUDA code.
samples = [-1.0, 0.0, 0.000099, 0.0001, 0.01, float("nan"), float("inf")]
for sf in samples:
upper = (sf <= 2**31 - 1) if sf == sf else False
# C++ floating-to-integer conversion for out-of-range/NaN is not modeled.
supplier = None if not (sf == sf and abs(sf) < (2**31 / 10000)) else int(sf * 10000)
print(f"scale_factor={sf!r}, upper_check={upper}, supplier_count={supplier!r}")
PYRepository: NVIDIA/cudf Length of output: 4801 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- row-count arithmetic before supplier-key helpers ---'
sed -n '135,245p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
sed -n '285,320p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
sed -n '35,50p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
printf '%s\n' '--- generator tests and validation conventions ---'
rg -n -C 3 'ScaleFactor|scale factor|isfinite|isfinite|std::isfinite|CUDF_EXPECTS\(.*scale|EXPECT_THROW\(.*generate_(partsupp|supplier|customer)' \
cpp/benchmarks cpp | head -250Repository: NVIDIA/cudf Length of output: 33006 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- relevant implementation ranges ---'
sed -n '145,245p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
sed -n '295,315p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
sed -n '35,45p' cpp/benchmarks/common/ndsh_data_generator/ndsh_data_generator.cpp
printf '%s\n' '--- scale-factor validation references ---'
rg -n -C 2 'scale_factor|ScaleFactor|std::isfinite|isfinite|CUDF_EXPECTS' \
cpp/benchmarks/common/ndsh_data_generator | head -300Repository: NVIDIA/cudf Length of output: 36212 Reject invalid scale factors before narrowing or supplier-key arithmetic. The upper-bound check accepts negative values and 📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| // Generate a table with the independent columns of the `orders` table | ||
| auto orders_independent = generate_orders_independent(scale_factor, stream, mr); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||
| /* | ||||||
| * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||||||
| * SPDX-License-Identifier: Apache-2.0 | ||||||
| */ | ||||||
|
|
||||||
| #include "ndsh_data_generator.hpp" | ||||||
|
|
||||||
| #include <cudf_test/base_fixture.hpp> | ||||||
|
|
||||||
| #include <cudf/reduction.hpp> | ||||||
| #include <cudf/scalar/scalar.hpp> | ||||||
|
|
||||||
| #include <gtest/gtest.h> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use the cuDF GoogleTest header. Replace the raw GoogleTest include with Proposed fix-#include <gtest/gtest.h>
+#include <cudf_test/cudf_gtest.hpp>As per coding guidelines, 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||
|
|
||||||
| struct NDSHDataGeneratorTest : public cudf::test::BaseFixture {}; | ||||||
|
|
||||||
| TEST_F(NDSHDataGeneratorTest, ScaleFactorPointZeroOne) | ||||||
| { | ||||||
| constexpr double scale_factor = 0.01; | ||||||
|
|
||||||
| auto [orders, lineitem, part] = cudf::datagen::generate_orders_lineitem_part(scale_factor); | ||||||
| auto partsupp = cudf::datagen::generate_partsupp(scale_factor); | ||||||
| auto supplier = cudf::datagen::generate_supplier(scale_factor); | ||||||
| auto customer = cudf::datagen::generate_customer(scale_factor); | ||||||
| auto nation = cudf::datagen::generate_nation(); | ||||||
| auto region = cudf::datagen::generate_region(); | ||||||
|
|
||||||
| auto const expect_cardinality = | ||||||
| [](cudf::table const& table, cudf::size_type rows, cudf::size_type columns) { | ||||||
| EXPECT_EQ(table.num_rows(), rows); | ||||||
| EXPECT_EQ(table.num_columns(), columns); | ||||||
| }; | ||||||
|
|
||||||
| expect_cardinality(*orders, 15'000, 9); | ||||||
| EXPECT_GE(lineitem->num_rows(), 15'000); | ||||||
| EXPECT_LE(lineitem->num_rows(), 105'000); | ||||||
| EXPECT_EQ(lineitem->num_columns(), 16); | ||||||
| expect_cardinality(*part, 2'000, 9); | ||||||
| expect_cardinality(*partsupp, 8'000, 5); | ||||||
| expect_cardinality(*supplier, 100, 7); | ||||||
| expect_cardinality(*customer, 1'500, 8); | ||||||
| expect_cardinality(*nation, 25, 4); | ||||||
| expect_cardinality(*region, 5, 3); | ||||||
|
|
||||||
| auto const expect_supplier_key_range = [](cudf::column_view const& keys, | ||||||
| cudf::size_type supplier_rows) { | ||||||
| EXPECT_EQ(keys.null_count(), 0); | ||||||
| auto const [minimum, maximum] = cudf::minmax(keys); | ||||||
| auto const min_key = static_cast<cudf::numeric_scalar<cudf::size_type> const*>(minimum.get()); | ||||||
| auto const max_key = static_cast<cudf::numeric_scalar<cudf::size_type> const*>(maximum.get()); | ||||||
| EXPECT_GE(min_key->value(), 1); | ||||||
| EXPECT_LE(max_key->value(), supplier_rows); | ||||||
| }; | ||||||
|
|
||||||
| expect_supplier_key_range(lineitem->view().column(2), supplier->num_rows()); | ||||||
| expect_supplier_key_range(partsupp->view().column(1), supplier->num_rows()); | ||||||
| } | ||||||
|
|
||||||
| TEST_F(NDSHDataGeneratorTest, ScaleFactorExceedsRowLimit) | ||||||
| { | ||||||
| EXPECT_THROW(cudf::datagen::generate_orders_lineitem_part(400), cudf::logic_error); | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.