Skip to content

Commit 98081dc

Browse files
Conarnarfacebook-github-bot
authored andcommitted
Use caller CUDA stream for D2H and H2D copies (#20498)
Summary: CudaAllocator memory copies now support async copy on a caller-provided CUDA stream. When a caller stream is available (via `getCallerStream()`), `copy_host_to_device` and `copy_device_to_host` use `cudaMemcpyAsync`. When no caller stream is set, the synchronous `cudaMemcpy` path is used as before. Additionally: - Added null pointer and zero-byte validation — null `dst`/`src` return `Error::InvalidArgument` instead of aborting in `cudaMemcpy`, and zero-byte copies return `Error::Ok` early. - Assert single-GPU case (index 0 or -1) until multi-GPU stream validation is added. - Wired `//executorch/extension/cuda:caller_stream` dependency in TARGETS. - Added `test_cuda_allocator` with coverage for sync/async paths and error handling. Differential Revision: D109590531
1 parent f61d7c1 commit 98081dc

4 files changed

Lines changed: 261 additions & 6 deletions

File tree

backends/cuda/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ install(
243243
if(BUILD_TESTING)
244244
include(${EXECUTORCH_ROOT}/tools/cmake/Test.cmake)
245245

246+
et_cxx_test(
247+
test_cuda_allocator SOURCES runtime/test/test_cuda_allocator.cpp EXTRA_LIBS
248+
aoti_cuda_backend
249+
)
250+
target_compile_definitions(test_cuda_allocator PRIVATE CUDA_AVAILABLE=1)
251+
246252
et_cxx_test(
247253
test_cuda_mutable_state SOURCES runtime/test/test_cuda_mutable_state.cpp
248254
EXTRA_LIBS aoti_cuda_backend

backends/cuda/runtime/TARGETS

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ runtime.cxx_library(
9292
"//executorch/runtime/core:device_allocator",
9393
],
9494
deps = [
95+
"//executorch/extension/cuda:caller_stream",
9596
"//executorch/runtime/platform:platform",
9697
],
9798
nvcc_flags = get_nvcc_arch_args() + [
@@ -163,3 +164,20 @@ cpp_unittest(
163164
platform = "gpu-remote-execution",
164165
),
165166
)
167+
168+
cpp_unittest(
169+
name = "test_cuda_allocator",
170+
srcs = ["test/test_cuda_allocator.cpp"],
171+
deps = [
172+
":cuda_allocator",
173+
"//executorch/extension/cuda:caller_stream",
174+
"//executorch/runtime/core:core",
175+
"//executorch/runtime/platform:platform",
176+
],
177+
external_deps = [("cuda", None, "cuda-lazy")],
178+
preprocessor_flags = ["-DCUDA_AVAILABLE=1"],
179+
keep_gpu_sections = True,
180+
remote_execution = re_test_utils.remote_execution(
181+
platform = "gpu-remote-execution",
182+
),
183+
)

backends/cuda/runtime/cuda_allocator.cpp

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include <cuda_runtime.h>
1212

13+
#include <executorch/extension/cuda/caller_stream.h>
1314
#include <executorch/runtime/platform/log.h>
1415

1516
namespace executorch::backends::cuda {
@@ -124,12 +125,30 @@ void CudaAllocator::deallocate(void* ptr, DeviceIndex index) {
124125
}
125126
}
126127

127-
// TODO(gasoonjia): Add support for async copy
128128
Error CudaAllocator::copy_host_to_device(
129129
void* dst,
130130
const void* src,
131131
size_t nbytes,
132132
DeviceIndex index) {
133+
ET_CHECK_OR_RETURN_ERROR(
134+
dst != nullptr,
135+
InvalidArgument,
136+
"CudaAllocator::copy_host_to_device dst is null");
137+
ET_CHECK_OR_RETURN_ERROR(
138+
src != nullptr,
139+
InvalidArgument,
140+
"CudaAllocator::copy_host_to_device src is null");
141+
if (nbytes == 0) {
142+
return Error::Ok;
143+
}
144+
// TODO: validate caller stream device matches index.
145+
// For now assert index is -1 or 0.
146+
ET_CHECK_OR_RETURN_ERROR(
147+
index == -1 || index == 0,
148+
InvalidArgument,
149+
"CudaAllocator::copy_host_to_device only supports device 0 or -1 (current), got %d",
150+
static_cast<int>(index));
151+
133152
int prev_device = 0;
134153
cudaError_t prev_device_err = cudaSuccess;
135154

@@ -139,8 +158,16 @@ Error CudaAllocator::copy_host_to_device(
139158
cudaSetDevice(index);
140159
}
141160
}
142-
143-
cudaError_t err = cudaMemcpy(dst, src, nbytes, cudaMemcpyHostToDevice);
161+
cudaError_t err = cudaSuccess;
162+
const auto caller_stream = executorch::extension::cuda::getCallerStream();
163+
if (caller_stream) {
164+
err = cudaMemcpyAsync(
165+
dst, src, nbytes, cudaMemcpyHostToDevice, *caller_stream);
166+
// We don't synchronize the stream here because the caller is expected to
167+
// synchronize the stream.
168+
} else {
169+
err = cudaMemcpy(dst, src, nbytes, cudaMemcpyHostToDevice);
170+
}
144171

145172
if (index >= 0 && prev_device_err == cudaSuccess) {
146173
cudaSetDevice(prev_device);
@@ -158,12 +185,30 @@ Error CudaAllocator::copy_host_to_device(
158185
return Error::Ok;
159186
}
160187

161-
// TODO(gasoonjia): Add support for async copy
162188
Error CudaAllocator::copy_device_to_host(
163189
void* dst,
164190
const void* src,
165191
size_t nbytes,
166192
DeviceIndex index) {
193+
ET_CHECK_OR_RETURN_ERROR(
194+
dst != nullptr,
195+
InvalidArgument,
196+
"CudaAllocator::copy_device_to_host dst is null");
197+
ET_CHECK_OR_RETURN_ERROR(
198+
src != nullptr,
199+
InvalidArgument,
200+
"CudaAllocator::copy_device_to_host src is null");
201+
if (nbytes == 0) {
202+
return Error::Ok;
203+
}
204+
// TODO: validate caller stream device matches index.
205+
// For now assert index is -1 or 0.
206+
ET_CHECK_OR_RETURN_ERROR(
207+
index == -1 || index == 0,
208+
InvalidArgument,
209+
"CudaAllocator::copy_device_to_host only supports device 0 or -1 (current), got %d",
210+
static_cast<int>(index));
211+
167212
int prev_device = 0;
168213
cudaError_t prev_device_err = cudaSuccess;
169214

@@ -173,8 +218,17 @@ Error CudaAllocator::copy_device_to_host(
173218
cudaSetDevice(index);
174219
}
175220
}
176-
177-
cudaError_t err = cudaMemcpy(dst, src, nbytes, cudaMemcpyDeviceToHost);
221+
cudaError_t err = cudaSuccess;
222+
const auto caller_stream = executorch::extension::cuda::getCallerStream();
223+
if (caller_stream) {
224+
err = cudaMemcpyAsync(
225+
dst, src, nbytes, cudaMemcpyDeviceToHost, *caller_stream);
226+
if (err == cudaSuccess) {
227+
err = cudaStreamSynchronize(*caller_stream);
228+
}
229+
} else {
230+
err = cudaMemcpy(dst, src, nbytes, cudaMemcpyDeviceToHost);
231+
}
178232

179233
if (index >= 0 && prev_device_err == cudaSuccess) {
180234
cudaSetDevice(prev_device);
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <gtest/gtest.h>
10+
11+
#include <cuda_runtime.h>
12+
13+
#include <cstdint>
14+
#include <vector>
15+
16+
#include <executorch/backends/cuda/runtime/cuda_allocator.h>
17+
#include <executorch/extension/cuda/caller_stream.h>
18+
#include <executorch/runtime/core/error.h>
19+
#include <executorch/runtime/platform/platform.h>
20+
21+
using executorch::backends::cuda::CudaAllocator;
22+
using executorch::runtime::Error;
23+
24+
namespace {
25+
bool cuda_device_available() {
26+
int device_count = 0;
27+
const cudaError_t err = cudaGetDeviceCount(&device_count);
28+
return err == cudaSuccess && device_count > 0;
29+
}
30+
} // namespace
31+
32+
TEST(CudaAllocatorTest, CopyHostToDevice) {
33+
if (!cuda_device_available()) {
34+
GTEST_SKIP() << "CUDA device unavailable";
35+
}
36+
et_pal_init();
37+
CudaAllocator& a = CudaAllocator::instance();
38+
constexpr size_t N = 1024;
39+
auto res = a.allocate(N, 0);
40+
ASSERT_TRUE(res.ok());
41+
void* dptr = res.get();
42+
43+
std::vector<uint8_t> h_src(N, 42);
44+
EXPECT_EQ(a.copy_host_to_device(dptr, h_src.data(), N, 0), Error::Ok);
45+
46+
a.deallocate(dptr, 0);
47+
}
48+
49+
TEST(CudaAllocatorTest, CopyDeviceToHost) {
50+
if (!cuda_device_available()) {
51+
GTEST_SKIP() << "CUDA device unavailable";
52+
}
53+
et_pal_init();
54+
CudaAllocator& a = CudaAllocator::instance();
55+
constexpr size_t N = 1024;
56+
auto res = a.allocate(N, 0);
57+
ASSERT_TRUE(res.ok());
58+
void* dptr = res.get();
59+
60+
std::vector<uint8_t> h_src(N, 42), h_dst(N, 0);
61+
ASSERT_EQ(a.copy_host_to_device(dptr, h_src.data(), N, 0), Error::Ok);
62+
EXPECT_EQ(a.copy_device_to_host(h_dst.data(), dptr, N, 0), Error::Ok);
63+
EXPECT_EQ(h_src, h_dst);
64+
65+
a.deallocate(dptr, 0);
66+
}
67+
68+
TEST(CudaAllocatorTest, CopyHostToDeviceWithCallerStream) {
69+
if (!cuda_device_available()) {
70+
GTEST_SKIP() << "CUDA device unavailable";
71+
}
72+
et_pal_init();
73+
int device = 0;
74+
ASSERT_EQ(cudaGetDevice(&device), cudaSuccess);
75+
ASSERT_EQ(device, 0) << "test assumes single GPU device 0";
76+
// TODO: validate caller stream device matches index once CallerStreamGuard
77+
// exposes device. For now assert single-GPU case.
78+
cudaStream_t s;
79+
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
80+
{
81+
executorch::extension::cuda::CallerStreamGuard g(s);
82+
83+
CudaAllocator& a = CudaAllocator::instance();
84+
auto res = a.allocate(256, 0);
85+
ASSERT_TRUE(res.ok());
86+
void* d = res.get();
87+
std::vector<uint8_t> h(256, 7);
88+
// should take async branch internally, still return Ok
89+
EXPECT_EQ(a.copy_host_to_device(d, h.data(), 256, 0), Error::Ok);
90+
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
91+
a.deallocate(d, 0);
92+
}
93+
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
94+
}
95+
96+
TEST(CudaAllocatorTest, CopyDeviceToHostWithCallerStream) {
97+
if (!cuda_device_available()) {
98+
GTEST_SKIP() << "CUDA device unavailable";
99+
}
100+
et_pal_init();
101+
int device = 0;
102+
ASSERT_EQ(cudaGetDevice(&device), cudaSuccess);
103+
ASSERT_EQ(device, 0) << "test assumes single GPU device 0";
104+
// TODO: validate caller stream device matches index once CallerStreamGuard
105+
// exposes device. For now assert single-GPU case.
106+
cudaStream_t s;
107+
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
108+
{
109+
executorch::extension::cuda::CallerStreamGuard g(s);
110+
111+
CudaAllocator& a = CudaAllocator::instance();
112+
auto res = a.allocate(256, 0);
113+
ASSERT_TRUE(res.ok());
114+
void* d = res.get();
115+
std::vector<uint8_t> h_src(256, 5), h_dst(256, 0);
116+
ASSERT_EQ(a.copy_host_to_device(d, h_src.data(), 256, 0), Error::Ok);
117+
EXPECT_EQ(a.copy_device_to_host(h_dst.data(), d, 256, 0), Error::Ok);
118+
EXPECT_EQ(h_src, h_dst);
119+
120+
a.deallocate(d, 0);
121+
}
122+
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
123+
}
124+
125+
TEST(CudaAllocatorTest, CopyHostToDeviceNullDstReturnsInvalidArgument) {
126+
if (!cuda_device_available()) {
127+
GTEST_SKIP() << "CUDA device unavailable";
128+
}
129+
et_pal_init();
130+
CudaAllocator& a = CudaAllocator::instance();
131+
// null dst should fail gracefully not CHECK abort
132+
std::vector<uint8_t> h(8, 1);
133+
Error e = a.copy_host_to_device(nullptr, h.data(), 8, 0);
134+
EXPECT_EQ(e, Error::InvalidArgument)
135+
<< "expected InvalidArgument for null dst, got "
136+
<< static_cast<uint32_t>(e);
137+
}
138+
139+
TEST(CudaAllocatorTest, CopyHostToDeviceNullSrcReturnsInvalidArgument) {
140+
if (!cuda_device_available()) {
141+
GTEST_SKIP() << "CUDA device unavailable";
142+
}
143+
et_pal_init();
144+
CudaAllocator& a = CudaAllocator::instance();
145+
void* dummy_dst = reinterpret_cast<void*>(0x1);
146+
Error e = a.copy_host_to_device(dummy_dst, nullptr, 8, 0);
147+
EXPECT_EQ(e, Error::InvalidArgument)
148+
<< "expected InvalidArgument for null src, got "
149+
<< static_cast<uint32_t>(e);
150+
}
151+
152+
TEST(CudaAllocatorTest, CopyDeviceToHostNullDstReturnsInvalidArgument) {
153+
if (!cuda_device_available()) {
154+
GTEST_SKIP() << "CUDA device unavailable";
155+
}
156+
et_pal_init();
157+
CudaAllocator& a = CudaAllocator::instance();
158+
void* dummy_src = reinterpret_cast<void*>(0x1);
159+
Error e = a.copy_device_to_host(nullptr, dummy_src, 8, 0);
160+
EXPECT_EQ(e, Error::InvalidArgument)
161+
<< "expected InvalidArgument for null dst, got "
162+
<< static_cast<uint32_t>(e);
163+
}
164+
165+
TEST(CudaAllocatorTest, CopyDeviceToHostNullSrcReturnsInvalidArgument) {
166+
if (!cuda_device_available()) {
167+
GTEST_SKIP() << "CUDA device unavailable";
168+
}
169+
et_pal_init();
170+
CudaAllocator& a = CudaAllocator::instance();
171+
std::vector<uint8_t> h(8, 1);
172+
// null src should fail gracefully not CHECK abort
173+
Error e = a.copy_device_to_host(h.data(), nullptr, 8, 0);
174+
EXPECT_EQ(e, Error::InvalidArgument)
175+
<< "expected InvalidArgument for null src, got "
176+
<< static_cast<uint32_t>(e);
177+
}

0 commit comments

Comments
 (0)