Skip to content

Improve nvtext::jaccard_index performance using cuco shared-memory sets - #24698

Draft
davidwendt wants to merge 19 commits into
NVIDIA:mainfrom
davidwendt:improve-jaccard
Draft

davidwendt wants to merge 19 commits into
NVIDIA:mainfrom
davidwendt:improve-jaccard

Conversation

@davidwendt

@davidwendt davidwendt commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

Improves the performance of nvtext::jaccard_index by replacing the segmented-sort based counting of unique and intersecting substring hashes with cuco static_set_ref sets in shared memory.

The substring hashes for each row are still computed once. The hash kernel now compacts the hashes using a warp ballot instead of a serial copy by lane 0. Each row is then counted by one of two kernels:

  • Rows whose sets fit in shared memory (up to 384 substrings) are processed by a warp per row. Each warp inserts the first column's hashes into one set and the second column's hashes into another set. Each successful insert into the second set is checked against the first set to count the intersection.
  • Larger rows are partitioned by the high bits of their hashes in a single scatter step, and each (row, partition) is processed by a block using the same set logic. The counts from all of a row's partitions are added together. If a partition's set fills up, the block reprocesses that partition in smaller sub-partitions.

This removes the CUB segmented sort, which also removes the workaround for sorting more than 2B elements in sections.

A hash value of 0 is now stored as 1, since 0 is used as the empty-slot sentinel. Previously, substrings that hashed to 0 were dropped and their slot in the row was left uninitialized.

The benchmark is updated so that each pair of rows shares half of its text, so the intersection counts are no longer always 0. It also adds row widths of 8192 and 32768 bytes to measure long rows, and skips configurations larger than 1GB. A new gtest compares the results for rows of up to 100K characters against a host reference implementation.

Benchmark results on an RTX A6000 (substring_width=5):

max_width num_rows before (ms) after (ms) speedup
128 262144 3.23 2.30 1.4x
512 262144 100.79 8.36 12.1x
2048 262144 127.56 31.95 4.0x
8192 131072 116.40 72.29 1.6x
32768 32768 166.11 75.07 2.2x

The large improvement at 512 is because the CUB segmented sort handles segments of a few hundred items poorly. Peak memory is about half of before when all rows fit in a warp's sets, and about the same otherwise.

Checklist

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

🤖 Generated with Claude Code

Replaces the segmented-sort based unique/intersect counting with cuco
static_set_refs in shared memory. Rows that fit are processed with a
warp per row. Larger rows are partitioned by the high bits of their
substring hashes and each (row, partition) is processed by a block.

Also updates the benchmark so the input pairs share common text and
includes longer rows, and adds a test for long rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@davidwendt davidwendt added libcudf Affects libcudf (C++/CUDA) code. improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Oct 19, 2026
@copy-pr-bot

copy-pr-bot Bot commented Oct 19, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

@davidwendt

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/cudf/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 8c943d37-6a8e-40d4-ade9-2422f6bba721
📥 Commits

Reviewing files that changed from the base of the PR and between 1b906f3 and b89727d.

📒 Files selected for processing: 3
  • cpp/benchmarks/text/jaccard.cpp
  • cpp/src/text/jaccard.cu
  • cpp/tests/text/jaccard_tests.cpp

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

📝 Summary

Summary by CodeRabbit

  • Improvements
    • Updated Jaccard index calculations to support rows containing very long strings, including empty strings, while preserving the existing similarity ratio and returning zero when the union is empty. The revised approach also handles rows with different amounts of distinct content. These changes are covered by additional comparisons against a reference implementation across a range of input lengths.

The Jaccard implementation now counts substring hashes with sets instead of a sorted-hash pipeline. It uses warp-level counting for smaller rows and partitioned counting for larger rows. Tests compare results with a host reference, and the benchmark adds shared-substring inputs and wider benchmark sizes.

Changes: Jaccard Counting

Layer / File(s) Summary
Hash representation and compaction
cpp/src/text/jaccard.cu
The code adds set hash types and capacities, and compacts valid substring hashes into unsorted row outputs.
Warp-level row counting
cpp/src/text/jaccard.cu
Rows within the warp-set capacity use separate sets to count unique hashes and intersections. Warp reductions write counts to row outputs.
Partitioned counting for larger rows
cpp/src/text/jaccard.cu
Larger rows are partitioned and counted with block-level sets. The counting path retries with finer sub-partitions when a set fills.
Jaccard integration and coverage
cpp/src/text/jaccard.cu, cpp/tests/text/jaccard_tests.cpp, cpp/benchmarks/text/jaccard.cpp
jaccard_index connects hashing and counting to the result calculation. Tests compare results with an ASCII reference. Benchmarks use paired inputs with a shared string and include larger width values.

Priority: ➖ Normal

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

Change: Refactor

Suggested reviewers: vyasr, kylefromnvidia, bdice

Merge Risk: ⚪ Minimal · up to b8972

The faster set-based Jaccard counting keeps 64-bit offsets for large inputs and computes the same ratio as before. No concrete correctness or stability defect was found, so the change appears ready to merge after normal CI.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: improving nvtext::jaccard_index performance with cuco shared-memory sets.
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.
Description check Passed The pull request description directly explains the Jaccard implementation changes, shared-memory set approach, benchmark updates, tests, and reported performance results.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

davidwendt and others added 2 commits October 20, 2026 14:50
Replaces __popc with cuda::std::popcount, names the hash bit width used
by partition_of, and documents why cub is used for the block-wide
collectives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

@davidwendt davidwendt moved this to Helps NeMo Curator in libcudf Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improvement / enhancement to an existing function libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change

Projects

Status: Helps NeMo Curator

Development

Successfully merging this pull request may close these issues.

1 participant