A deep dive into cuCascade's data lifecycle, batch read-only and mutable locking accessor classes, repositories, and representation conversion.
- Overview
- Data Representations
- Data Batch Lifecycle
- Representation Conversion
- Data Repositories
- Data Repository Manager
- Integration with Memory Module
- Key Source Files
The data module manages the lifecycle of data as it flows through processing pipelines and moves between memory tiers. It provides:
- Tier-agnostic data representations -- abstract interface with GPU and host implementations
- Locking read-only and mutable accessor classes for batch lifecycle -- prevents concurrent access conflicts during processing and tier movement
- Type-indexed conversion -- extensible registry for converting data between representations
- Partitioned repositories -- thread-safe storage with non-blocking retrieval and optional batch positions
- Multi-pipeline coordination -- manages data across operators with atomic ID generation
The data module depends on the memory module for allocators, memory spaces, and CUDA streams.
File: include/cucascade/data/common.hpp
All data in cuCascade is accessed through the idata_representation interface:
class idata_representation {
public:
virtual std::size_t get_size_in_bytes() const = 0;
virtual std::unique_ptr<idata_representation> clone(::cuda::stream_ref stream) = 0;
template <class TargetType>
TargetType& cast(); // Unsafe downcast (no dynamic_cast overhead)
Tier get_current_tier() const;
int get_device_id() const;
memory_space& get_memory_space() const;
};Data representations are thin wrappers -- they hold the data but delegate storage details (tier, device, allocator) to their associated memory_space.
File: include/cucascade/data/gpu_data_representation.hpp
Wraps a cudf::table residing in GPU device memory:
class gpu_table_representation : public idata_representation {
std::unique_ptr<cudf::table> _table;
public:
std::unique_ptr<cudf::table> release_table(::cuda::stream_ref stream); // Transfer ownership
std::size_t get_size_in_bytes() const override;
std::unique_ptr<idata_representation> clone(::cuda::stream_ref stream) override;
};clone()performs a deep copy usingcudf::table(table.view(), stream)- Owns the
cudf::tableobject but not the underlying GPU memory (managed by the allocator)
File: include/cucascade/data/cpu_data_representation.hpp
The preferred host representation. Directly copies each column's GPU device buffers into pinned
host memory without an intermediate GPU-side contiguous allocation. Column layout is described by
custom per-column column_metadata structs, enabling reconstruction without cudf's pack format.
class host_data_representation : public idata_representation {
std::unique_ptr<memory::host_table_allocation> _host_table;
public:
const std::unique_ptr<memory::host_table_allocation>& get_host_table() const;
std::size_t get_size_in_bytes() const override;
std::unique_ptr<idata_representation> clone(::cuda::stream_ref stream) override;
};A host_table_allocation (include/cucascade/memory/host_table.hpp) contains:
allocation--multiple_blocks_allocation(vector of fixed-size pinned memory blocks)columns-- per-columncolumn_metadatatrees describing buffer offsets, sizes, and nestingdata_size-- total bytes stored across all blocks
Each column_metadata node captures type_id, num_rows, null_count, scale (for decimals),
plus buffer offsets/sizes for the null mask and data buffer, and recursive children for nested
types (LIST, STRUCT, STRING, DICTIONARY32).
This representation avoids the extra GPU allocation and GPU-to-GPU copy that cudf::pack performs.
cudaMemcpyBatchAsync is used to transfer all column buffers in a single driver call per direction,
achieving ~99% of raw PCIe bandwidth even with multiple concurrent threads.
clone() copies data block-by-block with std::memcpy and duplicates the column metadata tree.
File: include/cucascade/data/cpu_data_representation.hpp
A legacy representation that uses cudf::pack() to serialize the table to a contiguous GPU buffer
before copying it to host memory. Useful when cudf's serialization format is required.
class host_data_packed_representation : public idata_representation {
std::unique_ptr<memory::host_table_packed_allocation> _host_table;
public:
const std::unique_ptr<memory::host_table_packed_allocation>& get_host_table() const;
std::size_t get_size_in_bytes() const override;
std::unique_ptr<idata_representation> clone(::cuda::stream_ref stream) override;
};A host_table_packed_allocation (include/cucascade/memory/host_table_packed.hpp) contains:
allocation--multiple_blocks_allocation(vector of fixed-size pinned memory blocks)metadata-- serialized cuDF table metadata fromcudf::pack()(for reconstruction viacudf::unpack())data_size-- actual data size in bytes
clone() copies data block-by-block with std::memcpy and duplicates the metadata vector.
File: include/cucascade/data/data_batch.hpp
A data_batch wraps a data representation and controls access through three states using a
reader-writer lock model. Data is only accessible through RAII accessor objects that hold the
appropriate lock — the idle data_batch pointer grants no data access.
Construct batches with the factory so they are always managed by the shared ownership required by the transition API:
auto batch = data_batch::make(batch_id, std::move(representation));| State | Meaning |
|---|---|
idle |
No active locks. Available for reading, mutation, or tier movement. |
read_only |
One or more read_only_data_batch shared locks are active (_read_only_count > 0). Concurrent readers allowed; exclusive access blocked. |
mutable_locked |
One mutable_data_batch exclusive lock is active. No concurrent readers; full read/write access. |
stateDiagram-v2
direction LR
idle --> read_only : to_read_only() / try_to_read_only()
idle --> mutable_locked : to_mutable() / try_to_mutable()
read_only --> idle : to_idle(read_only_data_batch&&) [last reader]
read_only --> mutable_locked : readonly_to_mutable(read_only_data_batch&&)
mutable_locked --> idle : to_idle(mutable_data_batch&&)
mutable_locked --> read_only : mutable_to_readonly(mutable_data_batch&&)
Key rules:
- Non-static transitions (
to_read_only,to_mutable,try_to_*) useshared_from_this()and do not consume the caller'sshared_ptr. - Static transitions (
to_idle,readonly_to_mutable,mutable_to_readonly) consume the accessor via&&, making the source null at the call site — the compiler enforces that you cannot use an accessor after releasing it. to_read_only()/to_mutable()block until the lock is available.try_to_read_only()/try_to_mutable()are non-blocking and returnstd::nullopton failure.- Multiple
read_only_data_batchhandles may coexist on the same batch (concurrent reads). mutable_data_batchis exclusive: it cannot coexist with any other reader or writer.- Copying a
read_only_data_batchacquires a new shared lock on the parent, incrementing_read_only_count.
See data_batch_state_transitions.md for the complete reference.
Access to batch data is only possible through one of two RAII accessor classes:
read_only_data_batch — shared (read) lock:
// Acquire shared lock (blocks if exclusive lock held)
read_only_data_batch ro = batch->to_read_only();
// Access data
auto* data = ro.get_data()->cast<gpu_table_representation>();
process(data->get_table_view());
// Release: either let ro go out of scope, or explicitly return to idle
auto idle_batch = cucascade::data_batch::to_idle(std::move(ro));Properties:
- Copyable — each copy acquires a new shared lock;
_read_only_countincrements per copy. - Movable — moves transfer lock ownership without changing the count.
- Destruction or
to_idle()decrements_read_only_count; the batch returns toidlewhen the count reaches zero.
mutable_data_batch — exclusive (write) lock:
// Acquire exclusive lock (blocks until all readers and writers release)
mutable_data_batch mut = batch->to_mutable();
// Read and write data
mut.set_data(std::move(new_representation));
// Release back to idle
auto idle_batch = cucascade::data_batch::to_idle(std::move(mut));Properties:
- Move-only — no copies allowed; only one exclusive lock can exist at a time.
- Destruction or
to_idle()releases the exclusive lock.
Upgrade / Downgrade:
// Upgrade: shared → exclusive (releases shared, acquires exclusive — may block)
mutable_data_batch mut = cucascade::data_batch::readonly_to_mutable(std::move(ro));
// Downgrade: exclusive → shared (releases exclusive, acquires shared — may block)
read_only_data_batch ro2 = cucascade::data_batch::mutable_to_readonly(std::move(mut));Each data_batch uses a std::shared_mutex: shared locks for readers (read_only_data_batch) and unique locks for writers (mutable_data_batch). The observable state (batch_state enum) is tracked atomically so it can be queried without acquiring the mutex. _read_only_count is also atomic for lock-free reader-count queries.
Independent of the locking model, batches support a subscriber reference count:
batch->subscribe(); // Increment interest count (lock-free)
batch->unsubscribe(); // Decrement interest count (lock-free)
batch->get_subscriber_count(); // Query count (lock-free)This is used by the pipeline and downgrade executor to track which batches are still of interest, independently of whether they are currently locked.
// Via read-only accessor (caller already holds lock)
read_only_data_batch ro = batch->to_read_only();
auto cloned = ro.clone(new_batch_id, stream);
// With representation conversion
auto cloned = ro.clone_to<host_data_representation>(registry, new_batch_id, host_space, stream);Cloning produces a new shared_ptr<data_batch> in idle state with the given ID. The clone
contains a deep copy of the data representation, residing in the same memory space as the original
(or a different space when using clone_to).
File: include/cucascade/data/representation_converter.hpp
The representation_converter_registry stores conversion functions indexed by (source_type, target_type):
// Register a custom converter
registry.register_converter<gpu_table_representation, host_data_representation>(
[](idata_representation& source,
const memory_space* target,
::cuda::stream_ref stream,
memory::reservation* reservation)
-> std::unique_ptr<idata_representation> {
// Convert GPU table to host (direct copy)
return ...;
}
);
// Convert data
auto host_data = registry.convert<host_data_representation>(
*gpu_data, target_host_space, stream);
// Convert using a caller-owned target reservation
auto reserved_host_data = registry.convert<host_data_representation>(
*gpu_data, target_reservation, stream);The registry is thread-safe (all operations guarded by std::mutex).
Registered via register_builtin_converters():
| Source | Target | Method |
|---|---|---|
gpu_table_representation |
host_data_representation |
cudaMemcpyBatchAsync (D2H) — copies each column's buffers directly to pinned host blocks; ~99% PCIe bandwidth |
host_data_representation |
gpu_table_representation |
Allocates rmm::device_buffer per column buffer, cudaMemcpyBatchAsync (H2D), reconstructs cudf::column tree |
gpu_table_representation |
host_data_packed_representation |
cudf::pack() on GPU -> cudaMemcpyAsync (D2H) to multi-block host allocation |
host_data_packed_representation |
gpu_table_representation |
cudaMemcpyAsync (H2D) from host blocks -> cudf::unpack() on device |
gpu_table_representation |
gpu_table_representation |
cudf::pack() -> cudaMemcpyPeerAsync -> cudf::unpack() (cross-device copy) |
host_data_packed_representation |
host_data_packed_representation |
Block-by-block std::memcpy + metadata copy (cross-NUMA copy) |
GPU to HOST (direct copy — preferred):
sequenceDiagram
participant Caller
participant Registry as converter_registry
participant GPU as gpu_table_representation
participant Host as host_data_representation
participant HostMR as fixed_size_host_memory_resource
Caller->>Registry: convert<host_data_representation>(gpu_data, host_space, stream)
Registry->>HostMR: allocate_multiple_blocks(total_column_bytes)
Note over HostMR: Returns vector of 1MB pinned host blocks
Registry->>Registry: Traverse column tree, compute buffer offsets into host blocks
Registry->>Registry: cudaMemcpyBatchAsync(all_bufs_in_one_call, D2H, stream)
Note over Registry: Single driver call for all column buffers
Registry->>Host: Create host_data_representation(blocks, column_metadata_tree)
Registry-->>Caller: unique_ptr<host_data_representation>
GPU to HOST (packed — uses cudf serialization):
sequenceDiagram
participant Caller
participant Registry as converter_registry
participant GPU as gpu_table_representation
participant Host as host_data_packed_representation
participant HostMR as fixed_size_host_memory_resource
Caller->>Registry: convert<host_data_packed_representation>(gpu_data, host_space, stream)
Registry->>GPU: cudf::pack(table)
Note over GPU: Serializes table to contiguous buffer + metadata
Registry->>HostMR: allocate_multiple_blocks(data_size)
Note over HostMR: Returns vector of 1MB pinned host blocks
Registry->>Registry: cudaMemcpyAsync(host_blocks, gpu_buffer, D2H, stream)
Note over Registry: Copies data block by block
Registry->>Host: Create host_data_packed_representation(blocks, metadata)
Registry-->>Caller: unique_ptr<host_data_packed_representation>
File: include/cucascade/data/data_repository.hpp
A repository is a thread-safe, partitioned queue of shared data_batch pointers with optional
positions. Storage operations hold the repository mutex and return immediately, regardless of
batch state. They do not acquire batch accessors — callers acquire the appropriate
read_only_data_batch or mutable_data_batch accessor after popping.
// Add a batch (idle state)
repository.add_data_batch(batch_ptr, partition_idx);
// Pop the next batch from a partition (non-blocking; returns {nullptr, nullopt} if empty)
auto [batch, position] = repository.pop_next_data_batch_with_position(partition_idx);
if (batch) {
auto ro = batch->to_read_only(); // acquire shared lock
process(ro.get_data());
}
// Pop a specific batch by ID (returns {nullptr, nullopt} if not found)
auto [batch, position] = repository.pop_data_batch_by_id_with_position(batch_id, partition_idx);
// Non-removing payload inspection is available in every ordering mode
auto [batch, position] = repository.get_data_batch_by_id_with_position(batch_id, partition_idx);pop_next_data_batch_with_position is non-blocking — it returns {nullptr, std::nullopt} immediately if the partition is
empty. Emptiness describes current storage; producer completion must be established separately.
All partition-specific reads throw std::out_of_range for an invalid partition index.
Plain retrieval methods return just the payload pointer (or nullptr); the _with_position
variants return the pointer and optional position. Both work in every ordering mode.
Repositories use a vector of partitions, each with an arrival-order list, a position-keyed map
and a batch-ID index. Lookups use indexes, and FIFO removal does not shift the remaining entries.
The indexes contain only resident entries, including separate entries when a batch is shared
across positions. Each partition retains arrival order. Insertion grows the partition vector when needed;
set_num_partitions() can also grow it explicitly and rejects zero, equal, or smaller counts.
// Partition 0: pipeline A data
repository.add_data_batch(batch_a, 0);
// Partition 1: pipeline B data
repository.add_data_batch(batch_b, 1);
// Pop from partition 0 only
auto [batch, position] = repository.pop_next_data_batch_with_position(0);Partition membership, sequence within a partition, and device placement are independent. Partition indices do not declare or enforce logical sequence. Callers that assign sort-range indices must explicitly choose the appropriate partition traversal order.
using positioned_data_batch =
std::pair<std::shared_ptr<data_batch>, std::optional<size_t>>;get_ordering() returns the synchronized batch-positioning mode, uniformly across all partitions:
| Mode | Meaning |
|---|---|
UNKNOWN |
No successful insertion has established a batch mode |
UNORDERED |
Every stored entry has nullopt for its position |
ORDERED |
Every stored entry has an explicit partition-local position |
add_data_batch(batch, partition_idx) establishes UNORDERED from UNKNOWN and throws
repository_ordering_error on an ORDERED repository. add_data_batch_at_position(batch, position, partition_idx) establishes ORDERED from UNKNOWN and throws repository_ordering_error on an
UNORDERED repository. Mode checks, insertion, and mode establishment occur under one mutex.
Failed insertion leaves contents, partition count, and mode unchanged. Draining never resets mode.
Both insertion APIs reject null batches with std::invalid_argument and accept the shared pointer
by const reference, preserving caller ownership even if a moved argument is rejected. Partition
growth can also throw std::length_error or std::bad_alloc; these are not mode/duplicate errors.
Positions may have gaps and arrive in any order. Duplicate resident positions within a partition
throw repository_duplicate_error; the same position may occur in different partitions.
Removing a batch frees its position for the same batch or another batch to be inserted again.
The same shared batch may occupy multiple positions without copying its payload. Empty or filtered-away
outputs do not require renumbering later positions. Positions identify sequence, independently of
batch IDs and the number of rows in a payload.
data_repository positioned;
positioned.add_data_batch_at_position(later_batch, 20);
positioned.add_data_batch_at_position(earlier_batch, 3);
// FIFO remains arrival order: returns later_batch with position 20.
auto [batch, position] = positioned.pop_next_data_batch_with_position();
// ID access preserves the position, too.
auto [found, found_position] =
positioned.get_data_batch_by_id_with_position(earlier_batch->get_batch_id());
auto [removed, removed_position] =
positioned.pop_data_batch_by_id_with_position(earlier_batch->get_batch_id());The three _with_position FIFO/ID retrieval methods work in every mode. They return {nullptr, nullopt}
on a miss, and {batch, nullopt} for an unpositioned batch. FIFO and ID retrieval do not sort data.
get_data_batch_by_position(position, partition_idx) and
pop_data_batch_by_position(position, partition_idx) return the payload pointer or nullptr
when absent. get_positions(partition_idx) returns a copy of the resident positions in
increasing order. These three operations return an empty result for an empty requested partition,
including in UNKNOWN mode. A nonempty partition requires ORDERED mode, otherwise
repository_ordering_error is thrown. Invalid partition indices still throw
std::out_of_range, checked before mode errors. Growing partitions never changes the batch mode.
Use of the API pop_next_data_batch_with_position() API as in the example above is intended to be used
when the retrieval and processing of batches does not care about the order, but the position information
should be forwarded so that another operator downstream which does care about order can use it.
For example a Project operator can process batches out of order, but should forward position information.
Repository insertion and retrieval do not synchronize CUDA work. Before publishing an
asynchronously produced GPU representation, the producer must call
record_writer_event(writer_stream) on that representation after enqueueing its writes.
A consumer on another stream or device must wait for the recorded event before accessing the
buffers, for example with cudaStreamWaitEvent. A null writer event on a GPU representation
means the writer is unknown: the reader must synchronize the owning device before reading,
as specified by idata_representation::get_writer_event(). Host or disk representations without
a GPU writer need no such ordering. Shared ownership and host-side batch accessor locks do not
replace this CUDA synchronization.
The producer asserts the logical meaning of positions and partition order. The repository checks
structural invariants; it does not validate sortedness or SQL comparators. Callers using ordered
range indices explicitly choose lexicographic (partition_idx, position) traversal, then row order.
A range partition can contain an unordered collection of runs that still needs merging.
Ordered consumers must first establish that all producers have successfully completed publication.
UNKNOWN, emptiness, and a missing position cannot distinguish delayed output from an absent or
filtered-away output. The repository provides no completion, cancellation, or watermark protocol.
An entirely empty completed input can remain UNKNOWN; position enumeration returns an empty list.
After successful completion, one coordinated consumer can enumerate and retrieve sparse positions:
// Caller has already established successful producer completion and exclusive drain ownership.
if (!repository.empty(partition_idx)) {
for (auto position : repository.get_positions(partition_idx)) {
auto batch = repository.pop_data_batch_by_position(position, partition_idx);
assert(batch && "An enumerated position must remain resident under exclusive drain ownership");
// Consume batch in this logical order.
}
}Enumeration is a snapshot, not an atomic claim of the batches. Concurrent removal can make later retrieval miss; ordered traversal therefore needs consumer coordination. Arrival-order acquisition and a mutex around final append do not establish logical execution order. Operators such as LIMIT must apply their state in logical input order at the execution boundary.
Use pop_next_data_batch(), pop_data_batch_by_id(), or get_data_batch_by_id() when the
position is not needed. These return a payload pointer (or nullptr) in every ordering mode.
Plain pops intentionally discard the removed batch's position; they do not change the repository's
ordering mode or rearrange remaining entries.
Use the corresponding _with_position() variant when the consumer needs to preserve the
position. These return positioned_data_batch, or {nullptr, std::nullopt} on a miss. FIFO
retrieval follows arrival order with either API; it does not sort by position.
Public methods are non-virtual and acquire the repository mutex before calling protected virtual
_unlocked implementation hooks. Payload-only and position-aware wrappers share the same hooks.
Overrides must call the base _unlocked helper while the lock is held, rather than a public method
that would acquire the mutex again. Subclasses must preserve the mode and storage-index invariants.
get_positions() enumerates sorted positions.
The snapshot is only complete after all producers finish, and consumers must coordinate removal.
| Type | Compatibility Alias | Use Case |
|---|---|---|
data_repository |
shared_data_repository |
Same batch shared across multiple repositories (fan-out) |
Repositories use shared_ptr<data_batch> because batches are created by data_batch::make() and
the accessor transition API uses shared_from_this().
File: include/cucascade/data/data_repository_manager.hpp
Repositories are indexed by (operator_id, port_id) pairs:
// Add repositories for different operators
manager.add_new_repository(0, "output", std::make_unique<data_repository>());
manager.add_new_repository(1, "input", std::make_unique<data_repository>());
manager.add_new_repository(1, "output", std::make_unique<data_repository>());
// Access a specific repository
auto& repo = manager.get_repository(1, "input");The manager provides globally unique, monotonically increasing batch IDs:
uint64_t id = manager.get_next_data_batch_id(); // atomic incrementThe manager registers and exposes repositories; it does not publish batches or coordinate fan-out. Publish directly through each destination repository, using the appropriate insertion API:
auto& repository = *manager.get_repository(0, "output");
repository.add_data_batch(shared_batch);
// For a separate destination whose batches carry logical positions:
auto& positioned_repository = *manager.get_repository(1, "input");
positioned_repository.add_data_batch_at_position(shared_batch, position);A caller sending a batch to several repositories performs separate publications and owns any coordination or failure policy needed across them. No atomic multi-repository publication is provided by the manager.
Memory management discovers the same payloads through for_each_repository() or
get_repositories(), followed by each repository's num_partitions(), get_batch_ids(), and
non-removing get_data_batch_by_id(). These work for positioned repositories without consuming
positions. Changing a resident batch's representation during spilling/restoration leaves its
repository partition and position intact. If a caller removes and republishes a batch, it must
carry and restore its coordinates explicitly; a new clone ID does not establish a new position.
Both manager lookup APIs return borrowed access, not ownership of a repository:
get_repository()returns a reference to theunique_ptrin the manager's map without taking the manager mutex. Callers must externally coordinate lookup with repository registration and cleanup, and keep the manager and the referenced repository alive throughout their use.get_repositories()collects a snapshot of raw pointers under the manager mutex. The lock protects snapshot collection, but does not keep those repositories alive after the call returns. Callers must prevent cleanup or manager destruction until all uses of the pointers have ended.
A repository's internal mutex protects its contents; it does not protect the lifetime of the repository object. The embedding application must provide the lifecycle coordination described above. Neither lookup API by itself makes concurrent lookup and cleanup safe for subsequent use.
The data and memory modules are connected at several points:
-
Memory spaces -- each
idata_representationholds a reference to itsmemory_space, which provides tier, device ID, and allocator access -
Stream acquisition -- data operations use
memory_space.acquire_stream()for CUDA async operations -
Reservation tracking -- when data is converted between tiers, the converter allocates in the target memory space using its allocator and reservation system
-
Downgrade coordination -- the application queries
memory_space.should_downgrade_memory()and enumerates manager-visible repositories and batch IDs to find candidates -
Processing validation --
try_to_lock_for_processing()checks that the requestedmemory_space_idmatches the batch's current location
Application
|
|-- memory_reservation_manager.request_reservation(strategy, size)
| |-- memory_space.make_reservation(size)
|
|-- data_repository_manager.get_repository(operator_id, port_id)
| |-- repository.add_data_batch(batch) [batch is idle]
|
|-- data_repository.pop_next_data_batch(partition_idx)
| |-- returns shared_ptr<data_batch> (idle, non-blocking)
|
|-- batch->to_read_only() [shared lock]
| |-- returns read_only_data_batch (blocks until available)
| |-- ro.get_data() grants read access to idata_representation
|
|-- [on memory pressure]
| mutable_data_batch mut = data_batch::readonly_to_mutable(std::move(ro))
| mut.convert_to<TargetRepresentation>(registry, target_space, stream)
| |-- converter_registry.convert(data, target_space, stream)
| |-- allocates in target memory_space
| |-- cudaMemcpy between tiers
| data_batch::to_idle(std::move(mut)) [release exclusive lock]
| File | Purpose |
|---|---|
include/cucascade/data/common.hpp |
idata_representation abstract interface |
include/cucascade/data/data_batch.hpp |
data_batch, read_only_data_batch, mutable_data_batch, batch_state |
include/cucascade/data/data_repository.hpp |
data_repository |
include/cucascade/data/data_repository_manager.hpp |
data_repository_manager, operator_port_key |
include/cucascade/data/representation_converter.hpp |
representation_converter_registry, converter_key |
include/cucascade/data/gpu_data_representation.hpp |
gpu_table_representation wrapping cudf::table |
include/cucascade/data/cpu_data_representation.hpp |
host_data_representation (direct buffer copy) and host_data_packed_representation (cudf::pack) |
include/cucascade/memory/host_table.hpp |
host_table_allocation and column_metadata for direct-copy host representations |
include/cucascade/memory/host_table_packed.hpp |
host_table_packed_allocation for packed (cudf::pack) host representations |
include/cucascade/utils/atomics.hpp |
atomic_peak_tracker, atomic_bounded_counter |
include/cucascade/utils/overloaded.hpp |
Variant visitor helper |
docs/data_batch_state_transitions.md |
Complete state machine reference |