Skip to content

Commit 76d938b

Browse files
committed
Define empty segments for segmented_offset_bitmask_binop via an identity seed
Seeding each segment's reduction with the identity of the binary operator, instead of loading the segment's first mask, gives an empty segment a well defined result (an all-valid destination mask, null count zero) and removes the read of sources[segment_start] that a segment with no masks would otherwise perform out of bounds. Also validate that segment offsets are non-decreasing and within the mask array, and stop the bitmask benchmark's segment size generator from drawing negative sizes, which produced non-monotonic offsets and an illegal memory access for a small expected_masks_per_segment.
1 parent f59b18d commit 76d938b

3 files changed

Lines changed: 115 additions & 22 deletions

File tree

‎cpp/benchmarks/bitmask/bitmask_and.cpp‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -12,6 +12,8 @@
1212

1313
#include <nvbench/nvbench.cuh>
1414

15+
#include <algorithm>
16+
#include <numeric>
1517
#include <random>
1618

1719
namespace {
@@ -34,7 +36,9 @@ auto setup_masks(nvbench::state& state)
3436
std::vector<cudf::size_type> segments(num_segments + 1);
3537
auto num_masks = 0;
3638
std::generate_n(segments.begin(), num_segments, [&normal_dist, &generator, &num_masks]() {
37-
cudf::size_type segment_size = normal_dist(generator);
39+
// The distribution is unbounded, so a small mean draws negative sizes; those would make the
40+
// offsets below non-monotonic. Empty segments are valid input and are kept.
41+
auto const segment_size = std::max(cudf::size_type{0}, cudf::size_type(normal_dist(generator)));
3842
num_masks += segment_size;
3943
return segment_size;
4044
});

‎cpp/include/cudf/detail/null_mask.cuh‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ CUDF_KERNEL void offset_bitmask_binop(Binop op,
130130
* @tparam Binop Type of binary operator
131131
*
132132
* @param op The binary operator to apply to the bitmasks
133+
* @param identity Identity element of `op`, used to seed each segment's reduction; a
134+
* segment containing no masks therefore produces a destination filled with `identity`
133135
* @param num_segments Number of segments to process
134136
* @param destinations Array of pointers to destination bitmasks where results will be written
135137
* @param destination_size Size of each destination mask in bitmask words (not bits)
@@ -142,6 +144,7 @@ CUDF_KERNEL void offset_bitmask_binop(Binop op,
142144
*/
143145
template <typename Binop>
144146
CUDF_KERNEL void segmented_offset_bitmask_binop(Binop op,
147+
bitmask_type identity,
145148
size_type num_segments,
146149
bitmask_type** const destinations,
147150
size_type destination_size,
@@ -162,14 +165,15 @@ CUDF_KERNEL void segmented_offset_bitmask_binop(Binop op,
162165
auto const lane = warp.thread_rank();
163166

164167
// Process one segment per warp.
165-
auto const segment_id = cudf::detail::grid_1d::global_thread_id() / warp.size();
168+
auto const segment_id = cudf::detail::grid_1d::global_thread_id() / warp.size();
169+
170+
// Exit early if this warp doesn't have a valid segment, before indexing anything by `segment_id`
171+
if (segment_id >= num_segments) { return; }
172+
166173
auto const segment_start = segment_offsets[segment_id];
167174
auto const segment_end = segment_offsets[segment_id + 1];
168175
auto const destination = destinations[segment_id];
169176

170-
// Exit early if this warp doesn't have a valid segment
171-
if (segment_id >= num_segments) { return; }
172-
173177
// Calculate bit range information
174178
auto const last_bit_index = source_size_bits - 1;
175179
auto const last_word_index = cudf::word_index(last_bit_index);
@@ -181,15 +185,12 @@ CUDF_KERNEL void segmented_offset_bitmask_binop(Binop op,
181185
// Process the mask such that each thread in warp handles different words
182186
for (size_type destination_word_index = lane; destination_word_index < destination_size;
183187
destination_word_index += warp.size()) {
184-
// Get the first mask word
185-
bitmask_type destination_word =
186-
detail::get_mask_offset_word(sources[segment_start],
187-
destination_word_index,
188-
source_begin_bits[segment_start],
189-
source_begin_bits[segment_start] + source_size_bits);
188+
// Seeding with the identity keeps an empty segment well defined and avoids indexing `sources`
189+
// with a segment's start offset before knowing that the segment contains anything.
190+
bitmask_type destination_word = identity;
190191

191192
// Apply the binary operation with each source mask in the segment
192-
for (size_type mask_pos = segment_start + 1; mask_pos < segment_end; mask_pos++) {
193+
for (size_type mask_pos = segment_start; mask_pos < segment_end; mask_pos++) {
193194
destination_word =
194195
op(destination_word,
195196
detail::get_mask_offset_word(sources[mask_pos],
@@ -242,7 +243,8 @@ rmm::device_uvector<size_type> inplace_segmented_bitmask_binop(
242243
size_type mask_size_bits,
243244
host_span<size_type const> segment_offsets,
244245
cuda::stream_ref stream,
245-
rmm::device_async_resource_ref mr);
246+
rmm::device_async_resource_ref mr,
247+
bitmask_type identity = ~bitmask_type{0});
246248

247249
/**
248250
* @copydoc bitmask_binop(Binop op, host_span<bitmask_type const* const>, host_span<size_type>
@@ -280,7 +282,8 @@ segmented_bitmask_binop(Binop op,
280282
size_type mask_size_bits,
281283
host_span<size_type const> segment_offsets,
282284
cuda::stream_ref stream,
283-
rmm::device_async_resource_ref mr)
285+
rmm::device_async_resource_ref mr,
286+
bitmask_type identity = ~bitmask_type{0})
284287
{
285288
auto const num_bytes = bitmask_allocation_size_bytes(mask_size_bits);
286289
CUDF_EXPECTS(
@@ -314,7 +317,8 @@ segmented_bitmask_binop(Binop op,
314317
mask_size_bits,
315318
segment_offsets,
316319
stream,
317-
cudf::get_current_device_resource_ref());
320+
cudf::get_current_device_resource_ref(),
321+
identity);
318322

319323
return std::pair(std::move(h_destination_masks),
320324
cudf::detail::make_std_vector<size_type>(null_counts, stream));
@@ -376,9 +380,12 @@ size_type inplace_bitmask_binop(Binop op,
376380
* @param[in] masks Host span of pointers to source bitmasks to be operated on
377381
* @param[in] masks_begin_bits The bit offsets from which each source mask is to be operated on
378382
* @param[in] mask_size_bits The number of bits to be operated on in each mask
379-
* @param[in] segment_offsets Host span of offsets defining the segments for the operation
383+
* @param[in] segment_offsets Host span of offsets defining the segments for the operation; must be
384+
* non-decreasing. An empty segment writes `identity` to its destination mask
380385
* @param[in] stream CUDA stream used for device memory operations and kernel launches
381386
* @param[in] mr Device memory resource used to allocate output device vector of null counts
387+
* @param[in] identity Identity element of `op`, defaulted to the all-set mask that is the identity
388+
* of bitwise AND
382389
* @return A device vector containing the counts of unset bits in the destination mask corresponding
383390
* to each segment after the AND operation
384391
*/
@@ -392,7 +399,8 @@ rmm::device_uvector<size_type> inplace_segmented_bitmask_binop(
392399
size_type mask_size_bits,
393400
host_span<size_type const> segment_offsets,
394401
cuda::stream_ref stream,
395-
rmm::device_async_resource_ref mr)
402+
rmm::device_async_resource_ref mr,
403+
bitmask_type identity)
396404
{
397405
CUDF_EXPECTS(
398406
std::all_of(masks_begin_bits.begin(), masks_begin_bits.end(), [](auto b) { return b >= 0; }),
@@ -402,8 +410,14 @@ rmm::device_uvector<size_type> inplace_segmented_bitmask_binop(
402410
"Mask pointer cannot be null");
403411
CUDF_EXPECTS(segment_offsets.size() >= 2,
404412
"At least one segment needs to be passed for bitwise operations");
413+
CUDF_EXPECTS(std::is_sorted(segment_offsets.begin(), segment_offsets.end()),
414+
"Segment offsets must be non-decreasing");
415+
CUDF_EXPECTS(
416+
segment_offsets.front() >= 0 && segment_offsets.back() <= static_cast<size_type>(masks.size()),
417+
"Segment offsets are out of the bounds of the mask array");
405418

406-
rmm::device_uvector<size_type> d_null_counts(segment_offsets.size() - 1, stream, mr);
419+
auto const num_segments = static_cast<size_type>(segment_offsets.size() - 1);
420+
rmm::device_uvector<size_type> d_null_counts(num_segments, stream, mr);
407421
auto temp_mr = cudf::get_current_device_resource_ref();
408422
auto d_masks = cudf::detail::make_device_uvector_async(masks, stream, temp_mr);
409423
auto d_begin_bits = cudf::detail::make_device_uvector_async(masks_begin_bits, stream, temp_mr);
@@ -413,13 +427,13 @@ rmm::device_uvector<size_type> inplace_segmented_bitmask_binop(
413427
auto constexpr block_size = 256;
414428
auto constexpr warps_per_block =
415429
util::div_rounding_up_safe<int>(block_size, cudf::detail::warp_size);
416-
auto const num_blocks =
417-
util::div_rounding_up_safe<int>(segment_offsets.size() - 1, warps_per_block);
430+
auto const num_blocks = util::div_rounding_up_safe<int>(num_segments, warps_per_block);
418431
static_assert(block_size % cudf::detail::warp_size == 0,
419432
"For segmented bitmask operations, block size must be a multiple of warp size");
420433
segmented_offset_bitmask_binop<<<num_blocks, block_size, 0, stream.get()>>>(
421434
op,
422-
segment_offsets.size() - 1,
435+
identity,
436+
num_segments,
423437
dest_masks.data(),
424438
dest_mask_size,
425439
d_masks.data(),

‎cpp/tests/bitmask/bitmask_tests.cpp‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,81 @@ TEST_F(MergeBitmaskTest, TestSegmentedBitmaskAndMultipleSegments)
675675
}
676676
}
677677

678+
TEST_F(MergeBitmaskTest, TestSegmentedBitmaskAndEmptySegments)
679+
{
680+
cudf::test::fixed_width_column_wrapper<bool> const bools_col1({0, 1, 0, 1, 1}, {0, 1, 1, 1, 0});
681+
cudf::test::fixed_width_column_wrapper<bool> const bools_col2({0, 2, 1, 0, 255}, {1, 1, 0, 1, 0});
682+
683+
auto const num_rows = 5;
684+
std::vector<cudf::column_view> const colviews{bools_col1, bools_col2};
685+
686+
// An empty segment reduces no masks, so it yields the identity of bitwise AND: every row valid.
687+
auto const expect_all_valid = [&](void const* mask) {
688+
EXPECT_EQ(cudf::null_count(static_cast<cudf::bitmask_type const*>(mask), 0, num_rows), 0);
689+
};
690+
691+
// The reduction of both masks leaves rows 1 and 3 valid.
692+
auto const expect_anded = [&](void const* mask) {
693+
auto odd_indices = cudf::test::iterators::nulls_at_multiples_of(2);
694+
auto const odd =
695+
std::get<0>(cudf::test::detail::make_null_mask(odd_indices, odd_indices + num_rows));
696+
CUDF_TEST_EXPECT_EQUAL_BUFFERS(mask, odd.data(), cudf::num_bitmask_words(num_rows));
697+
};
698+
699+
// Leading, then trailing, then interior empty segment. The trailing case is the one that would
700+
// index the mask array past its end if a segment's first mask were read unconditionally.
701+
{
702+
std::vector<cudf::size_type> const segment_offsets{0, 0, 2};
703+
auto const [result_masks, result_null_count] =
704+
cudf::segmented_bitmask_and(colviews, segment_offsets);
705+
ASSERT_EQ(result_masks.size(), 2);
706+
EXPECT_EQ(result_null_count[0], 0);
707+
EXPECT_EQ(result_null_count[1], 3);
708+
expect_all_valid(result_masks[0]->data());
709+
expect_anded(result_masks[1]->data());
710+
}
711+
712+
{
713+
std::vector<cudf::size_type> const segment_offsets{0, 2, 2};
714+
auto const [result_masks, result_null_count] =
715+
cudf::segmented_bitmask_and(colviews, segment_offsets);
716+
ASSERT_EQ(result_masks.size(), 2);
717+
EXPECT_EQ(result_null_count[0], 3);
718+
EXPECT_EQ(result_null_count[1], 0);
719+
expect_anded(result_masks[0]->data());
720+
expect_all_valid(result_masks[1]->data());
721+
}
722+
723+
{
724+
std::vector<cudf::size_type> const segment_offsets{0, 1, 1, 2};
725+
auto const [result_masks, result_null_count] =
726+
cudf::segmented_bitmask_and(colviews, segment_offsets);
727+
ASSERT_EQ(result_masks.size(), 3);
728+
EXPECT_EQ(result_null_count[0], 2);
729+
EXPECT_EQ(result_null_count[1], 0);
730+
EXPECT_EQ(result_null_count[2], 2);
731+
CUDF_TEST_EXPECT_EQUAL_BUFFERS(result_masks[0]->data(),
732+
static_cast<cudf::column_view>(bools_col1).null_mask(),
733+
cudf::num_bitmask_words(num_rows));
734+
expect_all_valid(result_masks[1]->data());
735+
CUDF_TEST_EXPECT_EQUAL_BUFFERS(result_masks[2]->data(),
736+
static_cast<cudf::column_view>(bools_col2).null_mask(),
737+
cudf::num_bitmask_words(num_rows));
738+
}
739+
}
740+
741+
TEST_F(MergeBitmaskTest, TestSegmentedBitmaskAndRejectsDecreasingOffsets)
742+
{
743+
cudf::test::fixed_width_column_wrapper<bool> const bools_col1({0, 1, 0, 1, 1}, {0, 1, 1, 1, 0});
744+
cudf::test::fixed_width_column_wrapper<bool> const bools_col2({0, 2, 1, 0, 255}, {1, 1, 0, 1, 0});
745+
std::vector<cudf::column_view> const colviews{bools_col1, bools_col2};
746+
747+
EXPECT_THROW(cudf::segmented_bitmask_and(colviews, std::vector<cudf::size_type>{0, 2, 1}),
748+
cudf::logic_error);
749+
EXPECT_THROW(cudf::segmented_bitmask_and(colviews, std::vector<cudf::size_type>{0, 3}),
750+
cudf::logic_error);
751+
}
752+
678753
TEST_F(MergeBitmaskTest, TestBitmaskOr)
679754
{
680755
cudf::test::fixed_width_column_wrapper<bool> const bools_col1({0, 1, 0, 1, 1}, {1, 1, 0, 0, 1});

0 commit comments

Comments
 (0)