Skip to main content

hrx/fabric/
executable.rs

1use super::*;
2use std::{ffi::CString, sync::Mutex};
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)]
12#[path = "bridge_ffi.rs"]
13pub(super) mod ffi;
14use ffi::*;
15
16pub(super) fn bridge_check(api: &Bridge, status: i32) -> Result<()> {
17    if status == 0 {
18        return Ok(());
19    }
20    let message = unsafe { CStr::from_ptr(api.hrx_fabric_error()) }.to_string_lossy();
21    Err(Error::Backend {
22        backend: "native executable",
23        operation: "executable",
24        code: status,
25        message: message.into_owned().into_boxed_str(),
26    })
27}
28
29/// One loaded GPU entry and its immutable native code and metadata.
30#[derive(Clone)]
31pub struct Kernel(pub(super) Arc<KernelInner>);
32pub(super) struct KernelInner {
33    pub(super) device: Device,
34    pub(super) code: Buffer,
35    image: Arc<GpuImage>,
36    sanitizer: Option<Arc<super::sanitizer::Feedback>>,
37    feedback_global: std::ops::Range<usize>,
38    instrumentation: Option<ShadowTemplate>,
39    address_state: Option<super::sanitizer::AddressState>,
40    pub(super) race_state: Option<super::sanitizer::RaceState>,
41    pub(super) info: hrx_fabric_gpu_info,
42}
43struct ShadowTemplate {
44    race_global: std::ops::Range<usize>,
45    address_global: std::ops::Range<usize>,
46    options: super::SanitizerRuntimeOptions,
47}
48struct GpuImage {
49    api: Arc<Bridge>,
50    raw: *mut hrx_fabric_gpu_image,
51}
52// Native image metadata is immutable after open; command construction uses
53// only caller-owned output. Thread-local diagnostics are copied immediately.
54unsafe impl Send for GpuImage {}
55unsafe impl Sync for GpuImage {}
56impl Drop for GpuImage {
57    fn drop(&mut self) {
58        unsafe { self.api.hrx_fabric_gpu_image_close(self.raw) };
59    }
60}
61
62/// One argument in native declaration order.
63pub enum Argument<'a> {
64    /// Device-visible address at a checked offset within shared backing.
65    Buffer(&'a Buffer, usize),
66    /// Exact bytes of one scalar/record argument.
67    Value(&'a [u8]),
68}
69impl Kernel {
70    /// Drain owned value/operation and workgroup race reports after all invocations retire.
71    /// Returns Busy while any submitted invocation retains the feedback storage.
72    /// A full channel reports dropped packets explicitly; it never stalls a kernel.
73    pub fn sanitizer_reports(&self) -> Result<super::SanitizerReports> {
74        self.0
75            .sanitizer
76            .as_ref()
77            .ok_or_else(|| Error::Unsupported("kernel has no sanitizer runtime".into()))?
78            .drain()
79    }
80    pub(crate) fn same_entry(&self, other: &Self) -> bool {
81        Arc::ptr_eq(&self.0, &other.0)
82    }
83    pub(crate) fn argument_layout(&self) -> Result<Vec<(u32, usize)>> {
84        (0..self.0.info.argument_count)
85            .map(|index| {
86                let mut info = hrx_fabric_gpu_argument::default();
87                bridge_check(&self.0.image.api, unsafe {
88                    self.0.image.api.hrx_fabric_gpu_argument_info(
89                        self.0.image.raw,
90                        index,
91                        &mut info,
92                    )
93                })?;
94                Ok((info.kind, info.size as usize))
95            })
96            .collect()
97    }
98    /// Required workgroup dimensions, when fixed by the compiled kernel.
99    pub fn workgroup_size(&self) -> [u32; 3] {
100        self.0.info.workgroup_size
101    }
102    /// Hardware subgroup width recorded in the loaded executable.
103    pub fn subgroup_size(&self) -> u32 {
104        self.0.info.wave_size
105    }
106    /// Static workgroup-local storage required by the loaded executable.
107    pub fn workgroup_storage_bytes(&self) -> u64 {
108        u64::from(self.0.info.local_bytes)
109    }
110    /// Device that owns the loaded executable allocation.
111    pub fn device(&self) -> &Device {
112        &self.0.device
113    }
114    pub(super) fn arguments(&self, arguments: &[Argument<'_>]) -> Result<(Vec<u8>, Vec<Buffer>)> {
115        if arguments.len() != self.0.info.argument_count as usize {
116            return Err(Error::Message("native argument count mismatch".into()));
117        }
118        let mut packed = vec![0; self.0.info.kernarg_bytes as usize];
119        let mut resources = vec![self.0.code.clone()];
120        if let Some(feedback) = &self.0.sanitizer {
121            resources.push(feedback.buffer().clone());
122        }
123        if let Some(state) = &self.0.address_state {
124            resources.push(state.shadow.clone());
125        }
126        if let Some(state) = &self.0.race_state {
127            resources.extend([state.shadow.clone(), state.queue_state.clone()]);
128        }
129        for (index, argument) in arguments.iter().enumerate() {
130            let mut info = hrx_fabric_gpu_argument::default();
131            bridge_check(&self.0.image.api, unsafe {
132                self.0.image.api.hrx_fabric_gpu_argument_info(
133                    self.0.image.raw,
134                    index as u32,
135                    &mut info,
136                )
137            })?;
138            let offset = info.offset as usize;
139            let end = offset
140                .checked_add(info.size as usize)
141                .filter(|end| *end <= packed.len())
142                .ok_or_else(|| Error::Message("native argument range overflow".into()))?;
143            let destination = &mut packed[offset..end];
144            match (info.kind, argument) {
145                (1, Argument::Value(bytes)) if bytes.len() == destination.len() => {
146                    destination.copy_from_slice(bytes)
147                }
148                (2, Argument::Buffer(buffer, offset))
149                    if destination.len() == 8 && *offset < buffer.len() =>
150                {
151                    let address = buffer
152                        .device_address(self.device())?
153                        .checked_add(*offset as u64)
154                        .ok_or_else(|| Error::Message("device argument address overflow".into()))?;
155                    destination.copy_from_slice(&address.to_le_bytes());
156                    resources.push((*buffer).clone());
157                }
158                _ => {
159                    return Err(Error::Message(format!(
160                        "native argument {index} has an incompatible kind, offset or size"
161                    )));
162                }
163            }
164        }
165        Ok((packed, resources))
166    }
167    pub(super) fn for_aql(
168        &self,
169        grid: [u32; 3],
170        ring: u64,
171        mask: u64,
172        arguments: &[Argument<'_>],
173    ) -> Result<Self> {
174        let Some(template) = &self.0.instrumentation else {
175            return Ok(self.clone());
176        };
177        let code = self.device().fabric().allocate_owned(
178            self.0.code.len(),
179            self.device(),
180            AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_WRITE | AMDF_MEMORY_ACCESS_EXECUTE,
181            4096,
182            false,
183            template.options.memory_budget.as_ref(),
184        )?;
185        let address_state = if template.address_global.is_empty() {
186            None
187        } else {
188            let mut ranges = vec![(code.device_address(self.device())?, code.len())];
189            for argument in arguments {
190                if let Argument::Buffer(buffer, _) = argument {
191                    ranges.push((buffer.device_address(self.device())?, buffer.len()));
192                }
193            }
194            Some(super::sanitizer::AddressState::new(
195                self.device(),
196                &template.options,
197                ranges,
198            )?)
199        };
200        let race_state = if template.race_global.is_empty() {
201            None
202        } else {
203            let mut options = template.options.clone();
204            options.maximum_shadow_bytes -= address_state.as_ref().map_or(0, |s| s.shadow.len());
205            Some(super::sanitizer::RaceState::new(
206                self.device(),
207                &options,
208                self.0.info.local_bytes,
209                grid,
210                ring,
211                mask,
212            )?)
213        };
214        let mut contents = vec![0; code.len()];
215        bridge_check(&self.0.image.api, unsafe {
216            self.0.image.api.hrx_fabric_gpu_image_load(
217                self.0.image.raw,
218                contents.as_mut_ptr(),
219                contents.len(),
220                code.device_address(self.device())?,
221            )
222        })?;
223        self.0
224            .sanitizer
225            .as_ref()
226            .ok_or_else(|| missing("sanitizer feedback"))?
227            .configure(&mut contents[self.0.feedback_global.clone()])?;
228        if let Some(state) = &address_state {
229            state.configure(&mut contents[template.address_global.clone()])?;
230        }
231        if let Some(state) = &race_state {
232            state.configure(&mut contents[template.race_global.clone()])?;
233        }
234        code.write(0, &contents)?;
235        Ok(Self(Arc::new(KernelInner {
236            device: self.device().clone(),
237            code,
238            image: self.0.image.clone(),
239            sanitizer: self.0.sanitizer.clone(),
240            feedback_global: self.0.feedback_global.clone(),
241            instrumentation: None,
242            race_state,
243            address_state,
244            info: self.0.info,
245        })))
246    }
247    pub(super) fn shadow_budget(&self) -> Option<&crate::residency::MemoryBudget> {
248        self.0
249            .instrumentation
250            .as_ref()
251            .and_then(|r| r.options.memory_budget.as_ref())
252    }
253    pub(super) fn dispatch_words(
254        &self,
255        grid: [u32; 3],
256        block: [u16; 3],
257        kernarg: &[u8],
258        address: u64,
259        scratch: Option<(&Buffer, u32, u32)>,
260    ) -> Result<Vec<u32>> {
261        if self.0.sanitizer.is_some() {
262            return Err(Error::Unsupported(
263                "sanitizer instrumentation requires AQL dispatch".into(),
264            ));
265        }
266        let mut words = vec![0; 128];
267        let mut count = 0;
268        bridge_check(&self.0.image.api, unsafe {
269            self.0.image.api.hrx_fabric_gpu_dispatch(
270                self.0.image.raw,
271                self.0.code.device_address(self.device())?,
272                block.as_ptr(),
273                grid.as_ptr(),
274                address,
275                kernarg.as_ptr(),
276                kernarg.len(),
277                scratch
278                    .map(|(buffer, _, _)| buffer.device_address(self.device()))
279                    .transpose()?
280                    .unwrap_or(0),
281                scratch.map_or(0, |(buffer, _, _)| buffer.len() as u64),
282                scratch.map_or(0, |(_, waves, _)| waves),
283                scratch.map_or(0, |(_, _, engines)| engines),
284                words.as_mut_ptr(),
285                words.len() as u32,
286                &mut count,
287            )
288        })?;
289        if count as usize > words.len() {
290            return Err(Error::Message("native command output overflow".into()));
291        }
292        words.truncate(count as usize);
293        Ok(words)
294    }
295}
296
297impl Device {
298    /// Load an entry from a compiler-produced artifact onto this GPU.
299    ///
300    /// # Safety
301    /// The executable is native code. The caller must trust its code and its
302    /// declared memory/argument contract; parsing is not a sandbox.
303    pub unsafe fn load(&self, artifact: &crate::loom::Artifact) -> Result<Kernel> {
304        if self.endpoint().engine() != Engine::Gpu || artifact.target() != self.target().as_str() {
305            return Err(Error::Unsupported("artifact and GPU target differ".into()));
306        }
307        unsafe { self.load_bytes(artifact.bytes(), artifact.symbol()) }
308    }
309    /// Load an artifact with an owned, bounded sanitizer feedback channel.
310    /// Supports address/value/operation reports and workgroup-local race checking.
311    /// Address shadow covers the complete bound allocations and executable storage.
312    /// Allocation starts must be 8-byte aligned; undeclared pointers stay poisoned.
313    /// Preparation rejects an address span whose shadow exceeds the configured limit.
314    /// Dispatch through an AQL queue, which supplies the native dispatch pointer.
315    /// # Safety
316    /// The artifact must use the current compiler and its bounded shadow checks.
317    /// It remains trusted native code. Use report-only instrumentation when
318    /// execution must complete after a diagnostic; trap mode can fault the queue.
319    pub unsafe fn load_sanitized(
320        &self,
321        artifact: &crate::loom::Artifact,
322        options: &super::SanitizerRuntimeOptions,
323    ) -> Result<Kernel> {
324        if artifact.target() != self.target().as_str() {
325            return Err(Error::Unsupported("artifact and GPU target differ".into()));
326        }
327        unsafe { self.load_image(artifact.bytes(), artifact.symbol(), Some(options), None) }
328    }
329    /// Load a native gfx1151 code object and select its named entry.
330    ///
331    /// # Safety
332    /// The native code must be trusted and obey its declared memory contract.
333    pub unsafe fn load_bytes(&self, bytes: &[u8], symbol: &str) -> Result<Kernel> {
334        unsafe { self.load_image(bytes, symbol, None, None) }
335    }
336    pub(super) unsafe fn load_image(
337        &self,
338        bytes: &[u8],
339        symbol: &str,
340        sanitizer_options: Option<&super::SanitizerRuntimeOptions>,
341        code_budget: Option<&crate::residency::MemoryBudget>,
342    ) -> Result<Kernel> {
343        if self.endpoint().engine() != Engine::Gpu {
344            return Err(Error::Unsupported("GPU code requires a GPU device".into()));
345        }
346        let api = &self.0.endpoint.0.instance.api;
347        let bridge = api.load_bridge()?;
348        let symbol = CString::new(symbol)
349            .map_err(|_| Error::Message("kernel symbol contains NUL".into()))?;
350        let mut raw = ptr::null_mut();
351        let mut info = hrx_fabric_gpu_info::default();
352        bridge_check(&bridge, unsafe {
353            bridge.hrx_fabric_gpu_image_open(
354                bytes.as_ptr(),
355                bytes.len(),
356                symbol.as_ptr(),
357                &mut raw,
358                &mut info,
359            )
360        })?;
361        if raw.is_null() {
362            return Err(missing("GPU image"));
363        }
364        let image = GpuImage { api: bridge, raw };
365        let global = |name: &std::ffi::CStr| -> Result<std::ops::Range<usize>> {
366            let mut offset = 0;
367            let mut length = 0;
368            bridge_check(&image.api, unsafe {
369                image.api.hrx_fabric_gpu_global_info(
370                    image.raw,
371                    name.as_ptr(),
372                    &mut offset,
373                    &mut length,
374                )
375            })?;
376            let start = usize::try_from(offset)
377                .map_err(|_| Error::Message("global offset overflow".into()))?;
378            let length = usize::try_from(length)
379                .map_err(|_| Error::Message("global size overflow".into()))?;
380            let end = start
381                .checked_add(length)
382                .filter(|end| *end as u64 <= info.storage_bytes)
383                .ok_or_else(|| Error::Message("global extent outside image".into()))?;
384            Ok(start..end)
385        };
386        let feedback_global = global(c"iree_feedback_config")?;
387        match (feedback_global.is_empty(), sanitizer_options.is_some()) {
388            (false, false) => {
389                return Err(Error::Unsupported(
390                    "artifact requires sanitizer feedback; use load_sanitized".into(),
391                ));
392            }
393            (true, true) => {
394                return Err(Error::Unsupported(
395                    "artifact has no structured sanitizer feedback global".into(),
396                ));
397            }
398            _ => (),
399        }
400        let race_global = global(c"iree_tsan_config")?;
401        let address_global = global(c"iree_asan_config")?;
402        for range in [&race_global, &address_global] {
403            if !range.is_empty() && range.len() != 96 {
404                return Err(Error::Unsupported(
405                    "unexpected sanitizer shadow configuration ABI".into(),
406                ));
407            }
408        }
409        let instrumentation = if race_global.is_empty() && address_global.is_empty() {
410            None
411        } else {
412            Some(ShadowTemplate {
413                race_global,
414                address_global,
415                options: sanitizer_options
416                    .ok_or_else(|| {
417                        Error::Unsupported("shadow instrumentation requires load_sanitized".into())
418                    })?
419                    .clone(),
420            })
421        };
422        let sites_global = global(c"loom_sanitizer_sites")?;
423        let size = usize::try_from(info.storage_bytes)
424            .map_err(|_| Error::Message("GPU image exceeds address space".into()))?;
425        let fabric = Fabric(self.0.endpoint.0.instance.clone());
426        let code = fabric.allocate_owned(
427            size,
428            self,
429            AMDF_MEMORY_ACCESS_READ | AMDF_MEMORY_ACCESS_WRITE | AMDF_MEMORY_ACCESS_EXECUTE,
430            4096,
431            false,
432            code_budget,
433        )?;
434        let mut contents = vec![0; size];
435        bridge_check(&image.api, unsafe {
436            image.api.hrx_fabric_gpu_image_load(
437                image.raw,
438                contents.as_mut_ptr(),
439                contents.len(),
440                code.device_address(self)?,
441            )
442        })?;
443        let sanitizer = sanitizer_options
444            .map(|options| {
445                let feedback =
446                    super::sanitizer::Feedback::new(self, options, &contents[sites_global])?;
447                let config = contents
448                    .get_mut(feedback_global.clone())
449                    .filter(|bytes| bytes.len() == 64)
450                    .ok_or_else(|| {
451                        Error::Unsupported("unexpected feedback configuration ABI".into())
452                    })?;
453                feedback.configure(config)?;
454                Ok::<_, Error>(Arc::new(feedback))
455            })
456            .transpose()?;
457        code.write(0, &contents)?;
458        Ok(Kernel(Arc::new(KernelInner {
459            sanitizer,
460            feedback_global,
461            instrumentation,
462            race_state: None,
463            address_state: None,
464            device: self.clone(),
465            code,
466            image: Arc::new(image),
467            info,
468        })))
469    }
470}
471
472pub(super) type BridgeSlot = Mutex<Option<Arc<Bridge>>>;
473
474impl Api {
475    /// The native bridge library: `HRX_FABRIC_LIBRARY`, or the one beside
476    /// `libamdf.so`. Every loader of bridge symbols uses this one path.
477    pub(super) fn bridge_path(&self) -> std::path::PathBuf {
478        std::env::var_os("HRX_FABRIC_LIBRARY")
479            .map(std::path::PathBuf::from)
480            .unwrap_or_else(|| self.directory.join("libhrx_fabric.so"))
481    }
482
483    pub(super) fn load_bridge(&self) -> Result<Arc<Bridge>> {
484        let api = self;
485
486        let mut slot = api
487            .bridge
488            .lock()
489            .map_err(|_| Error::Message("bridge loader poisoned".into()))?;
490        if slot.is_none() {
491            *slot = Some(Arc::new(unsafe { Bridge::new(api.bridge_path())? }));
492        }
493        Ok(slot.as_ref().unwrap().clone())
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::loom::{Compiler, CxxSource, Specialization};
501
502    #[test]
503    #[ignore = "requires native compiler, bridge, libamdf and gfx1151"]
504    fn replacing_retired_code_at_the_same_address_invalidates_all_instruction_lines() -> Result<()>
505    {
506        let gpu = Device::open(Engine::Gpu, 0)?;
507        let fabric = gpu.fabric();
508        let compiler = Compiler::for_target(None, gpu.target())?;
509        let mut kernels = Vec::new();
510        for stamp in [97u32, 173] {
511            let source = format!(
512                "#include <hip/hip_runtime.h>\n\
513                 __global__ [[loom::workgroup_size(64, 1, 1), loom::workgroup_count(256, 1, 1)]]\n\
514                 void stamp(unsigned* output) {{\n\
515                   unsigned i = blockIdx.x * 64u + threadIdx.x;\n\
516                   output[i] = i ^ {stamp}u;\n\
517                 }}"
518            );
519            let artifact = compiler
520                .import_cxx(CxxSource::new("stamp.cpp", source))?
521                .compile(&Specialization::new("stamp"))?;
522            // The owned fixture writes exactly 256 * 64 u32 values.
523            kernels.push(unsafe { gpu.load(&artifact) }?);
524        }
525        let mut replacement = Arc::try_unwrap(kernels.pop().unwrap().0)
526            .unwrap_or_else(|_| panic!("newly loaded fixture unexpectedly shared"));
527        let original = kernels.pop().unwrap();
528        assert_eq!(
529            original.0.info.storage_bytes,
530            replacement.info.storage_bytes
531        );
532        // Deliberately reuse one executable address, independent of allocator
533        // placement. Every rewrite below occurs after checked GPU retirement.
534        // This private test bypasses the public immutable-code API solely to
535        // reproduce freeing a code allocation and reusing its address.
536        replacement.code = original.0.code.clone();
537        let replacement = Kernel(Arc::new(replacement));
538        let queue = gpu.queue()?;
539        let output = fabric.allocate(256 * 64 * 4, std::slice::from_ref(&gpu))?;
540        let mut actual = vec![0; output.len()];
541        for _ in 0..8 {
542            for (kernel, stamp) in [(&original, 97u32), (&replacement, 173u32)] {
543                let mut contents = vec![0; kernel.0.info.storage_bytes as usize];
544                bridge_check(&kernel.0.image.api, unsafe {
545                    kernel.0.image.api.hrx_fabric_gpu_image_load(
546                        kernel.0.image.raw,
547                        contents.as_mut_ptr(),
548                        contents.len(),
549                        kernel.0.code.device_address(&gpu)?,
550                    )
551                })?;
552                kernel.0.code.write(0, &contents)?;
553                output.write(0, &vec![0xcd; output.len()])?;
554                // Same complete output binding for both known kernels.
555                let done = unsafe {
556                    queue.dispatch(
557                        kernel,
558                        [256, 1, 1],
559                        [64, 1, 1],
560                        &[Argument::Buffer(&output, 0)],
561                    )
562                }?;
563                if !done.wait_timeout(std::time::Duration::from_secs(5))? {
564                    std::mem::forget(done);
565                    return Err(Error::DeviceLost(
566                        "code-reuse fixture did not retire".into(),
567                    ));
568                }
569                output.read(0, &mut actual)?;
570                for (index, bytes) in actual.as_chunks::<4>().0.iter().enumerate() {
571                    assert_eq!(
572                        u32::from_le_bytes(*bytes),
573                        index as u32 ^ stamp,
574                        "stale executable at element {index}, expected stamp {stamp}"
575                    );
576                }
577            }
578        }
579        Ok(())
580    }
581}