Skip to content

Commit a215f77

Browse files
committed
fix(issue-2394): key arena per-stream state by CUDA stream ID
The arena memory resource keyed per-stream arenas by the raw cudaStream_t handle. CUDA can hand a destroyed stream's handle value to a new stream (ABA reuse), so a new logical stream could inherit stale arena state. Key stream_arenas_ by cudaStreamGetId instead, mirroring the pattern already used by stream_ordered_memory_resource. Closes #2394 Signed-off-by: nethum529 <nethumweerasinghe.nw@gmail.com>
1 parent 547286a commit a215f77

3 files changed

Lines changed: 117 additions & 11 deletions

File tree

cpp/include/rmm/mr/detail/arena_memory_resource_impl.hpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ namespace RMM_NAMESPACE {
2222
namespace mr {
2323
namespace detail {
2424

25+
/// The type used to key per-stream arenas by CUDA stream ID.
26+
using stream_id_type = unsigned long long;
27+
28+
/**
29+
* @brief Return the CUDA stream ID used to key per-stream arenas.
30+
*
31+
* Normalizes the default stream to `cudaStreamLegacy` for consistency between
32+
* PTDS and non-PTDS mode, then returns the stream's ID from `cudaStreamGetId`.
33+
* Keying by ID (rather than the raw `cudaStream_t` handle) prevents a newly
34+
* created stream that reuses a destroyed stream's handle value from inheriting
35+
* stale arena state (ABA reuse).
36+
*
37+
* @param stream The stream for which to get the ID.
38+
* @param may_throw If `true`, a `cudaStreamGetId` failure throws `rmm::cuda_error` (only safe to
39+
* pass from a context that is not `noexcept`, e.g. the `allocate` path). If `false`, failure is
40+
* only asserted in debug builds and silently ignored in release builds, matching the
41+
* `noexcept`-safe requirements of the `deallocate`/`deallocate_sync` path.
42+
* @return The CUDA stream ID.
43+
*/
44+
stream_id_type get_stream_id(cuda_stream_view stream, bool may_throw);
45+
2546
/**
2647
* @brief Implementation class for arena_memory_resource.
2748
*
@@ -81,17 +102,17 @@ class arena_memory_resource_impl {
81102

82103
void deallocate_from_other_arena(cuda_stream_view stream, void* ptr, std::size_t bytes);
83104

84-
arena& get_arena(cuda_stream_view stream);
105+
arena& get_arena(cuda_stream_view stream, bool may_throw);
85106
arena& get_thread_arena();
86-
arena& get_stream_arena(cuda_stream_view stream);
107+
arena& get_stream_arena(cuda_stream_view stream, bool may_throw);
87108

88109
void dump_memory_log(std::size_t bytes);
89110

90111
static bool use_per_thread_arena(cuda_stream_view stream);
91112

92113
global_arena global_arena_;
93114
std::map<std::thread::id, std::shared_ptr<arena>> thread_arenas_;
94-
std::map<cudaStream_t, arena> stream_arenas_;
115+
std::map<stream_id_type, arena> stream_arenas_;
95116
bool dump_log_on_failure_{};
96117
std::shared_ptr<rapids_logger::logger> logger_{};
97118
mutable std::shared_mutex map_mtx_;

cpp/src/mr/detail/arena_memory_resource_impl.cpp

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ namespace RMM_NAMESPACE {
1515
namespace mr {
1616
namespace detail {
1717

18+
stream_id_type get_stream_id(cuda_stream_view stream, bool may_throw)
19+
{
20+
// We use cudaStreamLegacy as the arena map key for the default stream for consistency between
21+
// PTDS and non-PTDS mode. Keying by the stream ID (rather than the raw cudaStream_t handle)
22+
// prevents a newly created stream that reuses a destroyed stream's handle value from inheriting
23+
// stale arena state (ABA reuse).
24+
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
25+
auto* const stream_to_store = stream.is_default() ? cudaStreamLegacy : stream.value();
26+
stream_id_type stream_id{};
27+
if (may_throw) {
28+
// Safe to throw: only called from the non-noexcept allocate() path.
29+
RMM_CUDA_TRY(cudaStreamGetId(stream_to_store, &stream_id));
30+
} else {
31+
// Called from the noexcept deallocate()/deallocate_sync() path: must not throw. A failure
32+
// here is only surfaced via assertion in debug builds.
33+
RMM_ASSERT_CUDA_SUCCESS(cudaStreamGetId(stream_to_store, &stream_id));
34+
}
35+
return stream_id;
36+
}
37+
1838
arena_memory_resource_impl::arena_memory_resource_impl(
1939
cuda::mr::any_resource<cuda::mr::device_accessible> upstream_mr,
2040
std::optional<std::size_t> arena_size,
@@ -39,7 +59,7 @@ void* arena_memory_resource_impl::allocate(cuda::stream_ref stream,
3959
#else
4060
bytes = rmm::align_up(bytes, rmm::CUDA_ALLOCATION_ALIGNMENT);
4161
#endif
42-
auto& arena = get_arena(sv);
62+
auto& arena = get_arena(sv, /*may_throw=*/true);
4363

4464
{
4565
std::shared_lock lock(mtx_);
@@ -73,7 +93,7 @@ void arena_memory_resource_impl::deallocate(cuda::stream_ref stream,
7393
#else
7494
bytes = rmm::align_up(bytes, rmm::CUDA_ALLOCATION_ALIGNMENT);
7595
#endif
76-
auto& arena = get_arena(sv);
96+
auto& arena = get_arena(sv, /*may_throw=*/false);
7797

7898
{
7999
std::shared_lock lock(mtx_);
@@ -138,10 +158,11 @@ void arena_memory_resource_impl::deallocate_from_other_arena(cuda_stream_view st
138158
}
139159
}
140160

141-
arena_memory_resource_impl::arena& arena_memory_resource_impl::get_arena(cuda_stream_view stream)
161+
arena_memory_resource_impl::arena& arena_memory_resource_impl::get_arena(cuda_stream_view stream,
162+
bool may_throw)
142163
{
143164
if (use_per_thread_arena(stream)) { return get_thread_arena(); }
144-
return get_stream_arena(stream);
165+
return get_stream_arena(stream, may_throw);
145166
}
146167

147168
arena_memory_resource_impl::arena& arena_memory_resource_impl::get_thread_arena()
@@ -162,18 +183,19 @@ arena_memory_resource_impl::arena& arena_memory_resource_impl::get_thread_arena(
162183
}
163184

164185
arena_memory_resource_impl::arena& arena_memory_resource_impl::get_stream_arena(
165-
cuda_stream_view stream)
186+
cuda_stream_view stream, bool may_throw)
166187
{
167188
RMM_LOGGING_ASSERT(!use_per_thread_arena(stream));
189+
auto const stream_id = get_stream_id(stream, may_throw);
168190
{
169191
std::shared_lock lock(map_mtx_);
170-
auto const iter = stream_arenas_.find(stream.value());
192+
auto const iter = stream_arenas_.find(stream_id);
171193
if (iter != stream_arenas_.end()) { return iter->second; }
172194
}
173195
{
174196
std::unique_lock lock(map_mtx_);
175-
stream_arenas_.emplace(stream.value(), global_arena_);
176-
return stream_arenas_.at(stream.value());
197+
stream_arenas_.emplace(stream_id, global_arena_);
198+
return stream_arenas_.at(stream_id);
177199
}
178200
}
179201

cpp/tests/mr/arena_mr_tests.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,5 +637,68 @@ TEST_F(ArenaTest, DumpLogOnFailure) // NOLINT
637637
EXPECT_GE(file_status.st_size, 0);
638638
}
639639

640+
// Distinct live streams get distinct IDs, and a stream's ID is stable across calls.
641+
TEST(ArenaStreamIdTest, DistinctStreamsHaveDistinctStableIds) // NOLINT
642+
{
643+
rmm::cuda_stream stream_a{};
644+
rmm::cuda_stream stream_b{};
645+
646+
EXPECT_NE(rmm::mr::detail::get_stream_id(stream_a.view(), /*may_throw=*/true),
647+
rmm::mr::detail::get_stream_id(stream_b.view(), /*may_throw=*/true));
648+
EXPECT_EQ(rmm::mr::detail::get_stream_id(stream_a.view(), /*may_throw=*/true),
649+
rmm::mr::detail::get_stream_id(stream_a.view(), /*may_throw=*/true));
650+
}
651+
652+
// The default stream is normalized via cudaStreamLegacy, so it maps to one consistent key.
653+
TEST(ArenaStreamIdTest, DefaultStreamNormalizesToLegacyId) // NOLINT
654+
{
655+
auto const id = rmm::mr::detail::get_stream_id(rmm::cuda_stream_view{}, /*may_throw=*/true);
656+
EXPECT_EQ(id, rmm::mr::detail::get_stream_id(rmm::cuda_stream_view{}, /*may_throw=*/true));
657+
658+
#ifndef CUDA_API_PER_THREAD_DEFAULT_STREAM
659+
// Non-PTDS: the default stream normalizes to cudaStreamLegacy, so it keys to the legacy id.
660+
EXPECT_EQ(rmm::mr::detail::get_stream_id(rmm::cuda_stream_view{}, /*may_throw=*/true),
661+
rmm::mr::detail::get_stream_id(rmm::cuda_stream_legacy, /*may_throw=*/true));
662+
#endif
663+
664+
// In both modes, a real created stream must never collide with the legacy key. This catches a
665+
// regression that collapses every stream to a single key.
666+
rmm::cuda_stream stream{};
667+
EXPECT_NE(rmm::mr::detail::get_stream_id(stream.view(), /*may_throw=*/true),
668+
rmm::mr::detail::get_stream_id(rmm::cuda_stream_legacy, /*may_throw=*/true));
669+
}
670+
671+
// Regression for #2394: CUDA can hand a destroyed stream's raw handle value to a new stream (ABA
672+
// reuse). Keying arenas by the raw handle would let the new stream inherit stale arena state.
673+
// Keying by cudaStreamGetId prevents this: a reused handle yields a distinct ID.
674+
//
675+
// This test cannot force reuse deterministically because CUDA does not guarantee handle-reuse
676+
// timing (see the issue's own caveat), so it probes up to a bounded number of iterations and
677+
// skips if reuse is never observed. It either proves the property or skips, never falsely fails.
678+
TEST(ArenaStreamIdTest, ReusedRawHandleYieldsDistinctId) // NOLINT
679+
{
680+
cudaStream_t raw{};
681+
rmm::mr::detail::stream_id_type id{};
682+
{
683+
rmm::cuda_stream original{};
684+
raw = original.view().value();
685+
id = rmm::mr::detail::get_stream_id(original.view(), /*may_throw=*/true);
686+
} // original destroyed here, freeing its raw handle for potential reuse
687+
688+
constexpr int max_iterations = 2048;
689+
bool observed_reuse = false;
690+
for (int i = 0; i < max_iterations; ++i) {
691+
rmm::cuda_stream candidate{};
692+
if (candidate.view().value() == raw) {
693+
// Same raw handle, but the ID must differ so no stale arena is inherited.
694+
EXPECT_NE(rmm::mr::detail::get_stream_id(candidate.view(), /*may_throw=*/true), id);
695+
observed_reuse = true;
696+
break;
697+
}
698+
}
699+
700+
if (!observed_reuse) { GTEST_SKIP() << "CUDA did not reuse the stream handle within the bound"; }
701+
}
702+
640703
} // namespace
641704
} // namespace rmm::test

0 commit comments

Comments
 (0)