Skip to content

[FEA] Improve cudf::gather scalability as number of columns increases #13509

Open

Description

@abellina

As the number of columns increases for cudf::gather with the same gather map, we see the number of kernels called increase proportionally and the runtime increases linearly. We are wondering if there are better ways to group or "batch" these calls so we perform less kernel invocations that can do more work all at once, in hopes of amortizing some of the cost with many columns or deeply nested schemas.

A very simple example is below. This creates a column of 10 int32_t rows and adds it to a struct N times (where N is between 2 and 1024):

#include <cudf/table/table.hpp>
#include <cudf_test/column_wrapper.hpp>

#include <rmm/mr/device/cuda_memory_resource.hpp>
#include <rmm/mr/device/device_memory_resource.hpp>
#include <rmm/mr/device/pool_memory_resource.hpp>

#include <memory>
#include <string>
#include <vector>
#include <nvtx3/nvToolsExt.h>

int main(int argc, char** argv)
{
  rmm::mr::cuda_memory_resource cuda_mr{};
  rmm::mr::pool_memory_resource mr{&cuda_mr};
  rmm::mr::set_current_device_resource(&mr);
  using col_t = cudf::test::fixed_width_column_wrapper<int32_t>;

  auto const values = std::vector<int32_t>{1,2,3,4,5,6,7,8,9,10};
  for (int num_cols = 2; num_cols <= 1024; num_cols *= 2) {
    std::vector<std::unique_ptr<cudf::column>> members(num_cols);
    for (auto i = 0; i < num_cols; ++i) {
      auto wrapper = col_t(values.begin(), values.end());
      members[i] = wrapper.release();
    }
    auto struct_col = cudf::test::structs_column_wrapper(std::move(members));
    auto gather_map = std::vector<cudf::offset_type>{1}; // gather 1 row
    std::stringstream msg;
    nvtxRangePush(msg.str().c_str()); 
    auto result = cudf::gather(
      cudf::table_view{{struct_col}}, 
      cudf::test::fixed_width_column_wrapper<int32_t>(gather_map.begin(), gather_map.end()),
      cudf::out_of_bounds_policy::NULLIFY);
    nvtxRangePop();
    std::cout << "Result: rows: " << result->num_rows() << " cols: " << result->num_columns() << std::endl;

  }
  return 0;
}

As the column count increases by 2x, the gather kernel takes 2x longer:

Screenshot from 2023-06-05 10-18-52

A similar argument can be made for columns that have nested things like arrays of structs (each with array members). The number of calls to underlying cub calls can increase drastically.

I am filing this issue to solicit comments/patches to see how we could improve this behavior.

Activity

  1. added
    Needs TriageNeed team to review and classify
    SparkFunctionality that helps Spark RAPIDS
    PerformancePerformance related issue
    on Jun 5, 2023
  2. abellina commented on Jun 5, 2023

    @abellina
    ContributorAuthor

    I also believe, that improving the performance for gather will help with copy_if (for non fixed width, for fix width it looks like we implement our own scatter kernel). copy_if is another kernel with very similar behavior. I think we can discuss what we want here and we can target copy_if as a follow on given what we learn with gather.

  3. nvdbaranec commented on Jun 5, 2023

    @nvdbaranec
    Contributor

    If we ignore lists and strings for the moment, I think it would be pretty easy to put together a proof-of-concept for doing batched fixed width gathers as a single kernel invocation (well, maybe 2 - one more for validity).

    Strings is probably not too hard of an extension. Lists would definitely be tricky. I'd have to wrap my head around the list gather stuff to remember :)

  4. bdice commented on Jun 5, 2023

    @bdice
    Contributor

    At one point cudf (possibly before libcudf!) used a stream pool for gather operations. Each gather is independent, so we can launch all the kernels on separate streams and synchronize them with an event on the input stream. I would love to reimplement this approach and see if it can improve the performance. See also #12086.

  5. added
    0 - BacklogIn queue waiting for assignment
    libcudfAffects libcudf (C++/CUDA) code.
    and removed
    Needs TriageNeed team to review and classify
    on Jun 26, 2023
  6. moved this to Needs owner in libcudfon Oct 23, 2024
  7. lamarrr commented on Jul 3, 2026

    @lamarrr
    Contributor

    I implemented a batched wide table gather kernel in #21333 and didn't see any performance improvement due to the memory indirection observed in gather implementations. Even with a batched gather kernel, the cost of fetching a row is O(N_rows) due to the serialized memory fetches.
    Perhaps a multi-stream gather would improve the scalability.

  8. res-life commented on Nov 9, 2026

    @res-life
    Contributor

    PR #25154 makes a related attempt within cudf::partition: it batches allocation-size readbacks for eligible non-null STRING columns, shares permutation/inverse maps, and copies multiple STRING columns in one character-copy kernel launch. Per-column offset scans remain separate.

    This suggests an opportunity to extract reusable permutation-aware batched materialization for other operations that apply the same complete row permutation to multiple columns.

    Candidate call sites identified in a source scan:

    Area Candidate optimization
    sort_by_key, stable_sort_by_key, and segmented_sort_by_key Batch materialization of the values table using the complete permutation returned by sorted order.
    hash_partition Construct the gather map once and reuse it across columns and validity handling; batch eligible column copies. There is overlapping work in #23507.
    round_robin_partition Exploit the analytically defined permutation and evaluate batched materialization without an additional stored inverse map.
    Groupby grouping/materialization Batch columns sharing the same row order, where no rows are dropped. Per-column value sorts may produce different permutations and require separate handling.
    Merge materialization Investigate batching STRING columns that share the same composite source/index order. This would require extending the helper to multiple source tables.

    These are candidates from static inspection, not independently demonstrated speedups. The initial scope could focus on non-null STRING columns and complete permutations, with existing gather paths retained for unsupported cases. Nullable STRING and nested columns need additional care because null rows can contain nonempty physical payloads.

    The earlier experiment in #21333 and its reported lack of improvement are useful context: reducing kernel launches alone does not guarantee better performance. The permutation-specific approach also targets repeated allocation-size synchronization and shared map preparation. Benchmarks should isolate these effects and compare against the existing gather implementation across row counts, column counts, string lengths, and permutation locality.

    #25154 provides a concrete implementation to evaluate and potentially generalize; its partition results do not establish gains for the other call sites above.

  9. res-life commented on Nov 9, 2026

    @res-life
    Contributor
  10. self-assigned this
    on Nov 9, 2026
  11. ttnghia commented on Nov 9, 2026

    @ttnghia
    Contributor

    I'm working on cudf::batch_gather, the batch processiong version for gather that execute as few kernel as possiblie. Currently in good progress.

  12. res-life commented on Nov 9, 2026

    @res-life
    Contributor

    @davidwendt @ttnghia Please help review the potential places needs to do optimization after @ttnghia done cudf::batch_gather.
    Link: #13509 (comment)

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

Metadata

Metadata

Assignees

Labels

0 - BacklogIn queue waiting for assignmentPerformancePerformance related issueSparkFunctionality that helps Spark RAPIDSfeature requestNew feature or requestlibcudfAffects libcudf (C++/CUDA) code.

Type

No type

Projects

  • Status
    Needs owner

Relationships

None yet

Development

No branches or pull requests

Issue actions