From d45020b58969a2bbb7fc82a13c8ca7d770600ae5 Mon Sep 17 00:00:00 2001 From: Felipe Aramburu Date: Fri, 31 Jul 2026 06:32:39 +0000 Subject: [PATCH 1/2] chore(cudf): NVTX phase ranges in convert_host_fast_to_gpu Named ranges (hg:convert / presync / acquire_stream / reconstruct / dev_alloc / nullmask_sync / offsets_cast / flush_submit / final_sync) around each phase of the host->GPU conversion, plus an RAII nvtx_scope helper. Zero-cost without a profiler attached; with nsys these produce a per-phase breakdown of conversion wall time (they are how the null-mask and batch-submission overheads in the follow-up commit were found). Co-Authored-By: Claude Fable 5 --- .../representation_converter_builtins.cpp | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/cudf/representation_converter_builtins.cpp b/src/cudf/representation_converter_builtins.cpp index aab773b..67858c4 100644 --- a/src/cudf/representation_converter_builtins.cpp +++ b/src/cudf/representation_converter_builtins.cpp @@ -50,6 +50,7 @@ #include #include +#include #include #include @@ -74,6 +75,14 @@ namespace cucascade { namespace { +// RAII NVTX range for profiling converter phases (thread-local push/pop stack). +struct nvtx_scope { + explicit nvtx_scope(const char* name) { nvtxRangePushA(name); } + ~nvtx_scope() { nvtxRangePop(); } + nvtx_scope(const nvtx_scope&) = delete; + nvtx_scope& operator=(const nvtx_scope&) = delete; +}; + // memory::column_metadata::type_id is a generic int32_t type tag; the cudf layer interprets it // as the numeric value of a cudf::type_id. These helpers convert across that boundary. inline cudf::type_id as_cudf_type_id(int32_t type_id) @@ -962,7 +971,9 @@ static rmm::device_buffer alloc_and_schedule_h2d( rmm::device_async_resource_ref mr, BatchCopyAccumulator& batch) { + nvtxRangePushA("hg:dev_alloc"); rmm::device_buffer buf(size, stream, mr); + nvtxRangePop(); if (size == 0) { return buf; } if (alloc.size() == 0) { throw std::invalid_argument( @@ -1005,7 +1016,10 @@ static rmm::device_buffer alloc_and_copy_h2d_sync( rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { + nvtx_scope range{"hg:nullmask_sync"}; + nvtxRangePushA("hg:dev_alloc"); rmm::device_buffer buf(size, stream, mr); + nvtxRangePop(); if (size == 0) { return buf; } const std::size_t block_size = alloc.block_size(); @@ -1075,6 +1089,7 @@ static std::unique_ptr reconstruct_column( // Flush pending H2D copies so the INT32 offsets buffer has valid data on device // before the cast reads from it. The cast replaces offsets_col, freeing the old // INT32 buffer, so batch must not hold dangling pointers to it. + nvtx_scope cast_range{"hg:offsets_cast"}; batch.flush(stream, cudaMemcpySrcAccessOrderDuringApiCall); offsets_col = cudf::cast(offsets_col->view(), cudf::data_type{cudf::type_id::INT64}, stream, mr); @@ -1167,6 +1182,7 @@ std::unique_ptr convert_host_fast_to_gpu( rmm::cuda_stream_view stream, [[maybe_unused]] memory::reservation* reservation) { + nvtx_scope convert_range{"hg:convert"}; auto& fast_source = source.cast(); const auto& fast_table = fast_source.get_host_table(); if (!fast_table) { throw std::runtime_error("convert_host_fast_to_gpu: host table is null"); } @@ -1178,7 +1194,9 @@ std::unique_ptr convert_host_fast_to_gpu( // host_data_representation is flushed before we read the pinned host blocks. // The caller's stream may be bound to a non-target device under multi-GPU; // synchronize is safe across devices. + nvtxRangePushA("hg:presync"); stream.synchronize(); + nvtxRangePop(); rmm::cuda_set_device_raii device_guard{rmm::cuda_device_id{target_memory_space->get_device_id()}}; @@ -1186,22 +1204,30 @@ std::unique_ptr convert_host_fast_to_gpu( // Using the caller's stream for the H2D batch under a target-device RAII // guard raises cudaErrorInvalidValue when stream and current device belong // to different CUDA contexts (multi-GPU case). + nvtxRangePushA("hg:acquire_stream"); auto target_stream = target_memory_space->acquire_stream(); auto mr = target_memory_space->get_default_allocator(); + nvtxRangePop(); // Collect all H→D copy ops across all columns, then fire one batched call. BatchCopyAccumulator batch; std::vector> gpu_columns; gpu_columns.reserve(fast_table->columns.size()); + nvtxRangePushA("hg:reconstruct"); for (const auto& col_meta : fast_table->columns) { gpu_columns.push_back( reconstruct_column(col_meta, *fast_table->allocation, target_stream, mr, batch)); } + nvtxRangePop(); // Source is CPU-written pinned host memory: fully prepared before this call. + nvtxRangePushA("hg:flush_submit"); batch.flush(target_stream, cudaMemcpySrcAccessOrderDuringApiCall); + nvtxRangePop(); auto new_table = std::make_unique(std::move(gpu_columns)); + nvtxRangePushA("hg:final_sync"); target_stream.synchronize(); + nvtxRangePop(); // STREAM-LINEAGE: writes happened on target_stream; record event so // cross-stream readers observe ordering. From 3fbe5c81f9c47f6393d706f4e02c0f546bdc5bca Mon Sep 17 00:00:00 2001 From: Felipe Aramburu Date: Fri, 31 Jul 2026 06:32:39 +0000 Subject: [PATCH 2/2] perf(cudf): skip uploading all-valid null masks in host->GPU reconstruction Null masks are copied synchronously per column (cudf column factories read the mask at construction, so it cannot ride the async copy batch). For an all-valid column (null_count == 0) the mask is semantically identical to no mask: skip the upload and construct the column non-nullable. On TPC-H SF1000 host-pinned (GB300), where every column is all-valid, this removes ~975 blocking memcpy+stream-sync round-trips and 5.2 GB of mask bytes per q9 iteration; NVTX phase profiling attributed ~10.4 ms of each 39 ms five-GB conversion to these syncs. q9 hot -3.5%, whole-suite ~-2-3%, results byte-identical. Semantics note: reconstructed all-valid columns are now non-nullable rather than nullable-with-full-validity. cudf treats these as equivalent; strict column-equality assertions that compare nullability flags may need the alternative approach (batching masks into the existing flush for constructors that do not read mask contents). Co-Authored-By: Claude Fable 5 --- src/cudf/representation_converter_builtins.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cudf/representation_converter_builtins.cpp b/src/cudf/representation_converter_builtins.cpp index 67858c4..17e4f67 100644 --- a/src/cudf/representation_converter_builtins.cpp +++ b/src/cudf/representation_converter_builtins.cpp @@ -1065,9 +1065,12 @@ static std::unique_ptr reconstruct_column( rmm::device_async_resource_ref mr, BatchCopyAccumulator& batch) { - // Null mask — copied synchronously because cudf factories access it on construction + // Null mask — copied synchronously because cudf factories access it on construction. + // An all-valid mask (null_count == 0) is semantically identical to no mask: skip its + // upload entirely and construct the column non-nullable, avoiding a per-column + // blocking memcpy + stream sync. rmm::device_buffer null_mask{}; - if (meta.has_null_mask) { + if (meta.has_null_mask && meta.null_count > 0) { null_mask = alloc_and_copy_h2d_sync(alloc, meta.null_mask_offset, meta.null_mask_size, stream, mr); }