Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
64e6cb3
add ProfilingOptions
rjzamora Jan 30, 2026
27cdf6e
revise
rjzamora Jan 30, 2026
15e4bc6
Merge remote-tracking branch 'upstream/main' into profiling-config
rjzamora Jan 30, 2026
49c498c
cleanup and tests
rjzamora Jan 30, 2026
08350a5
revise design
rjzamora Jan 30, 2026
4d8a312
Merge remote-tracking branch 'upstream/main' into profiling-config
rjzamora Jan 30, 2026
126c916
tedious migration
rjzamora Jan 30, 2026
1c5151e
remove unused allreduce def
rjzamora Jan 30, 2026
ee62732
Merge remote-tracking branch 'upstream/main' into channel-metadata-mi…
rjzamora Jan 30, 2026
d3b7583
update helper function
rjzamora Jan 30, 2026
bcb3168
Merge remote-tracking branch 'upstream/main' into profiling-config
rjzamora Jan 30, 2026
07443e5
cleanup and repartition
rjzamora Jan 30, 2026
c8be098
leave out rows when unknown
rjzamora Jan 30, 2026
fe2aefd
Merge branch 'main' into profiling-config
rjzamora Jan 30, 2026
05df373
leave off static partition count
rjzamora Jan 30, 2026
032d13d
improve cov
rjzamora Jan 30, 2026
3a8b3b8
Merge remote-tracking branch 'upstream/main' into profiling-config
rjzamora Jan 30, 2026
d0bf467
more coverage
rjzamora Jan 31, 2026
74737c5
missed a line
rjzamora Jan 31, 2026
3634c98
Merge remote-tracking branch 'upstream/main' into profiling-config
rjzamora Jan 31, 2026
f77569a
Merge branch 'main' into channel-metadata-migration
rjzamora Feb 2, 2026
206f9f7
Merge branch 'main' into channel-metadata-migration
rjzamora Feb 2, 2026
6f8c5e2
Merge branch 'main' into profiling-config
rjzamora Feb 2, 2026
6d7b972
align with rjzamora:channel-metadata-migration (since that is merging…
rjzamora Feb 2, 2026
22e58c5
Merge remote-tracking branch 'upstream/main' into profiling-config
rjzamora Feb 2, 2026
c44ae02
simplify explain.py logic
rjzamora Feb 2, 2026
8800222
reuse code
rjzamora Feb 2, 2026
09a0aac
add structlog tracing
rjzamora Feb 3, 2026
4ef47f7
Merge remote-tracking branch 'upstream/main' into structlog-profiling
rjzamora Feb 3, 2026
1c80b1b
add structlog events to RuntimeNodeProfiler
rjzamora Feb 3, 2026
1347562
use 'tracing' name for consistency
rjzamora Feb 3, 2026
96c5739
adopt 'tracing' terminology
rjzamora Feb 3, 2026
75db21e
move import
rjzamora Feb 3, 2026
20f26e4
fixes
rjzamora Feb 3, 2026
72a0916
clean up docstring
rjzamora Feb 3, 2026
0b78906
Merge remote-tracking branch 'upstream/main' into structlog-profiling
rjzamora Feb 3, 2026
f3027ba
Merge remote-tracking branch 'upstream/main' into structlog-profiling
rjzamora Feb 3, 2026
694cf89
partial code review
rjzamora Feb 3, 2026
459f780
partial code review (2)
rjzamora Feb 3, 2026
09ad6cc
drop unnecessary check
rjzamora Feb 3, 2026
22bb44e
Merge remote-tracking branch 'upstream/main' into structlog-profiling
rjzamora Feb 3, 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
1 change: 1 addition & 0 deletions docs/cudf/source/cudf_polars/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ For the most part, the public API of `cudf-polars` is the polars API.
DynamicPlanningOptions,
InMemoryExecutor,
ParquetOptions,
TracingOptions,
Cluster,
ShuffleMethod,
ShufflerInsertionMethod,
Expand Down
11 changes: 11 additions & 0 deletions python/cudf_polars/cudf_polars/experimental/benchmarks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ class RunConfig:
collect_traces: bool = False
stats_planning: bool
dynamic_planning: bool | None = None
trace_output_path: str | None = None
max_io_threads: int
native_parquet: bool
spill_to_pinned_memory: bool
Expand Down Expand Up @@ -377,6 +378,7 @@ def from_args(cls, args: argparse.Namespace) -> RunConfig:
collect_traces=args.collect_traces,
stats_planning=args.stats_planning,
dynamic_planning=args.dynamic_planning,
trace_output_path=args.trace_output_path,
max_io_threads=args.max_io_threads,
native_parquet=args.native_parquet,
extra_info=args.extra_info,
Expand Down Expand Up @@ -477,6 +479,8 @@ def get_executor_options(
if run_config.dynamic_planning:
# Pass empty dict to enable with defaults; None means disabled
executor_options["dynamic_planning"] = {}
if run_config.trace_output_path is not None:
executor_options["tracing"] = {"output_path": run_config.trace_output_path}

if (
benchmark
Expand Down Expand Up @@ -974,6 +978,13 @@ def parse_args(
default=False,
help="Enable dynamic shuffle planning (not yet implemented). ",
)
parser.add_argument(
"--trace-output-path",
dest="trace_output_path",
type=str,
default=None,
help="Path to write tracing output (row counts per node).",
)
parser.add_argument(
"--max-io-threads",
default=2,
Expand Down
68 changes: 67 additions & 1 deletion python/cudf_polars/cudf_polars/experimental/explain.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES.
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
# SPDX-License-Identifier: Apache-2.0
Expand Down Expand Up @@ -26,11 +26,13 @@

if TYPE_CHECKING:
from collections.abc import MutableMapping
from pathlib import Path

import polars as pl

from cudf_polars.dsl.ir import IR
from cudf_polars.experimental.base import PartitionInfo, StatsCollector
from cudf_polars.experimental.rapidsmpf.tracing import StreamingQueryTracer


def explain_query(
Expand Down Expand Up @@ -168,3 +170,67 @@ def _(ir: Sort, *, offset: str = "") -> str:
def _(ir: Scan, *, offset: str = "") -> str:
label = f"SCAN {ir.typ.upper()}"
return _repr_header(offset, label, ir.schema)


def write_query_trace(
trace_output: str | Path,
ir: IR,
partition_info: MutableMapping[IR, PartitionInfo],
tracer: StreamingQueryTracer,
) -> None:
"""
Write a post-execution trace showing actual row counts and decisions.

Parameters
----------
trace_output
Path to write the trace file.
ir
The lowered IR root node.
partition_info
Partition information for the IR nodes.
tracer
The tracer with actual row counts and decisions from execution.
"""
from pathlib import Path

trace_repr = _repr_trace_tree(ir, partition_info, tracer)
Path(trace_output).write_text(trace_repr)


def _repr_trace_tree(
ir: IR,
partition_info: MutableMapping[IR, PartitionInfo],
tracer: StreamingQueryTracer,
*,
offset: str = "",
) -> str:
"""Recursively build a tree representation with tracer data."""
header = _repr_ir(ir, offset=offset)
header = header.rstrip("\n")

# Get node tracer if it exists
if (node_tracer := tracer.node_tracers.get(ir)) is not None:
# Add actual row count if available
if node_tracer.row_count is not None:
header += f" rows={_fmt_row_count(node_tracer.row_count)}"

# Add decision if present
if node_tracer.decision is not None:
header += f" decision={node_tracer.decision}"

# Add actual chunk count
header += f" chunks={node_tracer.chunk_count}"

children_strs = [
_repr_trace_tree(child, partition_info, tracer, offset=offset + " ")
for child in ir.children
]

header += "\n"
return header + "".join(
f"{line}{offset} (repeated {count} times)\n"
if (count := sum(1 for _ in group)) > 1
else line
for line, group in groupby(children_strs)
)
Comment on lines +201 to +236

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also have an option to dump this tree in a structured form (json would be fine)? That way automated tools have a chance of reading it rather than needing to parse.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm working on that and will have something later today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR will already dump the information in structlog form when CUDF_POLARS_LOG_TRACES=1. Tom is working on the code needed to post-process that output to provide something similar to the above. I suppose we could drop some of this logic if we feel like the dump + post-process workflow works well enough for multiple ranks.

29 changes: 24 additions & 5 deletions python/cudf_polars/cudf_polars/experimental/rapidsmpf/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
import cudf_polars.experimental.rapidsmpf.union # noqa: F401
from cudf_polars.containers import DataFrame
from cudf_polars.dsl.ir import DataFrameScan, IRExecutionContext, Join, Scan, Union
from cudf_polars.dsl.tracing import LOG_TRACES
from cudf_polars.dsl.traversal import CachingVisitor, traversal
from cudf_polars.experimental.rapidsmpf.collectives import ReserveOpIDs
from cudf_polars.experimental.rapidsmpf.dispatch import FanoutInfo, lower_ir_node
from cudf_polars.experimental.rapidsmpf.nodes import (
generate_ir_sub_network_wrapper,
metadata_drain_node,
)
from cudf_polars.experimental.rapidsmpf.tracing import StreamingQueryTracer
from cudf_polars.experimental.rapidsmpf.utils import empty_table_chunk
from cudf_polars.experimental.statistics import collect_statistics
from cudf_polars.experimental.utils import _concat
Expand Down Expand Up @@ -107,7 +109,7 @@ def evaluate_logical_plan(
# NOTE: Distributed execution requires Dask for now
from cudf_polars.experimental.rapidsmpf.dask import evaluate_pipeline_dask

result, metadata_collector = evaluate_pipeline_dask(
result, metadata_collector, tracer = evaluate_pipeline_dask(
evaluate_pipeline,
ir,
partition_info,
Expand All @@ -118,7 +120,7 @@ def evaluate_logical_plan(
)
else:
# Single-process execution: Run locally
result, metadata_collector = evaluate_pipeline(
result, metadata_collector, tracer = evaluate_pipeline(
ir,
partition_info,
config_options,
Expand All @@ -127,6 +129,13 @@ def evaluate_logical_plan(
collect_metadata=collect_metadata,
)

# Write tracer output if configured
tracing = config_options.executor.tracing
if tracing is not None and tracing.output_path is not None and tracer is not None:
from cudf_polars.experimental.explain import write_query_trace

write_query_trace(tracing.output_path, ir, partition_info, tracer)

return result, metadata_collector


Expand All @@ -139,7 +148,7 @@ def evaluate_pipeline(
rmpf_context: Context | None = None,
*,
collect_metadata: bool = False,
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None]:
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None, StreamingQueryTracer | None]:
"""
Build and evaluate a RapidsMPF streaming pipeline.

Expand All @@ -162,7 +171,7 @@ def evaluate_pipeline(

Returns
-------
The output DataFrame and metadata collector.
The output DataFrame, metadata collector, and tracer.
"""
assert config_options.executor.name == "streaming", "Executor must be streaming"
assert config_options.executor.runtime == "rapidsmpf", "Runtime must be rapidsmpf"
Expand Down Expand Up @@ -232,6 +241,11 @@ def evaluate_pipeline(
metadata_collector: list[ChannelMetadata] | None = (
[] if collect_metadata else None
)
tracer: StreamingQueryTracer | None = (
StreamingQueryTracer()
if config_options.executor.tracing is not None or LOG_TRACES
else None
)
nodes, output = generate_network(
rmpf_context,
ir,
Expand All @@ -241,6 +255,7 @@ def evaluate_pipeline(
ir_context=ir_context,
collective_id_map=collective_id_map,
metadata_collector=metadata_collector,
tracer=tracer,
)

# Run the network
Expand Down Expand Up @@ -294,7 +309,7 @@ def evaluate_pipeline(
if _initial_mr is not None:
rmm.mr.set_current_device_resource(_original_mr)

return result, metadata_collector
return result, metadata_collector, tracer


def lower_ir_graph(
Expand Down Expand Up @@ -416,6 +431,7 @@ def generate_network(
ir_context: IRExecutionContext,
collective_id_map: dict[IR, list[int]],
metadata_collector: list[ChannelMetadata] | None,
tracer: StreamingQueryTracer | None = None,
) -> tuple[list[Any], DeferredMessages]:
"""
Translate the IR graph to a RapidsMPF streaming network.
Expand All @@ -440,6 +456,8 @@ def generate_network(
The list to collect the final metadata.
This list will be mutated when the network is executed.
If None, metadata will not be collected.
tracer
Profiler for collecting runtime statistics.

Returns
-------
Expand Down Expand Up @@ -472,6 +490,7 @@ def generate_network(
"max_io_threads": max_io_threads_local,
"stats": stats,
"collective_id_map": collective_id_map,
"tracer": tracer,
}
mapper: SubNetGenerator = CachingVisitor(
generate_ir_sub_network_wrapper, state=state
Expand Down
23 changes: 15 additions & 8 deletions python/cudf_polars/cudf_polars/experimental/rapidsmpf/dask.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from cudf_polars.dsl.ir import IR
from cudf_polars.experimental.base import PartitionInfo, StatsCollector
from cudf_polars.experimental.parallel import ConfigOptions
from cudf_polars.experimental.rapidsmpf.tracing import StreamingQueryTracer


class EvaluatePipelineCallback(Protocol):
Expand All @@ -39,8 +40,8 @@ def __call__(
rmpf_context: Context | None = None,
*,
collect_metadata: bool = False,
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None]:
"""Evaluate a pipeline and return the result DataFrame and metadata."""
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None, StreamingQueryTracer | None]:
"""Evaluate a pipeline and return the result DataFrame, metadata, and tracer."""
...


Expand All @@ -61,7 +62,7 @@ def evaluate_pipeline_dask(
collective_id_map: dict[IR, list[int]],
*,
collect_metadata: bool = False,
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None]:
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None, StreamingQueryTracer | None]:
"""
Evaluate a RapidsMPF streaming pipeline on a Dask cluster.

Expand All @@ -84,7 +85,7 @@ def evaluate_pipeline_dask(

Returns
-------
The output DataFrame and metadata collector.
The output DataFrame, metadata collector, and merged tracer.
"""
client = get_dask_client()
result = client.run(
Expand All @@ -99,12 +100,18 @@ def evaluate_pipeline_dask(
)
dfs: list[pl.DataFrame] = []
metadata_collector: list[ChannelMetadata] = []
for df, md in result.values():
merged_tracer: StreamingQueryTracer | None = None
for df, md, tracer in result.values():
dfs.append(df)
if md is not None:
metadata_collector.extend(md)
if tracer is not None:
if merged_tracer is None:
merged_tracer = tracer
else:
merged_tracer.merge(tracer)

return pl.concat(dfs), metadata_collector or None
return pl.concat(dfs), metadata_collector or None, merged_tracer


def _evaluate_pipeline_dask(
Expand All @@ -117,7 +124,7 @@ def _evaluate_pipeline_dask(
dask_worker: Any = None,
*,
collect_metadata: bool = False,
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None]:
) -> tuple[pl.DataFrame, list[ChannelMetadata] | None, StreamingQueryTracer | None]:
"""
Build and evaluate a RapidsMPF streaming pipeline.

Expand All @@ -144,7 +151,7 @@ def _evaluate_pipeline_dask(

Returns
-------
The output DataFrame and metadata collector.
The output DataFrame, metadata collector, and tracer.
"""
assert dask_worker is not None, "Dask worker must be provided"
assert config_options.executor.name == "streaming", "Executor must be streaming"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
from rapidsmpf.streaming.core.context import Context

from cudf_polars.dsl.ir import IR, IRExecutionContext
from cudf_polars.experimental.base import (
PartitionInfo,
StatsCollector,
)
from cudf_polars.experimental.base import PartitionInfo, StatsCollector
from cudf_polars.experimental.rapidsmpf.tracing import StreamingQueryTracer
from cudf_polars.experimental.rapidsmpf.utils import ChannelManager
from cudf_polars.utils.config import ConfigOptions

Expand Down Expand Up @@ -77,6 +75,8 @@ class GenState(TypedDict):
Statistics collector.
collective_id_map
The mapping of IR nodes to lists of collective IDs.
tracer
Runtime tracer for collecting execution statistics.
"""

context: Context
Expand All @@ -87,6 +87,7 @@ class GenState(TypedDict):
max_io_threads: int
stats: StatsCollector
collective_id_map: dict[IR, list[int]]
tracer: StreamingQueryTracer | None


SubNetGenerator: TypeAlias = GenericTransformer[
Expand Down
Loading
Loading