Skip to main content

optirs_gpu/memory/vendors/
rocm_backend.rs

1// ROCm backend for GPU memory management
2//
3// This module provides AMD ROCm/HIP-specific memory management functionality,
4// including device memory allocation, HIP streams, and performance optimization
5// features specific to AMD GPUs.
6//
7// # This is a host-memory simulation, not real ROCm
8//
9// `optirs-gpu` is Pure Rust with no FFI dependencies by default, and this
10// crate ships no ROCm/HIP runtime bindings. There is therefore no real
11// `hipMalloc` underneath this module: "device", "host", "coarse-grained",
12// "fine-grained" and "host-visible" memory are all the *same* system-heap
13// allocation (see `sim_alloc`/`sim_dealloc` below), and `RocmDeviceProperties`
14// /`RocmStats` are example numbers, not a query of real hardware. This
15// module models the ROCm memory-management *API shape* for testing that
16// shape in isolation; treat every allocation as host memory and every
17// device number as illustrative.
18//
19// This extends to data movement: `memcpy` and `memcpy_async` copy **zero
20// bytes**. They build a `HipOperation` record, hand it to
21// `HipStreamManager::execute_operation` (which returns immediately and never
22// dereferences `src_ptr`/`dst_ptr`), and increment
23// `RocmStats::memory_transfers` — that counter says "this many `memcpy`
24// calls were made," not "this many bytes moved." An earlier revision of
25// this module also injected a `std::thread::sleep` here to imitate transfer
26// latency by operation kind; that fake timing has been removed, so the
27// distinction between `MemcpyHostToDevice`/`MemcpyDeviceToHost`/
28// `MemcpyDeviceToDevice`/`MemcpyAsync` no longer affects anything
29// observable. `RocmStats::stream_operations` and `RocmStats::kernel_launches`
30// are declared for API-shape completeness but nothing in this module ever
31// increments them — read a `0` there as "not tracked," not "none occurred."
32
33use std::collections::HashMap;
34use std::ffi::c_void;
35use std::sync::{Arc, Mutex};
36use std::time::{Duration, Instant};
37
38/// Byte alignment every simulated allocation below uses.
39const SIM_ALLOC_ALIGN: usize = 256;
40
41/// Allocate `size` bytes through the system allocator, 256-byte aligned,
42/// without the two ways the naive `std::alloc::alloc(Layout::from_size_align_unchecked(size,
43/// 256))` this module used to call was undefined behaviour: a zero-size
44/// layout is unsound to pass to `GlobalAlloc::alloc`, and a real allocation
45/// failure returns null, which must never be treated as valid memory. See
46/// `cuda_backend::sim_alloc` for the full rationale (this mirrors it).
47///
48/// The payload is prefixed with one `SIM_ALLOC_ALIGN`-byte header recording
49/// the requested size, so [`sim_dealloc`] can reconstruct the exact `Layout`
50/// this function used — the `Layout::from_size_align_unchecked(1, 1)` this
51/// module used at free time was a mismatched-layout deallocation, itself
52/// unconditionally undefined behaviour.
53fn sim_alloc(size: usize) -> Result<*mut c_void, RocmError> {
54    if size == 0 {
55        return Ok(SIM_ALLOC_ALIGN as *mut c_void);
56    }
57    let total = SIM_ALLOC_ALIGN.checked_add(size).ok_or_else(|| {
58        RocmError::OutOfMemory(format!(
59            "{size}-byte request overflows the allocator's size limit"
60        ))
61    })?;
62    let layout = std::alloc::Layout::from_size_align(total, SIM_ALLOC_ALIGN)
63        .map_err(|e| RocmError::OutOfMemory(format!("invalid allocation layout: {e}")))?;
64    // SAFETY: `layout` has non-zero size (checked above) and a valid
65    // (power-of-two) alignment constructed by `Layout::from_size_align`.
66    let base = unsafe { std::alloc::alloc(layout) };
67    if base.is_null() {
68        return Err(RocmError::OutOfMemory(format!(
69            "allocator returned null for a {size}-byte request"
70        )));
71    }
72    // SAFETY: `base` is non-null and `layout`'s size is at least
73    // `SIM_ALLOC_ALIGN + size >= SIM_ALLOC_ALIGN >= size_of::<usize>()`, so
74    // writing one `usize` at the start of the block is in-bounds.
75    unsafe { (base as *mut usize).write(size) };
76    // SAFETY: `base` was allocated with `total = SIM_ALLOC_ALIGN + size`
77    // bytes, so offsetting by `SIM_ALLOC_ALIGN` stays within (or one past)
78    // the allocation.
79    Ok(unsafe { base.add(SIM_ALLOC_ALIGN) } as *mut c_void)
80}
81
82/// Free a pointer returned by [`sim_alloc`]. A no-op for a null pointer or
83/// the zero-size sentinel — neither was ever allocated.
84///
85/// Only ever called from this module (it is not `pub`) with a pointer
86/// `sim_alloc` returned that has not already been freed — the unsafety of
87/// the pointer arithmetic below is contained to that invariant, matching
88/// this module's existing style of confining `unsafe` to the raw
89/// `std::alloc` calls rather than marking `free()`'s public wrapper unsafe.
90fn sim_dealloc(ptr: *mut c_void) {
91    if ptr.is_null() || (ptr as usize) == SIM_ALLOC_ALIGN {
92        return;
93    }
94    // SAFETY: by this function's contract `ptr` came from `sim_alloc`.
95    let base = unsafe { (ptr as *mut u8).sub(SIM_ALLOC_ALIGN) };
96    // SAFETY: `sim_alloc` wrote a `usize` at `base` before returning.
97    let size = unsafe { (base as *const usize).read() };
98    if let Ok(layout) = std::alloc::Layout::from_size_align(SIM_ALLOC_ALIGN + size, SIM_ALLOC_ALIGN)
99    {
100        // SAFETY: `layout` is exactly the layout `sim_alloc` allocated
101        // `base` with.
102        unsafe { std::alloc::dealloc(base, layout) };
103    }
104}
105
106/// ROCm memory backend implementation
107pub struct RocmMemoryBackend {
108    /// Backend configuration
109    config: RocmConfig,
110    /// Device properties
111    device_properties: RocmDeviceProperties,
112    /// Active HIP contexts
113    contexts: HashMap<u32, HipContext>,
114    /// Memory pools
115    memory_pools: HashMap<RocmMemoryType, RocmMemoryPool>,
116    /// Statistics
117    stats: RocmStats,
118    /// Stream management
119    stream_manager: HipStreamManager,
120}
121
122/// ROCm backend configuration
123#[derive(Debug, Clone)]
124pub struct RocmConfig {
125    /// Device ID to use
126    pub device_id: u32,
127    /// Enable coarse-grained memory
128    pub enable_coarse_memory: bool,
129    /// Enable fine-grained memory
130    pub enable_fine_memory: bool,
131    /// Enable memory pools
132    pub enable_memory_pools: bool,
133    /// Enable async memory operations
134    pub enable_async_ops: bool,
135    /// Memory pool growth size
136    pub pool_growth_size: usize,
137    /// Enable host-visible device memory
138    pub enable_host_visible: bool,
139    /// Enable device coherent memory
140    pub enable_device_coherent: bool,
141    /// Maximum number of streams
142    pub max_streams: u32,
143    /// Enable GPU memory profiling
144    pub enable_profiling: bool,
145}
146
147impl Default for RocmConfig {
148    fn default() -> Self {
149        Self {
150            device_id: 0,
151            enable_coarse_memory: true,
152            enable_fine_memory: true,
153            enable_memory_pools: true,
154            enable_async_ops: true,
155            pool_growth_size: 64 * 1024 * 1024, // 64MB
156            enable_host_visible: true,
157            enable_device_coherent: false,
158            max_streams: 16,
159            enable_profiling: false,
160        }
161    }
162}
163
164/// ROCm device properties
165#[derive(Debug, Clone)]
166pub struct RocmDeviceProperties {
167    pub device_id: u32,
168    pub name: String,
169    pub arch: String,
170    pub gcn_arch_name: String,
171    pub total_global_memory: usize,
172    pub local_memory_size: usize,
173    pub max_work_group_size: u32,
174    pub max_work_item_dimensions: u32,
175    pub max_work_item_sizes: [u32; 3],
176    pub compute_units: u32,
177    pub wavefront_size: u32,
178    pub memory_clock_frequency: u32,
179    pub memory_bus_width: u32,
180    pub l2_cache_size: usize,
181    pub max_constant_buffer_size: usize,
182    pub pci_bus_id: u32,
183    pub pci_device_id: u32,
184    pub supports_cooperative_launch: bool,
185    pub supports_dynamic_parallelism: bool,
186}
187
188/// ROCm memory types
189#[derive(Debug, Clone, PartialEq, Eq, Hash)]
190pub enum RocmMemoryType {
191    Device,
192    Host,
193    HostVisible,
194    DeviceCoherent,
195    CoarseGrained,
196    FineGrained,
197}
198
199/// HIP context for managing device state
200pub struct HipContext {
201    /// Context handle (simulated)
202    pub handle: *mut c_void,
203    /// Device ID
204    pub device_id: u32,
205    /// Context flags
206    pub flags: HipContextFlags,
207    /// Creation time
208    pub created_at: Instant,
209    /// Active streams
210    pub streams: Vec<HipStream>,
211    /// Device memory info
212    pub memory_info: HipMemoryInfo,
213}
214
215/// HIP context creation flags
216#[derive(Debug, Clone)]
217pub struct HipContextFlags {
218    pub sched_auto: bool,
219    pub sched_spin: bool,
220    pub sched_yield: bool,
221    pub sched_blocking_sync: bool,
222    pub map_host: bool,
223}
224
225impl Default for HipContextFlags {
226    fn default() -> Self {
227        Self {
228            sched_auto: true,
229            sched_spin: false,
230            sched_yield: false,
231            sched_blocking_sync: false,
232            map_host: false,
233        }
234    }
235}
236
237/// HIP memory information
238#[derive(Debug, Clone)]
239pub struct HipMemoryInfo {
240    pub total_memory: usize,
241    pub free_memory: usize,
242    pub used_memory: usize,
243    pub coarse_memory: usize,
244    pub fine_memory: usize,
245}
246
247/// HIP stream for asynchronous operations
248pub struct HipStream {
249    /// Stream handle (simulated)
250    pub handle: *mut c_void,
251    /// Stream ID
252    pub id: u32,
253    /// Stream priority
254    pub priority: i32,
255    /// Stream flags
256    pub flags: HipStreamFlags,
257    /// Creation time
258    pub created_at: Instant,
259    /// Operations queue
260    pub operations: std::collections::VecDeque<HipOperation>,
261}
262
263/// HIP stream flags
264#[derive(Debug, Clone)]
265pub struct HipStreamFlags {
266    pub default: bool,
267    pub non_blocking: bool,
268    pub per_thread: bool,
269}
270
271impl Default for HipStreamFlags {
272    fn default() -> Self {
273        Self {
274            default: true,
275            non_blocking: false,
276            per_thread: false,
277        }
278    }
279}
280
281/// HIP asynchronous operation
282#[derive(Debug, Clone)]
283pub struct HipOperation {
284    pub op_type: HipOperationType,
285    pub src_ptr: Option<*mut c_void>,
286    pub dst_ptr: Option<*mut c_void>,
287    pub size: usize,
288    pub timestamp: Instant,
289}
290
291/// Types of HIP operations
292#[derive(Debug, Clone)]
293pub enum HipOperationType {
294    MemcpyHostToDevice,
295    MemcpyDeviceToHost,
296    MemcpyDeviceToDevice,
297    MemcpyAsync,
298    MemsetAsync,
299    KernelLaunch,
300    EventRecord,
301    EventSynchronize,
302}
303
304/// ROCm memory pool
305pub struct RocmMemoryPool {
306    /// Memory type
307    memory_type: RocmMemoryType,
308    /// Current size
309    current_size: usize,
310    /// Maximum size
311    max_size: usize,
312    /// Used size
313    used_size: usize,
314    /// Free blocks
315    free_blocks: std::collections::VecDeque<RocmMemoryBlock>,
316    /// Allocated blocks
317    allocated_blocks: HashMap<*mut c_void, RocmMemoryBlock>,
318    /// Memory attributes
319    attributes: RocmMemoryAttributes,
320}
321
322/// ROCm memory block
323#[derive(Debug, Clone)]
324pub struct RocmMemoryBlock {
325    pub ptr: *mut c_void,
326    pub size: usize,
327    pub memory_type: RocmMemoryType,
328    pub allocated_at: Instant,
329    pub last_access: Option<Instant>,
330    pub ref_count: u32,
331    pub agent_accessible: bool,
332}
333
334/// ROCm memory attributes
335#[derive(Debug, Clone)]
336pub struct RocmMemoryAttributes {
337    pub is_coarse_grained: bool,
338    pub is_fine_grained: bool,
339    pub is_host_accessible: bool,
340    pub is_device_accessible: bool,
341    pub is_coherent: bool,
342    pub numa_node: Option<u32>,
343}
344
345impl Default for RocmMemoryAttributes {
346    fn default() -> Self {
347        Self {
348            is_coarse_grained: true,
349            is_fine_grained: false,
350            is_host_accessible: false,
351            is_device_accessible: true,
352            is_coherent: false,
353            numa_node: None,
354        }
355    }
356}
357
358impl RocmMemoryPool {
359    pub fn new(memory_type: RocmMemoryType, max_size: usize) -> Self {
360        let attributes = match memory_type {
361            RocmMemoryType::CoarseGrained => RocmMemoryAttributes {
362                is_coarse_grained: true,
363                is_fine_grained: false,
364                is_host_accessible: false,
365                is_device_accessible: true,
366                is_coherent: false,
367                numa_node: None,
368            },
369            RocmMemoryType::FineGrained => RocmMemoryAttributes {
370                is_coarse_grained: false,
371                is_fine_grained: true,
372                is_host_accessible: true,
373                is_device_accessible: true,
374                is_coherent: true,
375                numa_node: Some(0),
376            },
377            RocmMemoryType::HostVisible => RocmMemoryAttributes {
378                is_coarse_grained: false,
379                is_fine_grained: false,
380                is_host_accessible: true,
381                is_device_accessible: true,
382                is_coherent: false,
383                numa_node: None,
384            },
385            _ => RocmMemoryAttributes::default(),
386        };
387
388        Self {
389            memory_type,
390            current_size: 0,
391            max_size,
392            used_size: 0,
393            free_blocks: std::collections::VecDeque::new(),
394            allocated_blocks: HashMap::new(),
395            attributes,
396        }
397    }
398
399    /// Allocate from pool
400    pub fn allocate(&mut self, size: usize) -> Result<*mut c_void, RocmError> {
401        // Try to find suitable free block
402        for i in 0..self.free_blocks.len() {
403            if self.free_blocks[i].size >= size {
404                let Some(mut block) = self.free_blocks.remove(i) else {
405                    continue;
406                };
407
408                // Split block if much larger
409                if block.size > size * 2 {
410                    let remaining_block = RocmMemoryBlock {
411                        ptr: unsafe { block.ptr.add(size) },
412                        size: block.size - size,
413                        memory_type: block.memory_type.clone(),
414                        allocated_at: block.allocated_at,
415                        last_access: None,
416                        ref_count: 0,
417                        agent_accessible: block.agent_accessible,
418                    };
419                    self.free_blocks.push_back(remaining_block);
420                    block.size = size;
421                }
422
423                block.last_access = Some(Instant::now());
424                block.ref_count = 1;
425
426                let ptr = block.ptr;
427                self.allocated_blocks.insert(ptr, block);
428                self.used_size += size;
429
430                return Ok(ptr);
431            }
432        }
433
434        // Need to allocate new memory
435        if self.current_size + size > self.max_size {
436            return Err(RocmError::OutOfMemory(
437                "Pool size limit exceeded".to_string(),
438            ));
439        }
440
441        let ptr = self.hip_malloc(size)?;
442        let block = RocmMemoryBlock {
443            ptr,
444            size,
445            memory_type: self.memory_type.clone(),
446            allocated_at: Instant::now(),
447            last_access: Some(Instant::now()),
448            ref_count: 1,
449            agent_accessible: self.attributes.is_device_accessible,
450        };
451
452        self.allocated_blocks.insert(ptr, block);
453        self.current_size += size;
454        self.used_size += size;
455
456        Ok(ptr)
457    }
458
459    /// Free back to pool
460    pub fn free(&mut self, ptr: *mut c_void) -> Result<(), RocmError> {
461        if let Some(block) = self.allocated_blocks.remove(&ptr) {
462            self.used_size -= block.size;
463
464            // Add to free blocks
465            self.free_blocks.push_back(RocmMemoryBlock {
466                ptr: block.ptr,
467                size: block.size,
468                memory_type: block.memory_type,
469                allocated_at: block.allocated_at,
470                last_access: None,
471                ref_count: 0,
472                agent_accessible: block.agent_accessible,
473            });
474
475            // Try to coalesce adjacent blocks
476            self.coalesce_free_blocks();
477
478            Ok(())
479        } else {
480            Err(RocmError::InvalidPointer(
481                "Pointer not found in pool".to_string(),
482            ))
483        }
484    }
485
486    fn coalesce_free_blocks(&mut self) {
487        // Sort free blocks by address
488        let mut blocks: Vec<RocmMemoryBlock> = self.free_blocks.drain(..).collect();
489        blocks.sort_by_key(|block| block.ptr as usize);
490
491        let mut coalesced = Vec::new();
492        let mut current_block: Option<RocmMemoryBlock> = None;
493
494        for block in blocks {
495            match current_block.take() {
496                None => current_block = Some(block),
497                Some(mut prev_block) => {
498                    let prev_end = prev_block.ptr as usize + prev_block.size;
499                    let block_start = block.ptr as usize;
500
501                    if prev_end == block_start && prev_block.memory_type == block.memory_type {
502                        // Coalesce blocks
503                        prev_block.size += block.size;
504                        current_block = Some(prev_block);
505                    } else {
506                        coalesced.push(prev_block);
507                        current_block = Some(block);
508                    }
509                }
510            }
511        }
512
513        if let Some(block) = current_block {
514            coalesced.push(block);
515        }
516
517        self.free_blocks = coalesced.into();
518    }
519
520    fn hip_malloc(&self, size: usize) -> Result<*mut c_void, RocmError> {
521        // Simulate HIP memory allocation
522        match self.memory_type {
523            RocmMemoryType::Device => sim_alloc(size), // hipMalloc equivalent
524            RocmMemoryType::Host => sim_alloc(size),   // hipMallocHost equivalent
525            RocmMemoryType::CoarseGrained => sim_alloc(size), // coarse-grained device memory
526            RocmMemoryType::FineGrained => sim_alloc(size), // fine-grained system memory
527            RocmMemoryType::HostVisible => sim_alloc(size), // host-visible device memory
528            _ => Err(RocmError::UnsupportedOperation(
529                "Unsupported memory type for allocation".to_string(),
530            )),
531        }
532    }
533}
534
535/// HIP stream manager
536pub struct HipStreamManager {
537    /// Available streams
538    streams: Vec<HipStream>,
539    /// Stream pool for reuse
540    stream_pool: std::collections::VecDeque<HipStream>,
541    /// Next stream ID
542    next_stream_id: u32,
543    /// Configuration
544    config: HipStreamConfig,
545}
546
547/// Stream manager configuration
548#[derive(Debug, Clone)]
549pub struct HipStreamConfig {
550    pub default_priority: i32,
551    pub enable_priorities: bool,
552    pub max_operations_per_stream: usize,
553}
554
555impl Default for HipStreamConfig {
556    fn default() -> Self {
557        Self {
558            default_priority: 0,
559            enable_priorities: true,
560            max_operations_per_stream: 1000,
561        }
562    }
563}
564
565impl HipStreamManager {
566    pub fn new(config: HipStreamConfig) -> Self {
567        Self {
568            streams: Vec::new(),
569            stream_pool: std::collections::VecDeque::new(),
570            next_stream_id: 0,
571            config,
572        }
573    }
574
575    /// Create new stream
576    /// Create new stream
577    ///
578    /// Reuses a previously [`Self::destroy_stream`]d stream from
579    /// `stream_pool` when one is available (its operation queue is cleared
580    /// and it is given a fresh ID) instead of always allocating a new one.
581    pub fn create_stream(&mut self, priority: Option<i32>) -> Result<u32, RocmError> {
582        let stream_id = self.next_stream_id;
583        self.next_stream_id += 1;
584
585        let mut stream = self.stream_pool.pop_front().unwrap_or_else(|| HipStream {
586            handle: std::ptr::null_mut(), // Would be actual HIP stream
587            id: stream_id,
588            priority: priority.unwrap_or(self.config.default_priority),
589            flags: HipStreamFlags::default(),
590            created_at: Instant::now(),
591            operations: std::collections::VecDeque::new(),
592        });
593        stream.id = stream_id;
594        stream.priority = priority.unwrap_or(self.config.default_priority);
595        stream.created_at = Instant::now();
596        stream.operations.clear();
597
598        self.streams.push(stream);
599        Ok(stream_id)
600    }
601
602    /// Destroy stream
603    ///
604    /// Returns the stream to `stream_pool` for [`Self::create_stream`] to
605    /// reuse instead of dropping it outright.
606    pub fn destroy_stream(&mut self, stream_id: u32) -> Result<(), RocmError> {
607        if let Some(pos) = self.streams.iter().position(|s| s.id == stream_id) {
608            let stream = self.streams.remove(pos);
609            self.stream_pool.push_back(stream);
610            Ok(())
611        } else {
612            Err(RocmError::InvalidStream("Stream not found".to_string()))
613        }
614    }
615
616    /// Add operation to stream
617    pub fn add_operation(
618        &mut self,
619        stream_id: u32,
620        operation: HipOperation,
621    ) -> Result<(), RocmError> {
622        if let Some(stream) = self.streams.iter_mut().find(|s| s.id == stream_id) {
623            if stream.operations.len() >= self.config.max_operations_per_stream {
624                return Err(RocmError::StreamFull(
625                    "Stream operation queue is full".to_string(),
626                ));
627            }
628
629            stream.operations.push_back(operation);
630            Ok(())
631        } else {
632            Err(RocmError::InvalidStream("Stream not found".to_string()))
633        }
634    }
635
636    /// Synchronize stream
637    pub fn synchronize_stream(&mut self, stream_id: u32) -> Result<(), RocmError> {
638        // First, collect all operations from the stream
639        let mut operations = Vec::new();
640        if let Some(stream) = self.streams.iter_mut().find(|s| s.id == stream_id) {
641            while let Some(operation) = stream.operations.pop_front() {
642                operations.push(operation);
643            }
644        } else {
645            return Err(RocmError::InvalidStream("Stream not found".to_string()));
646        }
647
648        // Now execute all operations
649        for operation in operations {
650            self.execute_operation(operation)?;
651        }
652
653        Ok(())
654    }
655
656    fn execute_operation(&self, _operation: HipOperation) -> Result<(), RocmError> {
657        // Host-memory simulation (see module docs): there is no real ROCm/HIP
658        // device to transfer to or from, so no data movement happens here.
659        // This used to also inject an artificial `std::thread::sleep` per
660        // operation type to mimic device-transfer latency; that fake timing
661        // has been removed rather than left as an undisclosed simulated
662        // number, so callers now see the true (near-zero) cost of this
663        // simulation instead of a fabricated one.
664        Ok(())
665    }
666}
667
668/// ROCm statistics
669#[derive(Debug, Clone, Default)]
670pub struct RocmStats {
671    pub total_allocations: u64,
672    pub total_deallocations: u64,
673    pub bytes_allocated: u64,
674    pub bytes_deallocated: u64,
675    pub device_memory_used: usize,
676    pub host_memory_used: usize,
677    pub coarse_grained_used: usize,
678    pub fine_grained_used: usize,
679    pub stream_operations: u64,
680    pub kernel_launches: u64,
681    pub memory_transfers: u64,
682    pub average_allocation_time: Duration,
683    pub peak_memory_usage: usize,
684}
685
686impl RocmMemoryBackend {
687    /// Create new ROCm backend
688    pub fn new(config: RocmConfig) -> Result<Self, RocmError> {
689        // Initialize ROCm device
690        let device_properties = Self::query_device_properties(config.device_id)?;
691
692        // Create memory pools
693        let mut memory_pools = HashMap::new();
694        if config.enable_memory_pools {
695            let pool_size = device_properties.total_global_memory / 4; // Use 1/4 of total memory
696
697            memory_pools.insert(
698                RocmMemoryType::Device,
699                RocmMemoryPool::new(RocmMemoryType::Device, pool_size),
700            );
701            memory_pools.insert(
702                RocmMemoryType::Host,
703                RocmMemoryPool::new(RocmMemoryType::Host, pool_size),
704            );
705
706            if config.enable_coarse_memory {
707                memory_pools.insert(
708                    RocmMemoryType::CoarseGrained,
709                    RocmMemoryPool::new(RocmMemoryType::CoarseGrained, pool_size),
710                );
711            }
712
713            if config.enable_fine_memory {
714                memory_pools.insert(
715                    RocmMemoryType::FineGrained,
716                    RocmMemoryPool::new(RocmMemoryType::FineGrained, pool_size / 2),
717                );
718            }
719
720            if config.enable_host_visible {
721                memory_pools.insert(
722                    RocmMemoryType::HostVisible,
723                    RocmMemoryPool::new(RocmMemoryType::HostVisible, pool_size / 4),
724                );
725            }
726        }
727
728        let stream_manager = HipStreamManager::new(HipStreamConfig::default());
729
730        Ok(Self {
731            config,
732            device_properties,
733            contexts: HashMap::new(),
734            memory_pools,
735            stats: RocmStats::default(),
736            stream_manager,
737        })
738    }
739
740    /// Query device properties
741    fn query_device_properties(device_id: u32) -> Result<RocmDeviceProperties, RocmError> {
742        // Simulate querying ROCm device properties
743        Ok(RocmDeviceProperties {
744            device_id,
745            name: format!("AMD GPU {}", device_id),
746            arch: "gfx906".to_string(), // Vega architecture
747            gcn_arch_name: "Vega20".to_string(),
748            total_global_memory: 16 * 1024 * 1024 * 1024, // 16GB
749            local_memory_size: 64 * 1024,                 // 64KB
750            max_work_group_size: 1024,
751            max_work_item_dimensions: 3,
752            max_work_item_sizes: [1024, 1024, 1024],
753            compute_units: 64,
754            wavefront_size: 64,
755            memory_clock_frequency: 1000000, // 1 GHz
756            memory_bus_width: 4096,
757            l2_cache_size: 4 * 1024 * 1024,      // 4MB
758            max_constant_buffer_size: 64 * 1024, // 64KB
759            pci_bus_id: 0x03,
760            pci_device_id: 0x66AF,
761            supports_cooperative_launch: true,
762            supports_dynamic_parallelism: false,
763        })
764    }
765
766    /// Allocate device memory
767    pub fn allocate(
768        &mut self,
769        size: usize,
770        memory_type: RocmMemoryType,
771    ) -> Result<*mut c_void, RocmError> {
772        let start_time = Instant::now();
773
774        let ptr = if self.config.enable_memory_pools {
775            if let Some(pool) = self.memory_pools.get_mut(&memory_type) {
776                pool.allocate(size)?
777            } else {
778                return Err(RocmError::UnsupportedMemoryType(
779                    "Memory type not supported".to_string(),
780                ));
781            }
782        } else {
783            // Direct allocation
784            self.direct_allocate(size, memory_type.clone())?
785        };
786
787        // Update statistics
788        self.stats.total_allocations += 1;
789        self.stats.bytes_allocated += size as u64;
790
791        match memory_type {
792            RocmMemoryType::Device => self.stats.device_memory_used += size,
793            RocmMemoryType::Host => self.stats.host_memory_used += size,
794            RocmMemoryType::CoarseGrained => self.stats.coarse_grained_used += size,
795            RocmMemoryType::FineGrained => self.stats.fine_grained_used += size,
796            _ => {}
797        }
798
799        let allocation_time = start_time.elapsed();
800        let total_time = self.stats.average_allocation_time.as_nanos() as u64
801            * (self.stats.total_allocations - 1)
802            + allocation_time.as_nanos() as u64;
803        self.stats.average_allocation_time =
804            Duration::from_nanos(total_time / self.stats.total_allocations);
805
806        let current_usage = self.stats.device_memory_used
807            + self.stats.host_memory_used
808            + self.stats.coarse_grained_used
809            + self.stats.fine_grained_used;
810        if current_usage > self.stats.peak_memory_usage {
811            self.stats.peak_memory_usage = current_usage;
812        }
813
814        Ok(ptr)
815    }
816
817    fn direct_allocate(
818        &self,
819        size: usize,
820        memory_type: RocmMemoryType,
821    ) -> Result<*mut c_void, RocmError> {
822        // Simulate direct HIP allocation
823        match memory_type {
824            RocmMemoryType::Device => sim_alloc(size), // hipMalloc
825            RocmMemoryType::Host => sim_alloc(size),   // hipMallocHost
826            RocmMemoryType::CoarseGrained => sim_alloc(size), // coarse-grained device memory
827            RocmMemoryType::FineGrained => sim_alloc(size), // fine-grained system memory
828            _ => Err(RocmError::UnsupportedMemoryType(
829                "Unsupported memory type".to_string(),
830            )),
831        }
832    }
833
834    /// Free device memory
835    pub fn free(&mut self, ptr: *mut c_void, memory_type: RocmMemoryType) -> Result<(), RocmError> {
836        if self.config.enable_memory_pools {
837            if let Some(pool) = self.memory_pools.get_mut(&memory_type) {
838                pool.free(ptr)?;
839            } else {
840                return Err(RocmError::UnsupportedMemoryType(
841                    "Memory type not supported".to_string(),
842                ));
843            }
844        } else {
845            // Direct deallocation. `ptr` was returned by `sim_alloc` via
846            // `hip_malloc`/`direct_allocate` above, and this is the first
847            // time it is freed.
848            sim_dealloc(ptr);
849        }
850
851        self.stats.total_deallocations += 1;
852        Ok(())
853    }
854
855    /// Copy memory
856    pub fn memcpy(
857        &mut self,
858        dst: *mut c_void,
859        src: *const c_void,
860        size: usize,
861        kind: RocmMemcpyKind,
862    ) -> Result<(), RocmError> {
863        let operation = HipOperation {
864            op_type: match kind {
865                RocmMemcpyKind::HostToDevice => HipOperationType::MemcpyHostToDevice,
866                RocmMemcpyKind::DeviceToHost => HipOperationType::MemcpyDeviceToHost,
867                RocmMemcpyKind::DeviceToDevice => HipOperationType::MemcpyDeviceToDevice,
868                RocmMemcpyKind::HostToHost => HipOperationType::MemcpyAsync,
869            },
870            src_ptr: Some(src as *mut c_void),
871            dst_ptr: Some(dst),
872            size,
873            timestamp: Instant::now(),
874        };
875
876        // Execute synchronously for now
877        self.stream_manager.execute_operation(operation)?;
878        self.stats.memory_transfers += 1;
879
880        Ok(())
881    }
882
883    /// Asynchronous memory copy
884    pub fn memcpy_async(
885        &mut self,
886        dst: *mut c_void,
887        src: *const c_void,
888        size: usize,
889        kind: RocmMemcpyKind,
890        stream_id: u32,
891    ) -> Result<(), RocmError> {
892        let operation = HipOperation {
893            // Mirrors the synchronous `memcpy`'s mapping above so the queued
894            // operation's recorded direction matches what the caller asked
895            // for instead of always reporting a generic `MemcpyAsync`.
896            op_type: match kind {
897                RocmMemcpyKind::HostToDevice => HipOperationType::MemcpyHostToDevice,
898                RocmMemcpyKind::DeviceToHost => HipOperationType::MemcpyDeviceToHost,
899                RocmMemcpyKind::DeviceToDevice => HipOperationType::MemcpyDeviceToDevice,
900                RocmMemcpyKind::HostToHost => HipOperationType::MemcpyAsync,
901            },
902            src_ptr: Some(src as *mut c_void),
903            dst_ptr: Some(dst),
904            size,
905            timestamp: Instant::now(),
906        };
907
908        self.stream_manager.add_operation(stream_id, operation)?;
909        Ok(())
910    }
911
912    /// Create HIP context
913    pub fn create_context(&mut self, flags: HipContextFlags) -> Result<u32, RocmError> {
914        let context_id = self.contexts.len() as u32;
915
916        let memory_info = HipMemoryInfo {
917            total_memory: self.device_properties.total_global_memory,
918            free_memory: self.device_properties.total_global_memory - self.stats.device_memory_used,
919            used_memory: self.stats.device_memory_used,
920            coarse_memory: self.stats.coarse_grained_used,
921            fine_memory: self.stats.fine_grained_used,
922        };
923
924        let context = HipContext {
925            handle: std::ptr::null_mut(), // Would be actual HIP context
926            device_id: self.config.device_id,
927            flags,
928            created_at: Instant::now(),
929            streams: Vec::new(),
930            memory_info,
931        };
932
933        self.contexts.insert(context_id, context);
934        Ok(context_id)
935    }
936
937    /// Get device properties
938    pub fn get_device_properties(&self) -> &RocmDeviceProperties {
939        &self.device_properties
940    }
941
942    /// Get statistics
943    pub fn get_stats(&self) -> &RocmStats {
944        &self.stats
945    }
946
947    /// Synchronize device
948    pub fn device_synchronize(&mut self) -> Result<(), RocmError> {
949        // Synchronize all streams
950        let stream_ids: Vec<u32> = self.stream_manager.streams.iter().map(|s| s.id).collect();
951        for stream_id in stream_ids {
952            self.stream_manager.synchronize_stream(stream_id)?;
953        }
954        Ok(())
955    }
956
957    /// Create stream
958    pub fn create_stream(&mut self, priority: Option<i32>) -> Result<u32, RocmError> {
959        self.stream_manager.create_stream(priority)
960    }
961
962    /// Destroy stream
963    pub fn destroy_stream(&mut self, stream_id: u32) -> Result<(), RocmError> {
964        self.stream_manager.destroy_stream(stream_id)
965    }
966
967    /// Query memory attributes
968    ///
969    /// Looks up which pool actually allocated `ptr` and returns that pool's
970    /// real attributes (which vary by [`RocmMemoryType`] — see the
971    /// attribute construction in [`RocmMemoryPool::new`]) instead of an
972    /// unconditional default. A pointer this backend never allocated is an
973    /// honest `Err`, not a guess.
974    pub fn query_memory_attributes(
975        &self,
976        ptr: *mut c_void,
977    ) -> Result<RocmMemoryAttributes, RocmError> {
978        self.memory_pools
979            .values()
980            .find(|pool| pool.allocated_blocks.contains_key(&ptr))
981            .map(|pool| pool.attributes.clone())
982            .ok_or_else(|| {
983                RocmError::InvalidPointer("pointer was not allocated by this backend".to_string())
984            })
985    }
986}
987
988// Safety: RocmMemoryBackend manages ROCm/HIP GPU memory pointers via *mut c_void.
989// While raw pointers are not Send/Sync by default, it's safe to share across threads
990// when protected by Arc<Mutex<>> because:
991// 1. All pointers point to HIP GPU memory managed by the ROCm driver
992// 2. The Mutex provides exclusive access for all mutable operations
993// 3. No thread-local state is maintained
994unsafe impl Send for RocmMemoryBackend {}
995unsafe impl Sync for RocmMemoryBackend {}
996
997/// ROCm memory copy kinds
998#[derive(Debug, Clone)]
999pub enum RocmMemcpyKind {
1000    HostToDevice,
1001    DeviceToHost,
1002    DeviceToDevice,
1003    HostToHost,
1004}
1005
1006/// ROCm errors
1007#[derive(Debug, Clone)]
1008pub enum RocmError {
1009    DeviceNotFound(String),
1010    OutOfMemory(String),
1011    InvalidPointer(String),
1012    InvalidStream(String),
1013    StreamFull(String),
1014    UnsupportedOperation(String),
1015    UnsupportedMemoryType(String),
1016    ContextCreationFailed(String),
1017    KernelLaunchFailed(String),
1018    SynchronizationFailed(String),
1019    InternalError(String),
1020}
1021
1022impl std::fmt::Display for RocmError {
1023    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1024        match self {
1025            RocmError::DeviceNotFound(msg) => write!(f, "Device not found: {}", msg),
1026            RocmError::OutOfMemory(msg) => write!(f, "Out of memory: {}", msg),
1027            RocmError::InvalidPointer(msg) => write!(f, "Invalid pointer: {}", msg),
1028            RocmError::InvalidStream(msg) => write!(f, "Invalid stream: {}", msg),
1029            RocmError::StreamFull(msg) => write!(f, "Stream full: {}", msg),
1030            RocmError::UnsupportedOperation(msg) => write!(f, "Unsupported operation: {}", msg),
1031            RocmError::UnsupportedMemoryType(msg) => write!(f, "Unsupported memory type: {}", msg),
1032            RocmError::ContextCreationFailed(msg) => write!(f, "Context creation failed: {}", msg),
1033            RocmError::KernelLaunchFailed(msg) => write!(f, "Kernel launch failed: {}", msg),
1034            RocmError::SynchronizationFailed(msg) => write!(f, "Synchronization failed: {}", msg),
1035            RocmError::InternalError(msg) => write!(f, "Internal error: {}", msg),
1036        }
1037    }
1038}
1039
1040impl std::error::Error for RocmError {}
1041
1042/// Thread-safe ROCm backend wrapper
1043pub struct ThreadSafeRocmBackend {
1044    backend: Arc<Mutex<RocmMemoryBackend>>,
1045}
1046
1047impl ThreadSafeRocmBackend {
1048    pub fn new(config: RocmConfig) -> Result<Self, RocmError> {
1049        let backend = RocmMemoryBackend::new(config)?;
1050        Ok(Self {
1051            backend: Arc::new(Mutex::new(backend)),
1052        })
1053    }
1054
1055    pub fn allocate(
1056        &self,
1057        size: usize,
1058        memory_type: RocmMemoryType,
1059    ) -> Result<*mut c_void, RocmError> {
1060        let mut backend = self.backend.lock().unwrap_or_else(|e| e.into_inner());
1061        backend.allocate(size, memory_type)
1062    }
1063
1064    pub fn free(&self, ptr: *mut c_void, memory_type: RocmMemoryType) -> Result<(), RocmError> {
1065        let mut backend = self.backend.lock().unwrap_or_else(|e| e.into_inner());
1066        backend.free(ptr, memory_type)
1067    }
1068
1069    pub fn get_stats(&self) -> RocmStats {
1070        let backend = self.backend.lock().unwrap_or_else(|e| e.into_inner());
1071        backend.get_stats().clone()
1072    }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077    use super::*;
1078
1079    /// Regression test for F26: a zero-size request must not reach
1080    /// `std::alloc::alloc` (unsound for a zero-size layout).
1081    #[test]
1082    fn sim_alloc_zero_size_is_a_safe_sentinel_not_a_ub_call() {
1083        let ptr = sim_alloc(0).expect("zero-size request must succeed");
1084        assert!(!ptr.is_null());
1085        sim_dealloc(ptr);
1086    }
1087
1088    /// A real allocation must be readable/writable across its full size and
1089    /// must free through the same layout it was allocated with.
1090    #[test]
1091    fn sim_alloc_real_allocation_round_trips_and_frees_cleanly() {
1092        for size in [1usize, 7, 256, 4096, 1_000_003] {
1093            let ptr = sim_alloc(size).expect("allocation must succeed") as *mut u8;
1094            assert!(!ptr.is_null());
1095            unsafe {
1096                for i in 0..size {
1097                    ptr.add(i).write(0xAB);
1098                }
1099                for i in 0..size {
1100                    assert_eq!(ptr.add(i).read(), 0xAB);
1101                }
1102                sim_dealloc(ptr as *mut c_void);
1103            }
1104        }
1105    }
1106
1107    #[test]
1108    fn sim_dealloc_null_is_a_no_op() {
1109        sim_dealloc(std::ptr::null_mut());
1110    }
1111
1112    #[test]
1113    fn test_rocm_backend_creation() {
1114        let config = RocmConfig::default();
1115        let backend = RocmMemoryBackend::new(config);
1116        assert!(backend.is_ok());
1117    }
1118
1119    #[test]
1120    fn test_memory_pool() {
1121        let mut pool = RocmMemoryPool::new(RocmMemoryType::CoarseGrained, 1024 * 1024);
1122        let ptr = pool.allocate(1024);
1123        assert!(ptr.is_ok());
1124
1125        let ptr = ptr.expect("unwrap failed");
1126        let result = pool.free(ptr);
1127        assert!(result.is_ok());
1128    }
1129
1130    #[test]
1131    fn test_hip_stream_manager() {
1132        let mut manager = HipStreamManager::new(HipStreamConfig::default());
1133        let stream_id = manager.create_stream(Some(1));
1134        assert!(stream_id.is_ok());
1135
1136        let stream_id = stream_id.expect("unwrap failed");
1137        let result = manager.destroy_stream(stream_id);
1138        assert!(result.is_ok());
1139    }
1140
1141    #[test]
1142    fn test_thread_safe_backend() {
1143        let config = RocmConfig::default();
1144        let backend = ThreadSafeRocmBackend::new(config);
1145        assert!(backend.is_ok());
1146
1147        let backend = backend.expect("unwrap failed");
1148        let stats = backend.get_stats();
1149        assert_eq!(stats.total_allocations, 0);
1150    }
1151}