Skip to content

[FEA] Implement Multi-output AST JIT & IR CSE - #23621

Open
lamarrr wants to merge 3 commits into
NVIDIA:mainfrom
lamarrr:ast-multi-output-cse
Open

[FEA] Implement Multi-output AST JIT & IR CSE#23621
lamarrr wants to merge 3 commits into
NVIDIA:mainfrom
lamarrr:ast-multi-output-cse

Conversation

@lamarrr

@lamarrr lamarrr commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Adds cudf::compute_table_jit, which evaluates multiple AST expressions in a single JIT-compiled transform and returns one output column per expression, in the supplied order.

The row-IR changes:

  • generate all requested outputs in one device function
  • deduplicate repeated column inputs
  • identify structurally equivalent IR nodes using a structural hash and equality check
  • reuse generated temporaries for common subexpressions, including across outputs
  • track nullability per output and use a null-aware multi-output UDF when nullable inputs require independent output masks

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@lamarrr
lamarrr requested a review from a team as a code owner August 11, 2026 13:42
@github-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Aug 11, 2026
@lamarrr lamarrr added feature request New feature or request non-breaking Non-breaking change labels Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 281ec8d1-8789-4dca-b424-f4ac4915c70d

📥 Commits

Reviewing files that changed from the base of the PR and between 6bedcba and 4a00724.

📒 Files selected for processing (2)
  • cpp/include/cudf/transform.hpp
  • cpp/src/jit/row_ir.hpp
💤 Files with no reviewable changes (1)
  • cpp/include/cudf/transform.hpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/jit/row_ir.hpp

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for JIT evaluation of multiple expressions in a single operation, producing one output column per expression.
    • Reuses shared calculations across expressions to improve efficiency.
    • Preserves independent null handling for each output.
    • Added validation for empty expression collections and documented evaluation errors.
  • Bug Fixes

    • Improved handling of nullable inputs and repeated subexpressions during JIT execution.
    • Ensured filtering and single-expression JIT operations continue to produce correct results.

Walkthrough

The PR adds compute_table_jit for multi-expression JIT evaluation. Row IR performs common-subexpression elimination, generates multiple outputs, and tracks nullability per output. Existing column and filter paths use the new converter API, with expanded CUDA test coverage.

Changes

JIT table transform

Layer / File(s) Summary
Public API and multi-output converter
cpp/include/cudf/transform.hpp, cpp/src/jit/row_ir.hpp, cpp/src/jit/row_ir.cpp
Adds the span-based compute_table_jit and replaces single-expression conversion with multi-expression conversion and per-output nullability.
CSE and code generation
cpp/src/jit/row_ir.hpp, cpp/src/jit/row_ir.cpp
Adds structural hashing, equivalence checks, input reuse, node aliasing, and emitted-state tracking to reuse common subexpressions.
Runtime wiring and validation
cpp/src/transform/transform.cu, cpp/tests/ast/transform_tests.cpp, cpp/tests/jit/row_ir.cpp
Wires column and filter execution through compute_table, adds compute_table_jit, and tests shared expressions, output nullability, null masks, and empty expression rejection.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • rapidsai/cudf#23117 — The PR consumes the multi-expression JIT execution APIs through compute_column_jit.
  • rapidsai/cudf#23173 — The PR uses the multi-output JIT transform and Row IR support.

Suggested reviewers: mythrocks, davidwendt

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: multi-output AST JIT support and row-IR common-subexpression elimination.
Description check ✅ Passed The description directly explains the new API, multi-output execution, row-IR CSE, nullability handling, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
cpp/src/jit/row_ir.hpp (1)

119-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document and enforce the node-address invariant for cse_nodes_ and alias_.

cse_nodes_ and node::alias_ store raw node const*. node keeps a public defaulted move constructor and move assignment. If any code moves a node after instantiate() registers it, both the map entry and every alias_ that points to it dangle, and emit_code then dereferences freed memory.

The current call paths appear safe because nodes are moved only before instantiate(), and output_irs_ holds them through std::unique_ptr. The invariant is implicit. Add a comment on the two members that states the requirement, so a later refactor does not break it silently.

♻️ Suggested documentation of the invariant
   std::unordered_multimap<size_t, node const*>
-    cse_nodes_;  ///< Nodes from completed outputs, indexed by structural hash
+    cse_nodes_;  ///< Nodes from completed outputs, indexed by structural hash.
+                 ///< Non-owning. Registered nodes must not be moved or destroyed
+                 ///< while this context is alive.
   node const* alias_ = nullptr;  ///< The equivalent IR node that this IR aliases, if any. This is
                                  ///< used to avoid emitting duplicate code for equivalent IR nodes.
+                                 ///< Non-owning. The aliased node must outlive this node and must
+                                 ///< not be moved after `instantiate()`.

Run the following script to confirm no node is moved after instantiation:

#!/bin/bash
# Find moves of row_ir::node objects that could invalidate cse_nodes_/alias_ pointers.
fd -e cpp -e hpp -e cu -e cuh . cpp | xargs rg -n -C4 'std::move\([^)]*\bnode\b' 
rg -nP -C4 '\bnode\s*&&|std::vector<\s*node\s*>' cpp/src cpp/tests

