-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add streaming_groupby for stateful streaming aggregation
#21924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
646e452
9b8eca0
4bcb63f
4d9be13
6ed0435
4fc5369
f4695ca
0c185d6
841148d
83370d8
328fb12
85346eb
7b4c7a7
454668c
14f81eb
4599992
1ee32d3
aa6c1ae
c300f7a
ab88165
7707bb6
ae1ddfb
a05a32b
3299ae1
d6eccb9
1441ff9
9d5c597
f1d455e
c3282ef
85b3110
00c9e52
03597a7
94736b2
2b6d135
52178bb
f41fcd7
3ea5db6
e1ed93c
41aba07
37ee31e
5f202af
99347c6
591eb76
4143742
560eab6
0b7eed9
7323d23
d111582
22ea99a
de3d106
e0e8baa
c4faed0
cbd708d
160c960
035e8e9
720dd5b
4081eb1
d9265d5
a0007a5
6432075
2e03526
cbdc728
d8726b5
2cfd96b
c3bfe21
aa71768
8c0d1fb
97081a7
ce523da
ab20d11
8229215
270c33c
2cd760a
baeb5dd
49eaecb
7b339f3
18112a0
21ed3ba
56d8977
ec6e8b2
22d82d4
251d8db
8bf5ceb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2019-2024, NVIDIA CORPORATION. | ||
| * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
|
|
@@ -137,7 +137,7 @@ class groupby { | |
| * result is the same order as was specified in the request. | ||
| * | ||
| * The returned `table` contains the group labels for each group, i.e., the | ||
| * unique rows from `keys`. Element `i` across all aggregation results | ||
| * distinct rows from `keys`. Element `i` across all aggregation results | ||
| * belongs to the group at row `i` in the group labels table. | ||
| * | ||
| * The order of the rows in the group labels is arbitrary. Furthermore, | ||
|
|
@@ -169,7 +169,7 @@ class groupby { | |
| * perform | ||
| * @param stream CUDA stream used for device memory operations and kernel launches. | ||
| * @param mr Device memory resource used to allocate the returned table and columns' device memory | ||
| * @return Pair containing the table with each group's unique key and | ||
| * @return Pair containing the table with each group's distinct key and | ||
| * a vector of aggregation_results for each request in the same order as | ||
| * specified in `requests`. | ||
| */ | ||
|
|
@@ -407,6 +407,189 @@ class groupby { | |
| rmm::cuda_stream_view stream, | ||
| rmm::device_async_resource_ref mr); | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Request for a single streaming groupby aggregation on a column. | ||
| * | ||
| * Analogous to `aggregation_request` but identifies the value column by index rather than | ||
| * by `column_view`, since data arrives in batches after construction, and carries exactly | ||
| * one aggregation per request. | ||
| * | ||
| * `column_index` refers to the position of the value column in the `table_view` passed | ||
| * to `streaming_groupby::aggregate()`. Multiple aggregations on the same column are | ||
| * expressed as separate requests (e.g., `[{col, sum}, {col, mean}]`). Internal | ||
| * deduplication ensures redundant computations are shared automatically. | ||
| */ | ||
| struct streaming_aggregation_request { | ||
| size_type column_index; ///< Index of the value column | ||
| std::unique_ptr<groupby_aggregation> aggregation; ///< Desired aggregation | ||
| }; | ||
|
PointKernel marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * @brief Stateful streaming groupby that accumulates partial aggregates across batches. | ||
| * | ||
| * `streaming_groupby` and the stateless `groupby` serve different use cases. Use | ||
| * the stateless `groupby` for single-shot aggregation when all input fits in memory | ||
| * at once. Use `streaming_groupby` when input arrives over multiple batches and | ||
| * memory efficiency matters: peak memory does not scale with the number of *input* | ||
| * rows, because only the distinct keys seen so far and one aggregation slot per | ||
| * group are kept across batches. Arbitrarily long high-duplicate streams therefore | ||
| * accumulate without running out of memory. | ||
| * | ||
| * If memory is not a concern, concatenating all batches and calling the stateless | ||
| * `groupby` once is also a valid choice. Reach for `streaming_groupby` when | ||
| * (a) the cumulative input does not fit in memory, or (b) partial-state aggregation | ||
| * across distributed workers (`merge()`) is part of the workload. | ||
| * | ||
| * Per-batch cost is O(batch_size): each batch does direct hash table insertion | ||
| * and in-place aggregation updates against the persistent state. Partial states | ||
| * can be combined via `merge()`, and final results are produced via `finalize()`. | ||
| * | ||
| * The `max_distinct_keys` parameter sets the upper bound on the number of distinct key | ||
| * combinations across the lifetime of this object. The persistent state is sized to | ||
| * `max_distinct_keys` (constant for the lifetime of the object); the stored distinct | ||
| * keys grow with the number of distinct keys actually seen, so the incremental key | ||
| * storage is O(`distinct_keys()` × key_size) and does not scale with cumulative input | ||
| * rows. | ||
| * | ||
| * Cumulative input rows are not bounded — only cumulative distinct keys. A single | ||
| * batch may also not exceed `max_distinct_keys` rows; this is an implementation | ||
| * limit because each in-flight batch row is encoded as `max_distinct_keys + row_idx` | ||
| * inside the hash set, which must fit in `cudf::size_type`. | ||
| * | ||
| * All column types (including variable-width types such as strings, lists, and structs) | ||
| * are supported for key columns. Only hash-based aggregation kinds are supported; use | ||
| * `is_streaming_groupby_supported()` to query a specific (value type, aggregation kind) | ||
| * combination. | ||
| * | ||
| * Supported aggregation kinds: | ||
| * SUM, SUM_OF_SQUARES, PRODUCT, MIN, MAX, COUNT_VALID, COUNT_ALL, | ||
| * MEAN, M2, VARIANCE, STD | ||
| * | ||
| * @throws std::invalid_argument for unsupported aggregation kinds | ||
| * @throws std::invalid_argument if a single batch exceeds `max_distinct_keys` rows | ||
| * @throws cudf::logic_error if cumulative distinct keys exceed `max_distinct_keys` | ||
| */ | ||
| class streaming_groupby { | ||
| public: | ||
| streaming_groupby() = delete; | ||
| ~streaming_groupby(); | ||
| streaming_groupby(streaming_groupby const&) = delete; | ||
| streaming_groupby& operator=(streaming_groupby const&) = delete; | ||
|
|
||
| /** @brief Move constructor. */ | ||
| streaming_groupby(streaming_groupby&&) noexcept; | ||
|
|
||
| /** | ||
| * @brief Move assignment operator. | ||
| * @return Reference to this object. | ||
| */ | ||
| streaming_groupby& operator=(streaming_groupby&&) noexcept; | ||
|
|
||
| /** | ||
| * @brief Construct a streaming groupby object with a persistent hash table. | ||
| * | ||
| * @param key_indices Indices of columns in the data table that serve as groupby keys | ||
| * @param requests The aggregations to perform and which columns to aggregate | ||
| * @param max_distinct_keys Upper bound on distinct key combinations. The hash set, | ||
| * companion vectors, and aggregation results table are all sized to this | ||
| * capacity. Cumulative input rows are not bounded. | ||
| * @param null_handling Indicates whether rows in keys that contain NULL values should be included | ||
| * | ||
| * @throws std::invalid_argument if `max_distinct_keys <= 0` | ||
| * @throws std::invalid_argument if any requested aggregation kind is unsupported | ||
| */ | ||
| explicit streaming_groupby(host_span<size_type const> key_indices, | ||
| host_span<streaming_aggregation_request const> requests, | ||
| size_type max_distinct_keys, | ||
| null_policy null_handling = null_policy::EXCLUDE); | ||
|
|
||
| /** | ||
| * @brief Feed a batch of data into the streaming aggregation. | ||
| * | ||
| * Batch keys are inserted into the persistent hash set and aggregation results | ||
| * are updated atomically. The input `data` table is not referenced after this | ||
| * call returns. | ||
| * | ||
| * @param data Table containing both key and value columns | ||
| * @param stream CUDA stream used for device memory operations and kernel launches | ||
| * | ||
| * @throws std::invalid_argument if `data.num_rows()` exceeds `max_distinct_keys` | ||
| * @throws cudf::logic_error if cumulative distinct keys exceed `max_distinct_keys` | ||
| */ | ||
| void aggregate(table_view const& data, rmm::cuda_stream_view stream = cudf::get_default_stream()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. request: can we have an API tell us the unique number of keys so far. seems like it's stored anyway and would be trivial to surface.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bump ^. Realistically, it's not possible to know the exact cardinailty beforehand and engines are supposed to make a guess. So when it does grow beyond initial mad_distinct_keys, we want to create a new streaming_groupby with more capacity, merge the current one into it and destroy. Currently, this means this aggregate() call lives in a try{} with the resizing done in catch{} block. It would be nicer to not have to rely on exceptions for this runtime behaviour.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I'm wondering whether this should store a stream instead of taking a new one each time. Do we need to wait on the previous stream before calling aggregate with a new batch and new stream?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I see that you only get that information AFTER checking for new distinct keys in the current batch. Which means you need to do some work before you can say the current batch will overflow or not.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already exposed as |
||
|
|
||
| /** | ||
| * @brief Merge another streaming_groupby's accumulated partial state into this one. | ||
| * | ||
| * Extracts the other object's accumulated intermediate state and merges it into this | ||
| * object's persistent hash table. The other object is not modified. | ||
| * Both objects must have been constructed with compatible aggregation requests, | ||
| * and this object must have had at least one `aggregate()` call. | ||
| * | ||
| * @param other The streaming_groupby whose partial state to merge | ||
| * @param stream CUDA stream used for device memory operations and kernel launches | ||
| * | ||
| * @throws std::invalid_argument if the other object has more distinct keys than | ||
| * `max_distinct_keys` | ||
| * @throws cudf::logic_error if this object has not been initialized via `aggregate()` | ||
| * @throws cudf::logic_error if distinct keys exceed `max_distinct_keys` after merge | ||
| */ | ||
| void merge(streaming_groupby const& other, | ||
| rmm::cuda_stream_view stream = cudf::get_default_stream()); | ||
|
|
||
| /** | ||
| * @brief Finalize the accumulated partial aggregates into final results. | ||
| * | ||
| * For most aggregation kinds the partial state is the final result. For kinds like | ||
| * MEAN, VARIANCE, or STD, a finalization step converts the internal partial representation | ||
| * (e.g., sum+count) into the user-facing result. | ||
| * | ||
| * This does not modify the internal state; `aggregate()` may be called again afterward. | ||
| * | ||
| * @param stream CUDA stream used for device memory operations and kernel launches | ||
| * @param mr Device memory resource used to allocate the returned table and columns | ||
| * @return Pair of distinct keys table and a vector of aggregation_results (one per request) | ||
| * | ||
| * @throws cudf::logic_error if no data has been accumulated | ||
| */ | ||
| [[nodiscard]] std::pair<std::unique_ptr<table>, std::vector<aggregation_result>> finalize( | ||
| rmm::cuda_stream_view stream = cudf::get_default_stream(), | ||
| rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()) const; | ||
|
|
||
| /** | ||
| * @brief Returns the number of distinct keys accumulated so far. | ||
| * | ||
| * Returns 0 before any successful `aggregate()` or `merge()` call. | ||
| * | ||
| * @return The current count of distinct keys in the persistent hash table | ||
| */ | ||
| [[nodiscard]] size_type distinct_keys() const noexcept; | ||
|
|
||
| private: | ||
| struct impl; | ||
| std::unique_ptr<impl> _impl; | ||
|
|
||
| void do_aggregate(table_view const& data, rmm::cuda_stream_view stream); | ||
| void do_merge(streaming_groupby const& other, rmm::cuda_stream_view stream); | ||
| [[nodiscard]] std::pair<std::unique_ptr<table>, std::vector<aggregation_result>> do_finalize( | ||
| rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) const; | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Returns true if `streaming_groupby` supports the given value type and | ||
| * aggregation kind combination. | ||
| * | ||
| * Use this to query support without constructing a `streaming_groupby`. A `true` | ||
| * return implies that an `aggregate()` call with a value column of `values_type` and | ||
| * an aggregation of `kind` will not be rejected on type/kind grounds. | ||
| * | ||
| * @param values_type Type of the value column the aggregation would run on | ||
| * @param kind Aggregation kind | ||
| * @return True if the combination is supported, false otherwise | ||
| */ | ||
| [[nodiscard]] bool is_streaming_groupby_supported(data_type values_type, aggregation::Kind kind); | ||
|
|
||
| /** @} */ | ||
| } // namespace groupby | ||
| } // namespace CUDF_EXPORT cudf | ||
Uh oh!
There was an error while loading. Please reload this page.