Skip to main content

hrx/fabric/
mod.rs

1//! Native AMD device ownership through libamdf's versioned C ABI.
2//! Discovery is passive; only opening a device activates hardware.
3#[allow(
4    non_camel_case_types,
5    non_snake_case,
6    non_upper_case_globals,
7    dead_code,
8    missing_docs,
9    unsafe_op_in_unsafe_fn,
10    clippy::all
11)]
12mod ffi;
13use crate::{Error, Result, Target};
14use ffi::*;
15use std::{ffi::CStr, path::Path, ptr, sync::Arc};
16
17fn check(operation: &'static str, status: amdf_status_t) -> Result<()> {
18    if status == 0 {
19        return Ok(());
20    }
21    let domain = (status >> 32) as u32;
22    let code = status as u32;
23    Err(Error::Backend {
24        backend: "libamdf",
25        operation,
26        code: code as i32,
27        message: format!("native status domain {domain}, code {code}").into_boxed_str(),
28    })
29}
30fn missing(name: &str) -> Error {
31    Error::Unsupported(format!("libamdf ABI is missing {name}"))
32}
33macro_rules! entry {
34    ($table:expr, $name:ident) => {
35        $table.$name.ok_or_else(|| missing(stringify!($name)))?
36    };
37}
38
39#[cfg(target_os = "linux")]
40mod storage;
41#[cfg(target_os = "linux")]
42pub use storage::{StorageExecution, StorageLayout, StorageOptions, StorageProgress, StorageRing};
43mod resident;
44pub use resident::{ResidentGpu, ResidentSession, ResidentStartup};
45mod xdna;
46pub use xdna::{XdnaBinding, XdnaCompletion, XdnaProgram};
47pub(crate) mod profile;
48pub use profile::{DeviceInterval, DeviceProfile, ProfiledGpu};
49mod queue;
50pub(crate) use queue::GraphArena;
51pub use queue::{Completion, PreparedGpu, Queue};
52mod sanitizer;
53pub use sanitizer::{
54    SanitizerAccess, SanitizerAddress, SanitizerCheck, SanitizerRace, SanitizerReport,
55    SanitizerReports, SanitizerRuntimeOptions, SanitizerSite,
56};
57mod executable;
58pub use executable::{Argument, Kernel};
59mod aql;
60pub use aql::{AqlCompletion, AqlQueue, PreparedAql};
61mod capabilities;
62pub use capabilities::{QueueCapabilities, QueueCommand};
63mod visibility;
64pub use visibility::{
65    AtomicScope, CacheExecutor, CacheOperation, CacheTransition, MemorySite, MemoryVisibility,
66    PreparedHostVisibility, TransitionKind, VisibilityFacts,
67};
68mod memory;
69pub use memory::{AllocationProfile, Buffer, ProspectiveVisibility};
70
71struct Api {
72    core: &'static amdf_api_t,
73    gpu: &'static amdf_gpu_api_t,
74    xdna: &'static amdf_xdna_api_t,
75    // Every native owner retains this mapping through its last native call.
76    _library: Amdf,
77    directory: std::path::PathBuf,
78    bridge: executable::BridgeSlot,
79}
80impl Api {
81    unsafe fn load(path: &Path) -> Result<Self> {
82        let library = unsafe { Amdf::new(path)? };
83        let mut core = ptr::null();
84        check("query_api", unsafe {
85            library.amdf_query_api(HRX_AMDF_ABI_VERSION, HRX_AMDF_ABI_VERSION, &mut core)
86        })?;
87        if core.is_null() {
88            return Err(missing("core table"));
89        }
90        unsafe {
91            validate_table(core.cast(), size_of::<amdf_api_t>(), HRX_AMDF_ABI_VERSION)?;
92        }
93        let core = unsafe { &*core };
94        let mut gpu = ptr::null();
95        let mut xdna = ptr::null();
96        check("query_extension(GPU)", unsafe {
97            entry!(core, query_extension)(
98                AMDF_EXTENSION_GPU,
99                AMDF_GPU_EXTENSION_VERSION_LATEST,
100                AMDF_GPU_EXTENSION_VERSION_LATEST,
101                &mut gpu,
102            )
103        })?;
104        check("query_extension(XDNA)", unsafe {
105            entry!(core, query_extension)(
106                AMDF_EXTENSION_XDNA,
107                AMDF_XDNA_EXTENSION_VERSION_LATEST,
108                AMDF_XDNA_EXTENSION_VERSION_LATEST,
109                &mut xdna,
110            )
111        })?;
112        if gpu.is_null() || xdna.is_null() {
113            return Err(missing("device extension table"));
114        }
115        unsafe {
116            validate_table(
117                gpu,
118                size_of::<amdf_gpu_api_t>(),
119                AMDF_GPU_EXTENSION_VERSION_LATEST,
120            )?;
121            validate_table(
122                xdna,
123                size_of::<amdf_xdna_api_t>(),
124                AMDF_XDNA_EXTENSION_VERSION_LATEST,
125            )?;
126        }
127        let gpu = unsafe { &*gpu.cast::<amdf_gpu_api_t>() };
128        let xdna = unsafe { &*xdna.cast::<amdf_xdna_api_t>() };
129        validate_bridge_api(core, xdna)?;
130        Ok(Self {
131            core,
132            gpu,
133            xdna,
134            _library: library,
135            directory: path
136                .parent()
137                .unwrap_or_else(|| Path::new("."))
138                .to_path_buf(),
139            bridge: Default::default(),
140        })
141    }
142}
143
144// Query results guarantee a two-word header, not the requested full structure.
145unsafe fn validate_table(
146    pointer: *const std::ffi::c_void,
147    bytes: usize,
148    version: u32,
149) -> Result<()> {
150    if pointer.is_null() {
151        return Err(missing("table header"));
152    }
153    let header = unsafe { pointer.cast::<[u32; 2]>().read() };
154    if (header[0] as usize) < bytes || header[1] != version {
155        return Err(missing("complete table with the requested version"));
156    }
157    Ok(())
158}
159// C adapters borrow these tables and call the entries directly. Validate them
160// before any native ownership is acquired, including error-path destructors.
161fn validate_bridge_api(core: &amdf_api_t, xdna: &amdf_xdna_api_t) -> Result<()> {
162    let _ = entry!(core, endpoint_query_info);
163    let _ = entry!(core, endpoint_query_queue_family_info);
164    let _ = entry!(core, host_mapping_cache_control);
165    let _ = entry!(core, host_mapping_destroy);
166    let _ = entry!(core, host_mapping_query_info);
167    let _ = entry!(core, instance_enumerate_memory_scopes);
168    let _ = entry!(core, kernel_queue_destroy);
169    let _ = entry!(core, kernel_queue_wait);
170    let _ = entry!(core, memory_create);
171    let _ = entry!(core, memory_destroy);
172    let _ = entry!(core, memory_map);
173    let _ = entry!(core, memory_query_address);
174    let _ = entry!(core, memory_scope_query_device_profile);
175    let _ = entry!(core, memory_scope_query_info);
176    let _ = entry!(xdna, context_create);
177    let _ = entry!(xdna, context_destroy);
178    let _ = entry!(xdna, context_enumerate_memory_scopes);
179    let _ = entry!(xdna, device_query_info);
180    let _ = entry!(xdna, endpoint_query_info);
181    let _ = entry!(xdna, kernel_queue_create);
182    let _ = entry!(xdna, kernel_queue_submit);
183    Ok(())
184}
185
186/// Native driver ownership policy selected before discovery.
187#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
188pub enum NativeLifetime {
189    /// Driver resources are scoped to the explicit fabric instance.
190    #[default]
191    Instance,
192    /// Use the provider's process-lifetime native domain, enabling its
193    /// process-scoped services such as KFD host registration.
194    Process,
195}
196/// One provider instance and its passive memory/discovery domain.
197#[derive(Clone)]
198pub struct Fabric(Arc<Instance>);
199struct Instance {
200    api: Arc<Api>,
201    raw: *mut amdf_instance_t,
202}
203// Instance operations used here are documented as thread-safe. Arc ownership
204// prevents destruction while an endpoint, device, or allocation remains alive.
205unsafe impl Send for Instance {}
206unsafe impl Sync for Instance {}
207impl Drop for Instance {
208    fn drop(&mut self) {
209        let status = unsafe { self.api.core.instance_destroy.unwrap()(self.raw) };
210        if status != 0 {
211            std::mem::forget(self.api.clone());
212        }
213    }
214}
215
216/// Native engine type.
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218pub enum Engine {
219    /// RDNA GPU.
220    Gpu,
221    /// Native XDNA array.
222    Xdna,
223}
224
225/// A passive endpoint snapshot bound to its owning fabric.
226#[derive(Clone)]
227pub struct Endpoint(Arc<EndpointInner>);
228struct EndpointInner {
229    instance: Arc<Instance>,
230    raw: *mut amdf_endpoint_t,
231    name: String,
232    engine: Engine,
233    target: Target,
234}
235unsafe impl Send for EndpointInner {}
236unsafe impl Sync for EndpointInner {}
237impl Drop for EndpointInner {
238    fn drop(&mut self) {
239        let status = unsafe { self.instance.api.core.endpoint_close.unwrap()(self.raw) };
240        if status != 0 {
241            std::mem::forget(self.instance.clone());
242        }
243    }
244}
245impl Endpoint {
246    /// Human-readable passive device name.
247    pub fn name(&self) -> &str {
248        &self.0.name
249    }
250    /// Exact compiler profile required by this endpoint.
251    pub fn target(&self) -> &Target {
252        &self.0.target
253    }
254    /// Engine selected without activating the device.
255    pub fn engine(&self) -> Engine {
256        self.0.engine
257    }
258    /// Activate this exact endpoint. Unsupported hardware is never substituted.
259    pub fn open(&self) -> Result<Device> {
260        if self.target().as_str() != "gfx1151" && !self.target().is_xdna() {
261            return Err(Error::Unsupported(format!(
262                "unqualified device {}",
263                self.target().as_str()
264            )));
265        }
266        let api = &self.0.instance.api;
267        // Validate the destructor before taking ownership of a native handle.
268        let _ = entry!(api.core, device_destroy);
269        let mut raw = ptr::null_mut();
270        unsafe {
271            match self.engine() {
272                Engine::Gpu => {
273                    let options = amdf_gpu_device_create_info_t {
274                        type_: HRX_AMDF_STRUCTURE_TYPE_GPU_DEVICE_CREATE_INFO,
275                        structure_size: size_of::<amdf_gpu_device_create_info_t>() as u32,
276                        ..Default::default()
277                    };
278                    check(
279                        "gpu.device_create",
280                        entry!(api.gpu, device_create)(self.0.raw, &options, &mut raw),
281                    )?;
282                }
283                Engine::Xdna => {
284                    let options = amdf_xdna_device_create_info_t {
285                        type_: HRX_AMDF_STRUCTURE_TYPE_XDNA_DEVICE_CREATE_INFO,
286                        structure_size: size_of::<amdf_xdna_device_create_info_t>() as u32,
287                        ..Default::default()
288                    };
289                    check(
290                        "xdna.device_create",
291                        entry!(api.xdna, device_create)(self.0.raw, &options, &mut raw),
292                    )?;
293                }
294            }
295        }
296        if raw.is_null() {
297            return Err(missing("created device"));
298        }
299        Ok(Device(Arc::new(DeviceInner {
300            endpoint: self.clone(),
301            raw,
302            stream_queues: Default::default(),
303        })))
304    }
305}
306
307/// An activated native address/execution domain, independent of any program.
308#[derive(Clone)]
309pub struct Device(Arc<DeviceInner>);
310struct DeviceInner {
311    endpoint: Endpoint,
312    raw: *mut amdf_device_t,
313    stream_queues: std::sync::Mutex<queue::StreamQueues>,
314}
315unsafe impl Send for DeviceInner {}
316unsafe impl Sync for DeviceInner {}
317impl Drop for DeviceInner {
318    fn drop(&mut self) {
319        let api = &self.endpoint.0.instance.api;
320        let status = unsafe { api.core.device_destroy.unwrap()(self.raw) };
321        if status != 0 {
322            std::mem::forget(self.endpoint.clone());
323        }
324    }
325}
326impl Device {
327    /// Passive identity retained by the activated device.
328    pub fn endpoint(&self) -> &Endpoint {
329        &self.0.endpoint
330    }
331    /// Exact target for offline compilation and executable admission.
332    pub fn target(&self) -> &Target {
333        self.endpoint().target()
334    }
335}
336
337impl Fabric {
338    /// Load a selected native provider and create an instance without discovery.
339    pub fn load(path: &Path) -> Result<Self> {
340        Self::load_with_lifetime(path, NativeLifetime::Instance)
341    }
342    /// Load a provider with an explicit native lifetime policy.
343    /// Process-lifetime KFD state can survive provider destruction and prevent
344    /// reacquisition. Use `resolve_with_lifetime(Process)` to share a retained
345    /// process provider, or keep this explicit provider alive until process exit.
346    pub fn load_with_lifetime(path: &Path, lifetime: NativeLifetime) -> Result<Self> {
347        let api = Arc::new(unsafe { Api::load(path)? });
348        let _ = entry!(api.core, instance_destroy);
349        let options = amdf_instance_create_info_t {
350            type_: AMDF_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
351            structure_size: size_of::<amdf_instance_create_info_t>() as u32,
352            native_lifetime: match lifetime {
353                NativeLifetime::Instance => AMDF_NATIVE_LIFETIME_INSTANCE,
354                NativeLifetime::Process => AMDF_NATIVE_LIFETIME_PROCESS,
355            },
356            ..Default::default()
357        };
358        let mut raw = ptr::null_mut();
359        check("instance_create", unsafe {
360            entry!(api.core, instance_create)(&options, &mut raw)
361        })?;
362        if raw.is_null() {
363            return Err(missing("created instance"));
364        }
365        Ok(Self(Arc::new(Instance { api, raw })))
366    }
367
368    /// Enumerate current endpoints without activating devices or creating queues.
369    pub fn endpoints(&self) -> Result<Vec<Endpoint>> {
370        let api = &self.0.api;
371        let _ = entry!(api.core, endpoint_close);
372        let mut count = 0;
373        check("endpoint_enumerate", unsafe {
374            entry!(api.core, endpoint_enumerate)(self.0.raw, 0, ptr::null_mut(), &mut count)
375        })?;
376        // Device arrivals can grow the snapshot between calls. Bound retries
377        // instead of assuming the first count remains sufficient.
378        let mut summaries = Vec::new();
379        for attempt in 0..4 {
380            summaries.resize(count as usize, amdf_endpoint_summary_t::default());
381            let status = unsafe {
382                entry!(api.core, endpoint_enumerate)(
383                    self.0.raw,
384                    summaries.len() as u32,
385                    summaries.as_mut_ptr(),
386                    &mut count,
387                )
388            };
389            if status as u32 == AMDF_STATUS_CODE_BUFFER_TOO_SMALL && attempt < 3 {
390                continue;
391            }
392            check("endpoint_enumerate", status)?;
393            summaries.truncate(count as usize);
394            break;
395        }
396        let mut endpoints = Vec::new();
397        for summary in summaries {
398            let mut raw = ptr::null_mut();
399            check("endpoint_open", unsafe {
400                entry!(api.core, endpoint_open)(self.0.raw, &summary.id, &mut raw)
401            })?;
402            if raw.is_null() {
403                return Err(missing("opened endpoint"));
404            }
405            let metadata = (|| -> Result<_> {
406                unsafe {
407                    let mut info = amdf_endpoint_info_t {
408                        type_: AMDF_STRUCTURE_TYPE_ENDPOINT_INFO,
409                        structure_size: size_of::<amdf_endpoint_info_t>() as u32,
410                        ..Default::default()
411                    };
412                    check(
413                        "endpoint_query_info",
414                        entry!(api.core, endpoint_query_info)(raw, &mut info),
415                    )?;
416                    let name = CStr::from_ptr(info.name.as_ptr())
417                        .to_string_lossy()
418                        .into_owned();
419                    let (engine, target) = match info.engine_kind {
420                        AMDF_ENGINE_KIND_GPU => {
421                            let mut gpu = amdf_gpu_endpoint_info_t {
422                                type_: HRX_AMDF_STRUCTURE_TYPE_GPU_ENDPOINT_INFO,
423                                structure_size: size_of::<amdf_gpu_endpoint_info_t>() as u32,
424                                ..Default::default()
425                            };
426                            check(
427                                "gpu.endpoint_query_info",
428                                entry!(api.gpu, endpoint_query_info)(raw, &mut gpu),
429                            )?;
430                            (
431                                Engine::Gpu,
432                                Target::new(&format!(
433                                    "gfx{}{}{:x}",
434                                    gpu.gfx_ip.major, gpu.gfx_ip.minor, gpu.gfx_ip.stepping
435                                ))?,
436                            )
437                        }
438                        AMDF_ENGINE_KIND_XDNA => {
439                            // The first release qualifies only the exact NPU5 deployment.
440                            if info.pci.device_id != 0x17f0 || info.pci.revision_id != 0x11 {
441                                return Err(Error::Unsupported(
442                                    "unqualified XDNA deployment".into(),
443                                ));
444                            }
445                            (Engine::Xdna, Target::xdna())
446                        }
447                        _ => return Err(Error::Unsupported("unknown AMD engine".into())),
448                    };
449                    Ok((name, engine, target))
450                }
451            })();
452            match metadata {
453                Ok((name, engine, target)) => endpoints.push(Endpoint(Arc::new(EndpointInner {
454                    instance: self.0.clone(),
455                    raw,
456                    name,
457                    engine,
458                    target,
459                }))),
460                Err(error) => {
461                    let status = unsafe { api.core.endpoint_close.unwrap()(raw) };
462                    if status != 0 {
463                        std::mem::forget(self.0.clone());
464                    }
465                    return Err(error);
466                }
467            }
468        }
469        Ok(endpoints)
470    }
471}
472
473impl Fabric {
474    /// Resolve the verified native bundle, or an explicit developer library.
475    pub fn resolve() -> Result<Self> {
476        Self::resolve_with_lifetime(NativeLifetime::Instance)
477    }
478    /// Resolve a provider in the requested native lifetime domain. Instance
479    /// providers are weakly cached; the process provider remains alive until
480    /// process exit because KFD's primary VM can outlive explicit destruction.
481    /// Selection never changes after admission within that lifetime domain.
482    pub fn resolve_with_lifetime(lifetime: NativeLifetime) -> Result<Self> {
483        struct Instances {
484            instance: std::sync::Weak<Instance>,
485            process: Option<Arc<Instance>>,
486        }
487        static INSTANCES: std::sync::Mutex<Instances> = std::sync::Mutex::new(Instances {
488            instance: std::sync::Weak::new(),
489            process: None,
490        });
491        let mut slots = INSTANCES
492            .lock()
493            .map_err(|_| Error::Message("fabric registry poisoned".into()))?;
494        let existing = match lifetime {
495            NativeLifetime::Instance => slots.instance.upgrade(),
496            NativeLifetime::Process => slots.process.clone(),
497        };
498        if let Some(instance) = existing {
499            return Ok(Self(instance));
500        }
501        let path = match std::env::var_os("HRX_AMDF_LIBRARY") {
502            Some(path) => std::path::PathBuf::from(path),
503            None => crate::bundle::resolve()?.join("libamdf.so"),
504        };
505        let fabric = Self::load_with_lifetime(&path, lifetime)?;
506        match lifetime {
507            NativeLifetime::Instance => slots.instance = Arc::downgrade(&fabric.0),
508            NativeLifetime::Process => slots.process = Some(fabric.0.clone()),
509        }
510        Ok(fabric)
511    }
512}
513impl Device {
514    /// Open a selected engine ordinal in the shared, weakly cached provider.
515    /// Live buffers keep their exact device identity; the registry owns no device.
516    pub fn open(engine: Engine, index: usize) -> Result<Self> {
517        Self::open_with_lifetime(engine, index, NativeLifetime::Instance)
518    }
519    /// Open in an explicitly selected native ownership domain.
520    pub fn open_with_lifetime(
521        engine: Engine,
522        index: usize,
523        lifetime: NativeLifetime,
524    ) -> Result<Self> {
525        type Devices = Vec<(Engine, usize, NativeLifetime, std::sync::Weak<DeviceInner>)>;
526        static DEVICES: std::sync::Mutex<Devices> = std::sync::Mutex::new(Vec::new());
527        let mut devices = DEVICES
528            .lock()
529            .map_err(|_| Error::Message("device registry poisoned".into()))?;
530        if let Some(device) = devices.iter().find_map(|(kind, ordinal, policy, device)| {
531            (*kind == engine && *ordinal == index && *policy == lifetime)
532                .then(|| device.upgrade())
533                .flatten()
534        }) {
535            return Ok(Self(device));
536        }
537        devices.retain(|(_, _, _, device)| device.strong_count() != 0);
538        let device = Fabric::resolve_with_lifetime(lifetime)?
539            .endpoints()?
540            .into_iter()
541            .filter(|endpoint| endpoint.engine() == engine)
542            .nth(index)
543            .ok_or_else(|| Error::Unsupported(format!("{engine:?} device {index} is unavailable")))?
544            .open()?;
545        devices.push((engine, index, lifetime, Arc::downgrade(&device.0)));
546        Ok(device)
547    }
548    /// Stable identity while this device or any dependent owner is live.
549    pub fn id(&self) -> usize {
550        Arc::as_ptr(&self.0) as usize
551    }
552    /// Allocation and discovery domain retained by this device.
553    pub fn fabric(&self) -> Fabric {
554        Fabric(self.0.endpoint.0.instance.clone())
555    }
556}
557
558#[cfg(test)]
559mod abi_tests {
560    use super::*;
561    #[test]
562    fn reject_truncated_and_mismatched_tables_before_full_reference() {
563        for header in [[8u32, 5], [size_of::<amdf_api_t>() as u32, 4]] {
564            assert!(
565                unsafe { validate_table(header.as_ptr().cast(), size_of::<amdf_api_t>(), 5) }
566                    .is_err()
567            );
568        }
569        assert!(validate_bridge_api(&amdf_api_t::default(), &amdf_xdna_api_t::default()).is_err());
570    }
571}