Also applies to: 284-287

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/jit/row_ir.hpp` around lines 119 - 120, Document on both cse_nodes_
and node::alias_ that registered nodes must not be moved or relocated after
instantiate() because these raw pointers must remain valid; preserve the
existing ownership and call paths without changing behavior.
cpp/tests/jit/row_ir.cpp (1)

477-479: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the nullability values, not only the count.

The test checks nullability.size(). It does not check the per-output policy. The inputs here are non-nullable and both outputs use PROPAGATE operators, so both entries must be ALL_VALID. Asserting the values protects the per-output nullability logic in generate_code.

💚 Suggested assertion
   EXPECT_EQ(code, expected_code);
   EXPECT_EQ(null_aware, cudf::null_aware::NO);
-  EXPECT_EQ(nullability.size(), 2);
+  ASSERT_EQ(nullability.size(), 2);
+  EXPECT_EQ(nullability[0], cudf::output_nullability::ALL_VALID);
+  EXPECT_EQ(nullability[1], cudf::output_nullability::ALL_VALID);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/jit/row_ir.cpp` around lines 477 - 479, Update the nullability
assertions in the test around generate_code to verify both entries are
ALL_VALID, not just that nullability has size two. Preserve the existing size
check and assert the expected value for each output in the nullability
collection.
cpp/tests/ast/transform_tests.cpp (1)

1567-1590: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a case with two identical expressions.

CommonSubexpression shares sum between different root expressions. It does not cover the case where the same root expression is passed twice. That case exercises node::is_equivalent on two SET_OUTPUT nodes whose subtrees are identical but whose output_reference indices differ. The new output_reference::operator== is what prevents the second output from aliasing the first and losing its store.

💚 Suggested additional test
+TEST_F(ComputeTableJitTest, DuplicateExpressions)
+{
+  auto c0    = column_wrapper<int32_t>{1, 2, 3, 4};
+  auto c1    = column_wrapper<int32_t>{10, 20, 30, 40};
+  auto table = cudf::table_view{{c0, c1}};
+
+  auto ref0 = cudf::ast::column_reference{0};
+  auto ref1 = cudf::ast::column_reference{1};
+  auto sum  = cudf::ast::operation{cudf::ast::ast_operator::ADD, ref0, ref1};
+
+  std::array<std::reference_wrapper<cudf::ast::expression const>, 2> expressions{sum, sum};
+  auto result = cudf::compute_table_jit(table, expressions);
+
+  auto expected_sum = column_wrapper<int32_t>{11, 22, 33, 44};
+  auto expected     = cudf::table_view{{expected_sum, expected_sum}};
+
+  CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result->view());
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/ast/transform_tests.cpp` around lines 1567 - 1590, Add a
duplicate-root-expression case to the CommonSubexpression test by passing the
same expression twice in the expressions array and expecting two distinct,
identical output columns. Ensure the assertions verify both outputs are stored
independently, exercising node::is_equivalent and output_reference::operator==
behavior.
cpp/src/jit/row_ir.cpp (1)

891-906: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Track nullable inputs per output to avoid redundant masks.

has_nullable_inputs marks an output that reads only valid inputs as PRESERVE, so make_outputs allocates and updates an unnecessary null mask. The null-aware ALL_VALID path is safe because generated assignments engage the cuda::std::optional<T> output, and null-mask writes are skipped when no mask exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/jit/row_ir.cpp` around lines 891 - 906, Update the nullability
handling around null_policies and generate_null_aware_udf so each output’s
nullable-input usage is tracked independently rather than applying
has_nullable_inputs to every output. Mark outputs that only read valid inputs as
ALL_VALID, and ensure make_outputs skips allocating or updating redundant null
masks while preserving optional-based null propagation for null-aware
assignments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cpp/src/jit/row_ir.cpp`:
- Around line 891-906: Update the nullability handling around null_policies and
generate_null_aware_udf so each output’s nullable-input usage is tracked
independently rather than applying has_nullable_inputs to every output. Mark
outputs that only read valid inputs as ALL_VALID, and ensure make_outputs skips
allocating or updating redundant null masks while preserving optional-based null
propagation for null-aware assignments.

In `@cpp/src/jit/row_ir.hpp`:
- Around line 119-120: Document on both cse_nodes_ and node::alias_ that
registered nodes must not be moved or relocated after instantiate() because
these raw pointers must remain valid; preserve the existing ownership and call
paths without changing behavior.

In `@cpp/tests/ast/transform_tests.cpp`:
- Around line 1567-1590: Add a duplicate-root-expression case to the
CommonSubexpression test by passing the same expression twice in the expressions
array and expecting two distinct, identical output columns. Ensure the
assertions verify both outputs are stored independently, exercising
node::is_equivalent and output_reference::operator== behavior.

In `@cpp/tests/jit/row_ir.cpp`:
- Around line 477-479: Update the nullability assertions in the test around
generate_code to verify both entries are ALL_VALID, not just that nullability
has size two. Preserve the existing size check and assert the expected value for
each output in the nullability collection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 70e84426-a33b-4404-9b79-1a59052e160b

📥 Commits

Reviewing files that changed from the base of the PR and between 76ea4bf and 6bedcba.

📒 Files selected for processing (6)
  • cpp/include/cudf/transform.hpp
  • cpp/src/jit/row_ir.cpp
  • cpp/src/jit/row_ir.hpp
  • cpp/src/transform/transform.cu
  • cpp/tests/ast/transform_tests.cpp
  • cpp/tests/jit/row_ir.cpp

@bdice bdice left a comment

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.

Really nice work. The CSE implementation is very clear. I would be interested in seeing some benchmarks for this, perhaps comparing to some baseline like a series of column transforms.

Comment thread cpp/src/jit/row_ir.cpp
{
if (auto* column = std::get_if<column_input>(&in);
column != nullptr && column->table_source.has_value() && column->column_index.has_value()) {
for (size_t i = 0; i < inputs_.size(); ++i) {

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 this use a find algorithm?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants