Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
423be42
SPMD mode
madsbk Mar 2, 2026
b6636b6
reserve_op_id
madsbk Mar 2, 2026
c7d0564
allgather_polars_dataframe
madsbk Mar 2, 2026
fa2ec5e
ContextSPMD
madsbk Mar 2, 2026
a7bd8ac
spmd_execution
madsbk Mar 2, 2026
6daef05
tests
madsbk Mar 2, 2026
6932b99
docs
madsbk Mar 2, 2026
c87dd57
rename to evaluate_pipeline_spmd_mode
madsbk Mar 2, 2026
bb168b3
cleanup
madsbk Mar 2, 2026
3df81a2
run_polars_dask
madsbk Mar 2, 2026
0240712
remove num_queries
madsbk Mar 2, 2026
db332fe
spmd_execution(): takes engine_kwargs
madsbk Mar 3, 2026
a401946
benchmark: spmd
madsbk Mar 3, 2026
8f4989c
doc
madsbk Mar 3, 2026
93fd93d
spmd_execution(): read rapidsmpf_py_executor_max_workers
madsbk Mar 3, 2026
eb34aae
Apply suggestions from code review
madsbk Mar 4, 2026
7b6283e
Merge branch 'main' of github.com:rapidsai/cudf into rapidsmpf-spmd
madsbk Mar 4, 2026
f0f3474
revert utils.py
madsbk Mar 4, 2026
6aff093
run_polars(): remove unused argument
madsbk Mar 4, 2026
fdef8ff
more tests and docs
madsbk Mar 4, 2026
e900e93
use stream_ordered_after
madsbk Mar 4, 2026
5ac3b84
Merge branch 'main' of github.com:rapidsai/cudf into rapidsmpf-spmd
madsbk Mar 5, 2026
ffa917c
Merge branch 'main' of github.com:rapidsai/cudf into rapidsmpf-spmd
madsbk Mar 5, 2026
985250f
distributed_scan
madsbk Mar 5, 2026
a66b7cc
rapidsmpf_options
madsbk Mar 5, 2026
cf2582b
cleanup
madsbk Mar 5, 2026
335c91c
SPMDContext
madsbk Mar 6, 2026
cdfffa6
docs
madsbk Mar 6, 2026
97a0555
Merge branch 'main' of github.com:rapidsai/cudf into rapidsmpf-spmd
madsbk Mar 6, 2026
8753701
run_duckdb(): removed num_queries
madsbk Mar 6, 2026
720c97e
cleanup
madsbk Mar 6, 2026
86cdabe
Context doesn't have a comm anymore
madsbk Mar 6, 2026
373537c
more tests and docs
madsbk Mar 6, 2026
51941d9
cleanup
madsbk Mar 6, 2026
7a55100
cleanup
madsbk Mar 6, 2026
9db3e52
docs
madsbk Mar 6, 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
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class PDSDSDuckDBQueries(PDSDSQueries):
args = parse_args(parser=parser)

if args.engine == "polars":
run_polars(PDSDSPolarsQueries, args, num_queries=99)
run_polars(PDSDSPolarsQueries, args)
Comment thread
madsbk marked this conversation as resolved.
Resolved
elif args.engine == "duckdb":
run_duckdb(PDSDSDuckDBQueries, args, num_queries=99)
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1796,7 +1796,7 @@ def q22(run_config: RunConfig) -> str:
args = parse_args(parser=parser)

if args.engine == "polars":
run_polars(PDSHQueries, args, num_queries=22)
run_polars(PDSHQueries, args)
elif args.engine == "duckdb":
run_duckdb(PDSHDuckDBQueries, args, num_queries=22)
else:
Expand Down
274 changes: 178 additions & 96 deletions python/cudf_polars/cudf_polars/experimental/benchmarks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

import rmm.statistics

from cudf_polars.experimental.rapidsmpf.spmd import spmd_execution

