Skip to content

Commit c7a6cc9

Browse files
authored
feat(cuda): track device memory accesses across streams (#10721)
## Problem Device memory shared across streams needs ordered access and deallocation without redundant waits. ## Solution Add typed allocations/views, copies, pointer guards, pooled access events, and kernel launches. ## Assessment Depends on #10720; activated by #10722. The driver remains excluded from publishing. Signed-off-by: Alexander Droste <alexander.droste@protonmail.com>
1 parent 0373c93 commit c7a6cc9

9 files changed

Lines changed: 1559 additions & 5 deletions

File tree

‎Cargo.lock‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎vortex-cuda/driver/Cargo.toml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[package]
55
name = "vortex-cuda-driver"
66
authors.workspace = true
7-
description = "Minimal CUDA driver API bindings for Vortex"
7+
description = "Minimal CUDA driver API bindings with cross-stream-only synchronization for Vortex"
88
edition = { workspace = true }
99
categories = { workspace = true }
1010
include = { workspace = true }
@@ -19,5 +19,12 @@ publish = false
1919
[lints]
2020
workspace = true
2121

22+
[features]
23+
default = []
24+
f16 = ["dep:half"]
25+
2226
[dependencies]
27+
half = { workspace = true, optional = true }
2328
libloading = { workspace = true }
29+
parking_lot = { workspace = true }
30+
smallvec = { workspace = true }

‎vortex-cuda/driver/src/context.rs‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
//! The device's primary context and deferred errors.
4+
//! The device's primary context, its event pool, and deferred errors.
55
66
use std::ffi::CStr;
77
use std::ffi::c_char;
88
use std::sync::Arc;
99
use std::sync::atomic::AtomicU32;
1010
use std::sync::atomic::Ordering;
1111

12+
use parking_lot::Mutex;
13+
1214
use crate::module::CudaModule;
1315
use crate::result;
1416
use crate::result::DriverError;
@@ -29,9 +31,12 @@ pub struct CudaContext {
2931
/// Whether the context is the device's primary context, released rather than destroyed.
3032
primary: bool,
3133
error_state: AtomicU32,
34+
/// Timing-disabled events returned by finished accesses, reused to avoid creating and
35+
/// destroying an event per access.
36+
events: Mutex<Vec<sys::CUevent>>,
3237
}
3338

34-
// SAFETY: The handles are driver handles usable from any thread.
39+
// SAFETY: The handles are driver handles usable from any thread; the pool is synchronized.
3540
unsafe impl Send for CudaContext {}
3641
// SAFETY: As above.
3742
unsafe impl Sync for CudaContext {}
@@ -55,6 +60,10 @@ impl Eq for CudaContext {}
5560
impl Drop for CudaContext {
5661
fn drop(&mut self) {
5762
self.record_err(self.bind_to_thread());
63+
for event in std::mem::take(self.events.get_mut()) {
64+
// SAFETY: Pooled events are owned by the pool and no longer in use.
65+
self.record_err(unsafe { sys::cuEventDestroy_v2(event) }.result());
66+
}
5867
// SAFETY: Releases the retain made by `new`, or destroys the context created by
5968
// `new_non_primary`, once.
6069
self.record_err(
@@ -116,6 +125,7 @@ impl CudaContext {
116125
has_async_alloc: pools > 0,
117126
primary,
118127
error_state: AtomicU32::new(0),
128+
events: Mutex::new(Vec::new()),
119129
});
120130
context.bind_to_thread()?;
121131
Ok(context)
@@ -232,6 +242,37 @@ impl CudaContext {
232242
CudaModule::load(self, image)
233243
}
234244

245+
/// A timing-disabled event from the pool, or a new one.
246+
pub(crate) fn take_event(&self) -> Result<sys::CUevent, DriverError> {
247+
if let Some(event) = self.events.lock().pop() {
248+
return Ok(event);
249+
}
250+
self.bind_to_thread()?;
251+
let mut event = std::ptr::null_mut();
252+
// SAFETY: The context is current; the driver writes the new event.
253+
unsafe {
254+
sys::cuEventCreate(
255+
&raw mut event,
256+
sys::CUevent_flags::CU_EVENT_DISABLE_TIMING.0,
257+
)
258+
}
259+
.result()?;
260+
Ok(event)
261+
}
262+
263+
/// Returns an event to the pool once no access will wait for it again.
264+
///
265+
/// Waits already enqueued on it are unaffected by later records.
266+
pub(crate) fn give_event(&self, event: sys::CUevent) {
267+
self.events.lock().push(event);
268+
}
269+
270+
/// The number of events waiting in the pool.
271+
#[cfg(test)]
272+
pub(crate) fn pooled_events(&self) -> usize {
273+
self.events.lock().len()
274+
}
275+
235276
/// The first error recorded where none could be returned, clearing it.
236277
pub fn check_err(&self) -> Result<(), DriverError> {
237278
match self.error_state.swap(0, Ordering::Relaxed) {

‎vortex-cuda/driver/src/launch.rs‎

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Kernel launches with stream-ordered device memory arguments.
5+
6+
use std::ffi::c_void;
7+
8+
use crate::memory::AccessKind;
9+
use crate::memory::ArgAccess;
10+
use crate::memory::CudaSlice;
11+
use crate::memory::CudaView;
12+
use crate::memory::CudaViewMut;
13+
use crate::module::CudaFunction;
14+
use crate::repr::DeviceRepr;
15+
use crate::result::DriverError;
16+
use crate::stream::CudaEvent;
17+
use crate::stream::CudaStream;
18+
use crate::sys;
19+
20+
/// Grid, block and dynamic shared memory of a launch.
21+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22+
pub struct LaunchConfig {
23+
/// Blocks per grid dimension.
24+
pub grid_dim: (u32, u32, u32),
25+
/// Threads per block dimension.
26+
pub block_dim: (u32, u32, u32),
27+
/// Dynamic shared memory per block, in bytes.
28+
pub shared_mem_bytes: u32,
29+
}
30+
31+
impl LaunchConfig {
32+
/// One-dimensional blocks of 1024 threads covering `n` elements.
33+
pub fn for_num_elems(n: u32) -> Self {
34+
Self {
35+
grid_dim: (n.div_ceil(1024), 1, 1),
36+
block_dim: (1024, 1, 1),
37+
shared_mem_bytes: 0,
38+
}
39+
}
40+
}
41+
42+
/// A kernel launch being assembled on a stream; create with [`CudaStream::launch_builder`].
43+
///
44+
/// Device memory arguments are ordered after earlier conflicting accesses and recorded as
45+
/// accesses of this launch.
46+
pub struct LaunchArgs<'a> {
47+
stream: &'a CudaStream,
48+
function: &'a CudaFunction,
49+
// `Vec`, unlike `SmallVec`, lets argument borrows end before the builder drops.
50+
args: Vec<*mut c_void>,
51+
accesses: Vec<ArgAccess<'a>>,
52+
timing: Option<sys::CUevent_flags>,
53+
}
54+
55+
impl std::fmt::Debug for LaunchArgs<'_> {
56+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57+
f.debug_struct("LaunchArgs")
58+
.field("args", &self.args.len())
59+
.finish()
60+
}
61+
}
62+
63+
impl CudaStream {
64+
/// Starts a launch of `function` on this stream.
65+
pub fn launch_builder<'a>(&'a self, function: &'a CudaFunction) -> LaunchArgs<'a> {
66+
LaunchArgs {
67+
stream: self,
68+
function,
69+
args: Vec::with_capacity(8),
70+
accesses: Vec::with_capacity(4),
71+
timing: None,
72+
}
73+
}
74+
}
75+
76+
/// A value that can be passed as a kernel argument.
77+
///
78+
/// # Safety
79+
///
80+
/// Implementations must push a pointer to storage that lives as long as the [`LaunchArgs`] and
81+
/// holds the argument in the kernel's expected representation.
82+
pub unsafe trait PushKernelArg<T> {
83+
/// Appends the argument.
84+
fn arg(&mut self, arg: T) -> &mut Self;
85+
}
86+
87+
// SAFETY: The reference outlives the launch and `T` has a device representation.
88+
unsafe impl<'a, 'b: 'a, T: DeviceRepr> PushKernelArg<&'b T> for LaunchArgs<'a> {
89+
#[inline]
90+
fn arg(&mut self, arg: &'b T) -> &mut Self {
91+
self.args.push((arg as *const T).cast_mut().cast());
92+
self
93+
}
94+
}
95+
96+
// SAFETY: The device pointer lives in the slice, which outlives the launch.
97+
unsafe impl<'a, 'b: 'a, T> PushKernelArg<&'b CudaSlice<T>> for LaunchArgs<'a> {
98+
#[inline]
99+
fn arg(&mut self, arg: &'b CudaSlice<T>) -> &mut Self {
100+
let (access, pointer) = arg.arg_access(AccessKind::Read);
101+
self.accesses.push(access);
102+
self.args
103+
.push((pointer as *const sys::CUdeviceptr).cast_mut().cast());
104+
self
105+
}
106+
}
107+
108+
// SAFETY: The device pointer lives in the slice, which outlives the launch.
109+
unsafe impl<'a, 'b: 'a, T> PushKernelArg<&'b mut CudaSlice<T>> for LaunchArgs<'a> {
110+
#[inline]
111+
fn arg(&mut self, arg: &'b mut CudaSlice<T>) -> &mut Self {
112+
let (access, pointer) = arg.arg_access(AccessKind::Write);
113+
self.accesses.push(access);
114+
self.args
115+
.push((pointer as *const sys::CUdeviceptr).cast_mut().cast());
116+
self
117+
}
118+
}
119+
120+
// SAFETY: The device pointer lives in the view, which outlives the launch.
121+
unsafe impl<'a, 'b: 'a, 'c: 'b, T> PushKernelArg<&'b CudaView<'c, T>> for LaunchArgs<'a> {
122+
#[inline]
123+
fn arg(&mut self, arg: &'b CudaView<'c, T>) -> &mut Self {
124+
let (access, pointer) = arg.arg_access();
125+
self.accesses.push(access);
126+
self.args
127+
.push((pointer as *const sys::CUdeviceptr).cast_mut().cast());
128+
self
129+
}
130+
}
131+
132+
// SAFETY: The device pointer lives in the view, which outlives the launch.
133+
unsafe impl<'a, 'b: 'a, 'c: 'b, T> PushKernelArg<&'b mut CudaViewMut<'c, T>> for LaunchArgs<'a> {
134+
#[inline]
135+
fn arg(&mut self, arg: &'b mut CudaViewMut<'c, T>) -> &mut Self {
136+
let (access, pointer) = arg.arg_access();
137+
self.accesses.push(access);
138+
self.args
139+
.push((pointer as *const sys::CUdeviceptr).cast_mut().cast());
140+
self
141+
}
142+
}
143+
144+
impl LaunchArgs<'_> {
145+
/// Makes [`launch`](Self::launch) return events recorded before and after the kernel.
146+
pub fn record_kernel_launch(&mut self, flags: sys::CUevent_flags) -> &mut Self {
147+
self.timing = Some(flags);
148+
self
149+
}
150+
151+
/// Enqueues the kernel after the arguments' earlier conflicting accesses.
152+
///
153+
/// # Safety
154+
///
155+
/// The arguments must match the kernel's parameters, and the kernel must access device
156+
/// memory only within the arguments, writing only those passed mutably.
157+
pub unsafe fn launch(
158+
&mut self,
159+
config: LaunchConfig,
160+
) -> Result<Option<(CudaEvent, CudaEvent)>, DriverError> {
161+
self.stream.bind()?;
162+
for access in &self.accesses {
163+
access
164+
.access
165+
.before(access.owner, self.stream, access.kind)?;
166+
}
167+
let start = self
168+
.timing
169+
.map(|flags| self.stream.record_event(Some(flags)))
170+
.transpose()?;
171+
// SAFETY: Under the caller's guarantees; the argument pointers outlive the call.
172+
unsafe {
173+
sys::cuLaunchKernel(
174+
self.function.cu_function(),
175+
config.grid_dim.0,
176+
config.grid_dim.1,
177+
config.grid_dim.2,
178+
config.block_dim.0,
179+
config.block_dim.1,
180+
config.block_dim.2,
181+
config.shared_mem_bytes,
182+
self.stream.cu_stream(),
183+
self.args.as_mut_ptr(),
184+
std::ptr::null_mut(),
185+
)
186+
}
187+
.result()?;
188+
// The kernel is already queued: a trailing-event failure must not skip access tracking.
189+
let end = self
190+
.timing
191+
.map(|flags| self.stream.record_event(Some(flags)))
192+
.transpose();
193+
for access in &self.accesses {
194+
access
195+
.access
196+
.after(access.owner, self.stream, access.kind)?;
197+
}
198+
Ok(start.zip(end?))
199+
}
200+
}

