Skip to content

Commit 07765c3

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` and synchronize the stream before returning — preserving the blocking API contract while allowing work to be issued on the caller's stream. 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 a0a730a commit 07765c3

4 files changed

Lines changed: 254 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: 58 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+
if (nbytes == 0) {
134+
return Error::Ok;
135+
}
136+
ET_CHECK_OR_RETURN_ERROR(
137+
dst != nullptr,
138+
InvalidArgument,
139+
"CudaAllocator::copy_host_to_device dst is null");
140+
ET_CHECK_OR_RETURN_ERROR(
141+
src != nullptr,
142+
InvalidArgument,
143+
"CudaAllocator::copy_host_to_device src is null");
144+
// TODO: validate caller stream device matches index.
145+
// For now assert single-GPU case.
146+
ET_CHECK_OR_RETURN_ERROR(
147+
index == -1 || index == 0,
148+
InvalidArgument,
149+
"CudaAllocator::copy_host_to_device only supports device 0, got %d",
150+
static_cast<int>(index));
151+
133152
int prev_device = 0;
134153
cudaError_t prev_device_err = cudaSuccess;
135154

@@ -139,8 +158,14 @@ 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+
} else {
167+
err = cudaMemcpy(dst, src, nbytes, cudaMemcpyHostToDevice);
168+
}
144169

145170
if (index >= 0 && prev_device_err == cudaSuccess) {
146171
cudaSetDevice(prev_device);
@@ -158,12 +183,30 @@ Error CudaAllocator::copy_host_to_device(
158183
return Error::Ok;
159184
}
160185

161-
// TODO(gasoonjia): Add support for async copy
162186
Error CudaAllocator::copy_device_to_host(
163187
void* dst,
164188
const void* src,
165189
size_t nbytes,
166190
DeviceIndex index) {
191+
if (nbytes == 0) {
192+
return Error::Ok;
193+
}
194+
ET_CHECK_OR_RETURN_ERROR(
195+
dst != nullptr,
196+
InvalidArgument,
197+
"CudaAllocator::copy_device_to_host dst is null");
198+
ET_CHECK_OR_RETURN_ERROR(
199+
src != nullptr,
200+
InvalidArgument,
201+
"CudaAllocator::copy_device_to_host src is null");
202+
// TODO: validate caller stream device matches index.
203+
// For now assert single-GPU case.
204+
ET_CHECK_OR_RETURN_ERROR(
205+
index == -1 || index == 0,
206+
InvalidArgument,
207+
"CudaAllocator::copy_device_to_host only supports device 0, got %d",
208+
static_cast<int>(index));
209+
167210
int prev_device = 0;
168211
cudaError_t prev_device_err = cudaSuccess;
169212

@@ -173,8 +216,17 @@ Error CudaAllocator::copy_device_to_host(
173216
cudaSetDevice(index);
174217
}
175218
}
176-
177-
cudaError_t err = cudaMemcpy(dst, src, nbytes, cudaMemcpyDeviceToHost);
219+
cudaError_t err = cudaSuccess;
220+
const auto caller_stream = executorch::extension::cuda::getCallerStream();
221+
if (caller_stream) {
222+
err = cudaMemcpyAsync(
223+
dst, src, nbytes, cudaMemcpyDeviceToHost, *caller_stream);
224+
if (err == cudaSuccess) {
225+
err = cudaStreamSynchronize(*caller_stream);
226+
}
227+
} else {
228+
err = cudaMemcpy(dst, src, nbytes, cudaMemcpyDeviceToHost);
229+
}
178230

179231
if (index >= 0 && prev_device_err == cudaSuccess) {
180232
cudaSetDevice(prev_device);
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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+
executorch::extension::cuda::CallerStreamGuard g(s);
81+
82+
CudaAllocator& a = CudaAllocator::instance();
83+
auto res = a.allocate(256, 0);
84+
ASSERT_TRUE(res.ok());
85+
void* d = res.get();
86+
std::vector<uint8_t> h(256, 7);
87+
// should take async branch internally, still return Ok
88+
EXPECT_EQ(a.copy_host_to_device(d, h.data(), 256, 0), Error::Ok);
89+
a.deallocate(d, 0);
90+
cudaStreamDestroy(s);
91+
}
92+
93+
TEST(CudaAllocatorTest, CopyDeviceToHostWithCallerStream) {
94+
if (!cuda_device_available()) {
95+
GTEST_SKIP() << "CUDA device unavailable";
96+
}
97+
et_pal_init();
98+
int device = 0;
99+
ASSERT_EQ(cudaGetDevice(&device), cudaSuccess);
100+
ASSERT_EQ(device, 0) << "test assumes single GPU device 0";
101+
// TODO: validate caller stream device matches index once CallerStreamGuard
102+
// exposes device. For now assert single-GPU case.
103+
cudaStream_t s;
104+
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
105+
executorch::extension::cuda::CallerStreamGuard g(s);
106+
107+
CudaAllocator& a = CudaAllocator::instance();
108+
auto res = a.allocate(256, 0);
109+
ASSERT_TRUE(res.ok());
110+
void* d = res.get();
111+
std::vector<uint8_t> h_src(256, 5), h_dst(256, 0);
112+
ASSERT_EQ(a.copy_host_to_device(d, h_src.data(), 256, 0), Error::Ok);
113+
EXPECT_EQ(a.copy_device_to_host(h_dst.data(), d, 256, 0), Error::Ok);
114+
EXPECT_EQ(h_src, h_dst);
115+
116+
a.deallocate(d, 0);
117+
cudaStreamDestroy(s);
118+
}
119+
120+
TEST(CudaAllocatorTest, CopyHostToDeviceNullDstReturnsInvalidArgument) {
121+
if (!cuda_device_available()) {
122+
GTEST_SKIP() << "CUDA device unavailable";
123+
}
124+
et_pal_init();
125+
CudaAllocator& a = CudaAllocator::instance();
126+
// null dst should fail gracefully not CHECK abort
127+
std::vector<uint8_t> h(8, 1);
128+
Error e = a.copy_host_to_device(nullptr, h.data(), 8, 0);
129+
EXPECT_EQ(e, Error::InvalidArgument)
130+
<< "expected InvalidArgument for null dst, got "
131+
<< static_cast<uint32_t>(e);
132+
}
133+
134+
TEST(CudaAllocatorTest, CopyHostToDeviceNullSrcReturnsInvalidArgument) {
135+
if (!cuda_device_available()) {
136+
GTEST_SKIP() << "CUDA device unavailable";
137+
}
138+
et_pal_init();
139+
CudaAllocator& a = CudaAllocator::instance();
140+
void* dummy_dst = reinterpret_cast<void*>(0x1);
141+
Error e = a.copy_host_to_device(dummy_dst, nullptr, 8, 0);
142+
EXPECT_EQ(e, Error::InvalidArgument)
143+
<< "expected InvalidArgument for null src, got "
144+
<< static_cast<uint32_t>(e);
145+
}
146+
147+
TEST(CudaAllocatorTest, CopyDeviceToHostNullDstReturnsInvalidArgument) {
148+
if (!cuda_device_available()) {
149+
GTEST_SKIP() << "CUDA device unavailable";
150+
}
151+
et_pal_init();
152+
CudaAllocator& a = CudaAllocator::instance();
153+
void* dummy_src = reinterpret_cast<void*>(0x1);
154+
Error e = a.copy_device_to_host(nullptr, dummy_src, 8, 0);
155+
EXPECT_EQ(e, Error::InvalidArgument)
156+
<< "expected InvalidArgument for null dst, got "
157+
<< static_cast<uint32_t>(e);
158+
}
159+
160+
TEST(CudaAllocatorTest, CopyDeviceToHostNullSrcReturnsInvalidArgument) {
161+
if (!cuda_device_available()) {
162+
GTEST_SKIP() << "CUDA device unavailable";
163+
}
164+
et_pal_init();
165+
CudaAllocator& a = CudaAllocator::instance();
166+
std::vector<uint8_t> h(8, 1);
167+
// null src should fail gracefully not CHECK abort
168+
Error e = a.copy_device_to_host(h.data(), nullptr, 8, 0);
169+
EXPECT_EQ(e, Error::InvalidArgument)
170+
<< "expected InvalidArgument for null src, got "
171+
<< static_cast<uint32_t>(e);
172+
}

0 commit comments

Comments
 (0)