# The dtype for count() aggregations depends on the presence
# of the polars-runtime-64 package (`polars[rt64]`).
HAS_POLARS_RT_64 = pl.config.plr.RUNTIME_REPR == "rt64"
Expand Down Expand Up @@ -434,7 +436,13 @@ def from_args(cls, args: argparse.Namespace) -> RunConfig:
)
cluster = "single" if scheduler == "synchronous" else "distributed"
elif cluster is not None:
scheduler = "synchronous" if cluster == "single" else "distributed"
match cluster:
case "single":
scheduler = "synchronous"
case "distributed":
scheduler = "distributed"
case "spmd": # launched via rrun, not Dask
scheduler = None
else:
cluster = "single"
scheduler = "synchronous"
Expand Down Expand Up @@ -929,11 +937,12 @@ def build_parser(num_queries: int = 22) -> argparse.ArgumentParser:
"--cluster",
default=None,
type=str,
choices=["single", "distributed"],
choices=["single", "distributed", "spmd"],
help=textwrap.dedent("""\
Cluster type to use with the 'streaming' executor.
- single : Run locally in a single process
- distributed : Use Dask for multi-GPU execution"""),
- distributed : Use Dask for multi-GPU execution
- spmd : SPMD execution via rrun launcher"""),
)
parser.add_argument(
"-s",
Expand Down Expand Up @@ -1560,54 +1569,26 @@ def run_polars_query(
)


def run_polars(
def _run_query_loop(
benchmark: Any,
args: argparse.Namespace,
num_queries: int = 22,
) -> None:
"""Run the queries using the given benchmark and executor options."""
vars(args).update({"query_set": benchmark.name})
run_config = RunConfig.from_args(args)
validation_failures: list[int] = []
query_failures: list[tuple[int, int]] = []

client = initialize_dask_cluster(run_config, args)

# Update n_workers from the actual cluster when using scheduler file/address
if client is not None:
actual_n_workers = client.scheduler_info()["n_workers"]
run_config = dataclasses.replace(run_config, n_workers=actual_n_workers)

run_config: RunConfig,
engine: pl.GPUEngine | None,
client: Any,
numeric_type: str,
date_type: str,
validation_files: dict[int, Path] | None,
) -> tuple[
defaultdict[int, list[SuccessRecord | FailedRecord]],
dict[int, Any],
list[int],
list[tuple[int, int]],
]:
"""Execute all queries in ``run_config`` and return accumulated results."""
records: defaultdict[int, list[SuccessRecord | FailedRecord]] = defaultdict(list)
plans: dict[int, SerializablePlan] = {}
engine: pl.GPUEngine | None = None
numeric_type, date_type = check_input_data_type(run_config)

if args.validate_directory is not None:
validation_files = list_validation_files(args.validate_directory)
else:
validation_files = None

if run_config.executor != "cpu":
executor_options = get_executor_options(run_config, benchmark=benchmark)
if run_config.runtime == "rapidsmpf":
parquet_options = {
"use_rapidsmpf_native": run_config.native_parquet,
}
else:
parquet_options = {}
engine = pl.GPUEngine(
raise_on_fail=True,
memory_resource=rmm.mr.CudaAsyncMemoryResource(
release_threshold=args.rmm_release_threshold
)
if run_config.rmm_async
else None,
cuda_stream_policy=run_config.stream_policy,
executor=run_config.executor,
executor_options=executor_options,
parquet_options=parquet_options,
)
validation_failures: list[int] = []
query_failures: list[tuple[int, int]] = []

for q_id in run_config.queries:
try:
Expand Down Expand Up @@ -1645,80 +1626,141 @@ def run_polars(
if result.validation_failed:
validation_failures.append(q_id)

run_config = dataclasses.replace(run_config, records=dict(records), plans=plans)
return records, plans, validation_failures, query_failures

# consolidate logs
if _HAS_STRUCTLOG and run_config.collect_traces:

def gather_logs() -> str:
logger = logging.getLogger()
return logger.handlers[0].stream.getvalue() # type: ignore[attr-defined]
def _consolidate_logs(run_config: RunConfig, client: Any) -> RunConfig:
"""Merge structlog traces from the local process and Dask workers into run_config."""
if not (_HAS_STRUCTLOG and run_config.collect_traces):
return run_config

if client is not None:
# Gather logs from both client (for Query Plan) and workers
worker_logs = "\n".join(client.run(gather_logs).values())
client_logs = gather_logs()
all_logs = client_logs + "\n" + worker_logs
else:
all_logs = gather_logs()
def gather_logs() -> str:
logger = logging.getLogger()
return logger.handlers[0].stream.getvalue() # type: ignore[attr-defined]

parsed_logs = [json.loads(log) for log in all_logs.splitlines() if log]
# Some other log records can end up in here. Filter those out.
scope_values = {s.value for s in Scope}
parsed_logs = [log for log in parsed_logs if log.get("scope") in scope_values]
# Now we want to augment the existing Records with the trace data.
if client is not None:
# Gather logs from both client (for Query Plan) and workers
worker_logs = "\n".join(client.run(gather_logs).values())
client_logs = gather_logs()
all_logs = client_logs + "\n" + worker_logs
else:
all_logs = gather_logs()

def group_key(x: dict) -> int:
return x["query_id"]
parsed_logs = [json.loads(log) for log in all_logs.splitlines() if log]
# Some other log records can end up in here. Filter those out.
scope_values = {s.value for s in Scope}
parsed_logs = [log for log in parsed_logs if log.get("scope") in scope_values]
# Now we want to augment the existing Records with the trace data.

def sort_key(x: dict) -> tuple[int, int]:
return x["query_id"], x["iteration"]
def group_key(x: dict) -> int:
return x["query_id"]

grouped = itertools.groupby(
sorted(parsed_logs, key=sort_key),
key=group_key,
)
def sort_key(x: dict) -> tuple[int, int]:
return x["query_id"], x["iteration"]

for query_id, run_logs_group in grouped:
run_logs = list(run_logs_group)
by_iteration = [
list(x)
for _, x in itertools.groupby(run_logs, key=lambda x: x["iteration"])
]
run_records = run_config.records[query_id]
assert len(by_iteration) == len(run_records) # same number of iterations
all_traces = [list(iteration) for iteration in by_iteration]

new_records: list[SuccessRecord | FailedRecord] = []
for rec, traces in zip(run_records, all_traces, strict=True):
if rec.status == "success":
new_records.append(dataclasses.replace(rec, traces=traces))
else:
new_records.append(rec)
grouped = itertools.groupby(
sorted(parsed_logs, key=sort_key),
key=group_key,
)

run_config.records[query_id] = new_records
for query_id, run_logs_group in grouped:
run_logs = list(run_logs_group)
by_iteration = [
list(x)
for _, x in itertools.groupby(run_logs, key=lambda x: x["iteration"])
]
run_records = run_config.records[query_id]
assert len(by_iteration) == len(run_records) # same number of iterations
all_traces = [list(iteration) for iteration in by_iteration]

new_records: list[SuccessRecord | FailedRecord] = []
for rec, traces in zip(run_records, all_traces, strict=True):
if rec.status == "success":
new_records.append(dataclasses.replace(rec, traces=traces))
else:
new_records.append(rec)

if args.summarize:
run_config.summarize()
run_config.records[query_id] = new_records

return run_config


def run_polars(
benchmark: Any,
args: argparse.Namespace,
) -> None:
"""Run the queries using the given benchmark and executor options."""
vars(args).update({"query_set": benchmark.name})
run_config = RunConfig.from_args(args)

if run_config.cluster == "spmd":
run_polars_spmd(benchmark, args, run_config)
return # run_polars_spmd calls sys.exit; return keeps mypy happy

numeric_type, date_type = check_input_data_type(run_config)
validation_files = (
list_validation_files(args.validate_directory)
if args.validate_directory is not None
else None
)

client = initialize_dask_cluster(run_config, args)
if client is not None:
run_config = dataclasses.replace(
run_config, n_workers=client.scheduler_info()["n_workers"]
)

engine = None
if run_config.executor != "cpu":
executor_options = get_executor_options(run_config, benchmark=benchmark)
parquet_options = (
{"use_rapidsmpf_native": True} if run_config.runtime == "rapidsmpf" else {}
)
engine = pl.GPUEngine(
raise_on_fail=True,
memory_resource=rmm.mr.CudaAsyncMemoryResource(
release_threshold=args.rmm_release_threshold
)
if run_config.rmm_async
else None,
cuda_stream_policy=run_config.stream_policy,
executor=run_config.executor,
executor_options=executor_options,
parquet_options=parquet_options,
)

records, plans, validation_failures, query_failures = _run_query_loop(
benchmark,
args,
run_config,
engine,
client,
numeric_type,
date_type,
validation_files,
)
run_config = dataclasses.replace(run_config, records=dict(records), plans=plans)
run_config = _consolidate_logs(run_config, client=client)
if client is not None:
client.close(timeout=60)

if args.summarize:
run_config.summarize()

if args.validate and run_config.executor != "cpu":
print("\nValidation Summary")
print("==================")
if validation_failures:
print(
f"{len(validation_failures)} queries failed validation: {sorted(set(validation_failures))}"
f"{len(validation_failures)} queries failed validation: "
f"{sorted(set(validation_failures))}"
)
else:
print("✅ All validated queries passed.")

args.output.write(json.dumps(run_config.serialize(engine=engine)))
args.output.write("\n")

exit_code = 1 if (query_failures or validation_failures) else 0
sys.exit(exit_code)
sys.exit(1 if (query_failures or validation_failures) else 0)


def setup_logging(query_id: int, iteration: int) -> None: # noqa: D103
Expand Down Expand Up @@ -1952,3 +1994,43 @@ def run_duckdb(

args.output.write(json.dumps(run_config.serialize(engine=None)))
args.output.write("\n")


def run_polars_spmd(
benchmark: Any,
args: argparse.Namespace,
run_config: RunConfig,
) -> None:
"""Run benchmark queries using SPMD execution via the ``rrun`` launcher."""
executor_options = get_executor_options(run_config, benchmark=benchmark)
# "runtime" and "cluster" are reserved — spmd_execution sets them
executor_options.pop("runtime", None)
executor_options.pop("cluster", None)
parquet_options = (
{"use_rapidsmpf_native": True} if run_config.native_parquet else {}
)
numeric_type, date_type = check_input_data_type(run_config)
validation_files = (
list_validation_files(args.validate_directory)
if args.validate_directory is not None
else None
)
with spmd_execution(
executor_options=executor_options,
parquet_options=parquet_options,
) as (_, engine):
records, plans, validation_failures, query_failures = _run_query_loop(
benchmark,
args,
run_config,
engine,
None,
numeric_type,
date_type,
validation_files,
)
run_config = dataclasses.replace(run_config, records=dict(records), plans=plans)
# engine is not JSON-serializable (holds the SPMD Cython context)
args.output.write(json.dumps(run_config.serialize(engine=None)))
args.output.write("\n")
sys.exit(1 if (query_failures or validation_failures) else 0)
Comment thread
madsbk marked this conversation as resolved.
Resolved
Loading