‎vortex-cuda/driver/src/lib.rs‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,41 @@
33

44
//! Minimal CUDA driver API bindings for Vortex.
55
//!
6-
//! Owns CUDA contexts, streams, events, and loaded modules. The driver library is loaded
7-
//! lazily, so dependents build and run without CUDA until they first use it.
6+
//! Covers the driver surface Vortex uses: the primary context, streams, events, modules,
7+
//! kernel launches, and stream-ordered device memory. The driver library is loaded lazily, so
8+
//! dependents build and run without CUDA until they first use it.
9+
//!
10+
//! Device memory orders its accesses across streams automatically, paying for driver calls
11+
//! only when an access crosses streams; see [`CudaSlice`].
812
913
mod context;
14+
mod launch;
15+
mod memory;
1016
mod module;
17+
mod repr;
1118
pub mod result;
1219
mod stream;
1320
pub mod sys;
1421

1522
pub use context::CudaContext;
23+
pub use launch::LaunchArgs;
24+
pub use launch::LaunchConfig;
25+
pub use launch::PushKernelArg;
26+
pub use memory::CudaSlice;
27+
pub use memory::CudaView;
28+
pub use memory::CudaViewMut;
29+
pub use memory::DevicePtr;
30+
pub use memory::DevicePtrMut;
31+
pub use memory::DeviceSlice;
32+
pub use memory::HostSlice;
33+
pub use memory::SyncOnDrop;
1634
pub use module::CudaFunction;
1735
pub use module::CudaModule;
36+
pub use repr::DeviceRepr;
37+
pub use repr::ValidAsZeroBits;
1838
pub use result::DriverError;
1939
pub use stream::CudaEvent;
2040
pub use stream::CudaStream;
41+
42+
#[cfg(test)]
43+
mod tests;

0 commit comments

Comments
 (0)