Skip to main content

scirs2_core/gpu/
stream_allocator.rs

1//! Per-stream GPU memory allocator for CUDA stream isolation.
2//!
3//! Each CUDA stream gets its own allocation pool to avoid inter-stream contention.
4//! Falls back to shared CPU-backed pool when GPU is unavailable.
5//!
6//! # Architecture
7//!
8//! ```text
9//!   StreamAllocator
10//!         │
11//!         ├── StreamArena (stream 0) ── Vec<Vec<u8>> allocations
12//!         ├── StreamArena (stream 1) ── Vec<Vec<u8>> allocations
13//!         └── ...
14//! ```
15//!
16//! # Usage
17//!
18//! ```rust
19//! use scirs2_core::gpu::stream_allocator::{StreamAllocator, StreamId};
20//!
21//! let alloc = StreamAllocator::new(64 * 1024 * 1024, 512 * 1024 * 1024);
22//! let s0 = StreamId::default_stream();
23//! alloc.register_stream(s0).unwrap();
24//! let _ptr = alloc.allocate(s0, 4096).unwrap();
25//! alloc.reset_stream(s0);
26//! ```
27
28use std::collections::HashMap;
29use std::sync::Mutex;
30
31use super::GpuError;
32
33// ---------------------------------------------------------------------------
34// StreamId
35// ---------------------------------------------------------------------------
36
37/// Unique identifier for a GPU stream.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct StreamId(pub u64);
40
41impl StreamId {
42    /// The default (null) stream.
43    pub fn default_stream() -> Self {
44        StreamId(0)
45    }
46
47    /// Create a stream with a specific numeric id.
48    pub fn new(id: u64) -> Self {
49        StreamId(id)
50    }
51}
52
53impl std::fmt::Display for StreamId {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(f, "Stream({})", self.0)
56    }
57}
58
59// ---------------------------------------------------------------------------
60// StreamArena
61// ---------------------------------------------------------------------------
62
63/// Per-stream allocation arena backed by heap-allocated `Vec<u8>` buffers.
64struct StreamArena {
65    stream_id: StreamId,
66    allocated_bytes: usize,
67    max_bytes: usize,
68    /// Each element is an individual allocation; the Vec<u8> owns the memory.
69    allocations: Vec<Vec<u8>>,
70}
71
72impl StreamArena {
73    fn new(stream_id: StreamId, max_bytes: usize) -> Self {
74        Self {
75            stream_id,
76            allocated_bytes: 0,
77            max_bytes,
78            allocations: Vec::new(),
79        }
80    }
81
82    /// Allocate `bytes` bytes and return a raw pointer into the new buffer.
83    ///
84    /// # Safety
85    ///
86    /// The returned pointer is valid until the next call to `reset()` on this
87    /// arena or until the arena is dropped.  Callers must not use the pointer
88    /// after either of those events.
89    fn allocate(&mut self, bytes: usize) -> Result<*mut u8, GpuError> {
90        if bytes == 0 {
91            return Err(GpuError::InvalidParameter(
92                "allocation size must be > 0".to_string(),
93            ));
94        }
95        if self.allocated_bytes + bytes > self.max_bytes {
96            return Err(GpuError::OutOfMemory(format!(
97                "{}: {} bytes requested, {} bytes available",
98                self.stream_id,
99                bytes,
100                self.max_bytes.saturating_sub(self.allocated_bytes),
101            )));
102        }
103        let mut buf = vec![0u8; bytes];
104        let ptr = buf.as_mut_ptr();
105        self.allocations.push(buf);
106        self.allocated_bytes += bytes;
107        Ok(ptr)
108    }
109
110    /// Release all allocations in this arena (bulk free).
111    fn reset(&mut self) {
112        self.allocations.clear();
113        self.allocated_bytes = 0;
114    }
115
116    fn allocated_bytes(&self) -> usize {
117        self.allocated_bytes
118    }
119}
120
121// ---------------------------------------------------------------------------
122// StreamAllocator
123// ---------------------------------------------------------------------------
124
125/// Manager for per-stream GPU memory allocation.
126///
127/// Each registered stream owns an independent `StreamArena`.  Allocations on
128/// different streams cannot interfere with each other.
129pub struct StreamAllocator {
130    arenas: Mutex<HashMap<StreamId, StreamArena>>,
131    per_stream_max_bytes: usize,
132    global_max_bytes: usize,
133}
134
135impl StreamAllocator {
136    /// Create a new `StreamAllocator`.
137    ///
138    /// - `per_stream_max_bytes`: maximum bytes a single stream arena may hold.
139    /// - `global_max_bytes`: maximum total bytes across all streams combined.
140    pub fn new(per_stream_max_bytes: usize, global_max_bytes: usize) -> Self {
141        Self {
142            arenas: Mutex::new(HashMap::new()),
143            per_stream_max_bytes,
144            global_max_bytes,
145        }
146    }
147
148    /// Register a new stream.  Must be called before allocating on the stream.
149    ///
150    /// Returns `Err` if the stream is already registered.
151    pub fn register_stream(&self, stream_id: StreamId) -> Result<(), GpuError> {
152        let mut arenas = self.arenas.lock().map_err(|_| {
153            GpuError::Other("StreamAllocator mutex poisoned during register_stream".to_string())
154        })?;
155        if arenas.contains_key(&stream_id) {
156            return Err(GpuError::InvalidParameter(format!(
157                "stream {stream_id} is already registered",
158            )));
159        }
160        arenas.insert(
161            stream_id,
162            StreamArena::new(stream_id, self.per_stream_max_bytes),
163        );
164        Ok(())
165    }
166
167    /// Unregister a stream and free all its allocations.
168    pub fn unregister_stream(&self, stream_id: StreamId) {
169        if let Ok(mut arenas) = self.arenas.lock() {
170            arenas.remove(&stream_id);
171        }
172    }
173
174    /// Allocate `bytes` bytes on the given stream.
175    ///
176    /// Returns a raw pointer into the stream's arena buffer.  The pointer is
177    /// valid until the next `reset_stream` or `unregister_stream` call for
178    /// this stream.
179    pub fn allocate(&self, stream_id: StreamId, bytes: usize) -> Result<*mut u8, GpuError> {
180        let mut arenas = self.arenas.lock().map_err(|_| {
181            GpuError::Other("StreamAllocator mutex poisoned during allocate".to_string())
182        })?;
183
184        // Check global cap before acquiring per-stream arena.
185        let total: usize = arenas.values().map(|a| a.allocated_bytes()).sum();
186        if total + bytes > self.global_max_bytes {
187            return Err(GpuError::OutOfMemory(format!(
188                "global limit: {} bytes requested, {} bytes available",
189                bytes,
190                self.global_max_bytes.saturating_sub(total),
191            )));
192        }
193
194        let arena = arenas.get_mut(&stream_id).ok_or_else(|| {
195            GpuError::InvalidParameter(format!(
196                "stream {stream_id} is not registered; call register_stream first",
197            ))
198        })?;
199
200        arena.allocate(bytes)
201    }
202
203    /// Reset all allocations on a stream (bulk free for arena-style use).
204    ///
205    /// This is a no-op if the stream is not registered.
206    pub fn reset_stream(&self, stream_id: StreamId) {
207        if let Ok(mut arenas) = self.arenas.lock() {
208            if let Some(arena) = arenas.get_mut(&stream_id) {
209                arena.reset();
210            }
211        }
212    }
213
214    /// Total bytes allocated across all streams.
215    pub fn total_allocated_bytes(&self) -> usize {
216        self.arenas
217            .lock()
218            .map(|arenas| arenas.values().map(|a| a.allocated_bytes()).sum())
219            .unwrap_or(0)
220    }
221
222    /// List all registered stream IDs.
223    pub fn registered_streams(&self) -> Vec<StreamId> {
224        self.arenas
225            .lock()
226            .map(|arenas| arenas.keys().copied().collect())
227            .unwrap_or_default()
228    }
229}
230
231// ---------------------------------------------------------------------------
232// Tests
233// ---------------------------------------------------------------------------
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_stream_allocator_register() {
241        let alloc = StreamAllocator::new(1024, 8192);
242        let s0 = StreamId::new(0);
243        let s1 = StreamId::new(1);
244
245        alloc.register_stream(s0).expect("register s0 failed");
246        alloc.register_stream(s1).expect("register s1 failed");
247
248        let streams = alloc.registered_streams();
249        assert!(streams.contains(&s0), "s0 should be registered");
250        assert!(streams.contains(&s1), "s1 should be registered");
251        assert_eq!(streams.len(), 2);
252    }
253
254    #[test]
255    fn test_stream_allocator_register_duplicate() {
256        let alloc = StreamAllocator::new(1024, 8192);
257        let s0 = StreamId::default_stream();
258        alloc.register_stream(s0).expect("first register failed");
259        let result = alloc.register_stream(s0);
260        assert!(result.is_err(), "duplicate registration should return Err");
261    }
262
263    #[test]
264    fn test_stream_allocator_allocate() {
265        let alloc = StreamAllocator::new(1024 * 1024, 8 * 1024 * 1024);
266        let s0 = StreamId::new(10);
267        let s1 = StreamId::new(20);
268        alloc.register_stream(s0).expect("register s0");
269        alloc.register_stream(s1).expect("register s1");
270
271        let p0 = alloc.allocate(s0, 512).expect("allocate on s0");
272        let p1 = alloc.allocate(s1, 512).expect("allocate on s1");
273
274        // Pointers must be non-null and distinct (different arenas).
275        assert!(!p0.is_null(), "s0 pointer should not be null");
276        assert!(!p1.is_null(), "s1 pointer should not be null");
277        assert_ne!(p0, p1, "pointers from different streams should differ");
278
279        assert_eq!(alloc.total_allocated_bytes(), 1024);
280    }
281
282    #[test]
283    fn test_stream_allocator_overflow() {
284        let alloc = StreamAllocator::new(256, 8192);
285        let s = StreamId::new(5);
286        alloc.register_stream(s).expect("register");
287
288        // Should succeed within limit.
289        alloc
290            .allocate(s, 200)
291            .expect("first allocation should succeed");
292
293        // This would exceed per-stream cap.
294        let result = alloc.allocate(s, 200);
295        assert!(
296            matches!(result, Err(GpuError::OutOfMemory(_))),
297            "expected OutOfMemory, got {result:?}"
298        );
299    }
300
301    #[test]
302    fn test_stream_allocator_global_overflow() {
303        // Global cap of 300 bytes, per-stream cap of 200 bytes.
304        let alloc = StreamAllocator::new(200, 300);
305        let s0 = StreamId::new(0);
306        let s1 = StreamId::new(1);
307        alloc.register_stream(s0).expect("register s0");
308        alloc.register_stream(s1).expect("register s1");
309
310        alloc.allocate(s0, 200).expect("first allocation");
311        // Second stream allocation would exceed global cap.
312        let result = alloc.allocate(s1, 200);
313        assert!(
314            matches!(result, Err(GpuError::OutOfMemory(_))),
315            "expected global OutOfMemory"
316        );
317    }
318
319    #[test]
320    fn test_stream_allocator_reset() {
321        let alloc = StreamAllocator::new(1024, 8192);
322        let s = StreamId::new(99);
323        alloc.register_stream(s).expect("register");
324        alloc.allocate(s, 512).expect("allocate");
325        assert_eq!(alloc.total_allocated_bytes(), 512);
326
327        alloc.reset_stream(s);
328        assert_eq!(alloc.total_allocated_bytes(), 0, "reset should clear bytes");
329    }
330
331    #[test]
332    fn test_stream_allocator_unregister() {
333        let alloc = StreamAllocator::new(1024, 8192);
334        let s = StreamId::new(7);
335        alloc.register_stream(s).expect("register");
336        assert_eq!(alloc.registered_streams().len(), 1);
337
338        alloc.unregister_stream(s);
339        assert!(
340            alloc.registered_streams().is_empty(),
341            "stream should be gone after unregister"
342        );
343
344        // Allocating on an unregistered stream should fail.
345        let result = alloc.allocate(s, 64);
346        assert!(
347            result.is_err(),
348            "allocate on unregistered stream should fail"
349        );
350    }
351
352    #[test]
353    fn test_stream_id_default() {
354        assert_eq!(StreamId::default_stream(), StreamId(0));
355    }
356
357    #[test]
358    fn test_stream_allocator_zero_size_rejected() {
359        let alloc = StreamAllocator::new(1024, 8192);
360        let s = StreamId::new(3);
361        alloc.register_stream(s).expect("register");
362        let result = alloc.allocate(s, 0);
363        assert!(result.is_err(), "zero-size allocation should fail");
364    }
365}