Skip to content

[MINOR] Reject invalid CUDA streams in buffer factories - #1263

Merged
rapids-bot[bot] merged 6 commits into
rapidsai:mainfrom
nirandaperera:minor-invalid-stream-guards
Oct 19, 2026
Merged

rapids-bot[bot] merged 6 commits into
rapidsai:mainfrom
nirandaperera:minor-invalid-stream-guards

Conversation

@nirandaperera

Copy link
Copy Markdown
Contributor

Passing cuda::invalid_stream to a buffer factory used to succeed and only fail later, as cudaErrorInvalidResourceHandle from a CUDA call far from the cause. The public HostBuffer::from_rmm_device_buffer also skipped the stream ordering that BufferResource::move does. The buffer factories and Buffer::rebind_stream now throw std::invalid_argument on an invalid stream. from_rmm_device_buffer now orders its stream after the device buffer's pending work. Follow-up to #1261.

  • BufferResource::make_buffer, BufferResource::move, the HostBuffer constructors (used by from_uint8_vector and from_owned_vector), HostBuffer::from_rmm_device_buffer and Buffer::rebind_stream throw std::invalid_argument for cuda::invalid_stream.
  • HostBuffer::from_rmm_device_buffer makes stream wait on the device buffer's stream, and frees the memory on the HostBuffer's current stream. Before, memory was freed on the original stream, which could be stale after rebind_stream.
  • The stream join in BufferResource::move and from_rmm_device_buffer now just checks capacity() > 0. An empty buffer owns no memory, so there's nothing to join. This includes default-constructed buffers, which have no stream.
  • Added tests that each entry point throws on an invalid stream, and updated the docstrings.

@nirandaperera
nirandaperera requested a review from a team as a code owner October 15, 2026 23:09
@nirandaperera nirandaperera added breaking Introduces a breaking change improvement Improves an existing functionality labels Oct 15, 2026
@nirandaperera nirandaperera changed the title Reject invalid CUDA streams in buffer factories [MINOR] Reject invalid CUDA streams in buffer factories Oct 15, 2026
Comment on lines +179 to +182
// Deallocate on the HostBuffer's current stream, which `set_stream()` may
// have changed since construction.
[shared_db_ = std::move(shared_db)](cuda::stream_ref s) mutable {
shared_db_->set_stream(s);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the from_rmm_device_buffer deallocator now frees memory on the HostBuffer's current stream, HostBuffer::set_stream(cuda::invalid_stream) leads to freeing on an invalid stream inside the noexcept destructor. I think HostBuffer::set_stream should get the same std::invalid_argument.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

EXPECT_NO_THROW(test_buffer(std::move(buffer), source_data));
}

TEST(HostBuffer, invalid_stream_throws) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add an invalid-stream EXPECT_THROW for HostBuffer::from_rmm_device_buffer and HostBuffer::from_external? It would also help to test the new from_rmm_device_buffer behavior, build a pinned rmm::device_buffer on stream A, call from_rmm_device_buffer(..., B), check the contents are read correctly on B, and destroy it after set_stream(C). Right now nothing covers the new join or the free on the current stream.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the checks. Two things from the earlier comment are still open:

  1. an EXPECT_THROW for HostBuffer::from_external(span, invalid, ...)
  2. a test of the new ordering and freeing. For example: fill a pinned rmm::device_buffer on stream A, call from_rmm_device_buffer(..., B), check the contents on B, then set_stream(C) and destroy it. If this is out of scope for this PR, that's fine, but please say so in the PR description, because it currently says each entry point is tested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 1 is already there. I will add a test to 2. That is a good point. It was a bug uncovered during this invalid stream changes

@pentschev pentschev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more test asks but otherwise LGTM. I'm preemptively approving.

Comment thread cpp/tests/test_host_buffer.cpp Outdated
Comment on lines +283 to +284
auto br =
rapidsmpf::BufferResource::create(rmm::mr::get_current_device_resource_ref());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

br here is created without PinnedPoolProperties, so br->pinned_mr() throws std::invalid_argument while the argument is being built. EXPECT_THROW then passes without ever calling from_rmm_device_buffer. Could you create a separate resource with pinned memory turned on, e.g. BufferResource::create(rmm::mr::get_current_device_resource_ref(), rapidsmpf::PinnedPoolProperties{})? Then allocate the rmm::device_buffer outside the EXPECT_THROW, so only the from_rmm_device_buffer call is inside it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks. This was a good catch. this check was masked by the invalid arg exception from pinned mr, rather than the invalid stream

EXPECT_NO_THROW(test_buffer(std::move(buffer), source_data));
}

TEST(HostBuffer, invalid_stream_throws) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the checks. Two things from the earlier comment are still open:

  1. an EXPECT_THROW for HostBuffer::from_external(span, invalid, ...)
  2. a test of the new ordering and freeing. For example: fill a pinned rmm::device_buffer on stream A, call from_rmm_device_buffer(..., B), check the contents on B, then set_stream(C) and destroy it. If this is out of scope for this PR, that's fine, but please say so in the PR description, because it currently says each entry point is tested.

@nirandaperera

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit b4e60fd into rapidsai:main Oct 19, 2026
113 of 115 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Introduces a breaking change improvement Improves an existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants