Skip to main content

hrx/fabric/
profile.rs

1//! Optional device-clock diagnostics. No hooks run during ordinary submission.
2use super::*;
3use serde::Serialize;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6static NEXT_RECORDER: AtomicU64 = AtomicU64::new(1);
7
8type Marker = unsafe extern "C" fn(u64, *mut u32, u32, *mut u32) -> i32;
9type Clock = unsafe extern "C" fn(u32, u32, *mut u64) -> i32;
10
11pub(super) struct MarkerApi {
12    _library: libloading::Library,
13    marker: Marker,
14    frequency: u64,
15    device: [u32; 2],
16}
17impl MarkerApi {
18    pub(super) fn load(device: &Device) -> Result<Self> {
19        let api = &device.0.endpoint.0.instance.api;
20        let mut info = amdf_endpoint_info_t {
21            type_: AMDF_STRUCTURE_TYPE_ENDPOINT_INFO,
22            structure_size: size_of::<amdf_endpoint_info_t>() as u32,
23            ..Default::default()
24        };
25        check("profile endpoint", unsafe {
26            entry!(api.core, endpoint_query_info)(device.0.endpoint.0.raw, &mut info)
27        })?;
28        if info.native_identity.type_ != AMDF_ENDPOINT_NATIVE_IDENTITY_TYPE_LINUX_DEVICE {
29            return Err(Error::Unsupported(
30                "GPU timestamps require a Linux device identity".into(),
31            ));
32        }
33        // The discriminator above validates this union member. Symbols are
34        // optional so older runtimes retain ordinary inference compatibility.
35        let (library, marker, clock, native) = unsafe {
36            let library = libloading::Library::new(api.bridge_path())?;
37            let marker = *library
38                .get::<Marker>(b"hrx_fabric_gpu_profile_marker\0")
39                .map_err(|_| missing("GPU profile markers"))?;
40            let clock = *library
41                .get::<Clock>(b"hrx_fabric_gpu_profile_clock\0")
42                .map_err(|_| missing("GPU profile clock"))?;
43            (
44                library,
45                marker,
46                clock,
47                info.native_identity.value.linux_device,
48            )
49        };
50        let mut frequency = 0;
51        let status = unsafe { clock(native.major, native.minor, &mut frequency) };
52        if status != 0 || frequency == 0 {
53            return Err(Error::Unsupported(format!(
54                "GPU profile clock unavailable: errno {status}"
55            )));
56        }
57        Ok(Self {
58            _library: library,
59            marker,
60            frequency,
61            device: [native.major, native.minor],
62        })
63    }
64    pub(super) fn emit(&self, address: u64, words: &mut Vec<u32>) -> Result<()> {
65        let mut packet = [0u32; 32];
66        let mut count = 0;
67        let status = unsafe {
68            (self.marker)(
69                address,
70                packet.as_mut_ptr(),
71                packet.len() as u32,
72                &mut count,
73            )
74        };
75        if status != 0 || count == 0 || count as usize > packet.len() {
76            return Err(Error::Unsupported(format!(
77                "GPU timestamp marker unavailable: errno {status}"
78            )));
79        }
80        words.extend_from_slice(&packet[..count as usize]);
81        Ok(())
82    }
83}
84
85/// One instrumented command interval in the device's clock domain.
86#[derive(Debug, Clone, Serialize)]
87pub struct DeviceInterval {
88    /// Caller-assigned operation name.
89    pub label: String,
90    /// Start counter value.
91    pub start_tick: u64,
92    /// End counter value, after completion ordering.
93    pub end_tick: u64,
94}
95/// Serialized diagnostic execution, including marker and barrier overhead.
96/// These intervals measure an instrumented timeline, not hardware utilization.
97#[derive(Debug, Clone, Serialize)]
98pub struct DeviceProfile {
99    /// Process owning the queue and recorder identifiers.
100    pub process: u32,
101    /// Linux device major/minor identifying the counter's device domain.
102    pub device: [u32; 2],
103    /// Process-local identity of the originating queue.
104    pub queue: usize,
105    /// Process-local recorder identity, disambiguating graphs on the same queue.
106    pub recorder: u64,
107    /// Monotonically increasing sample identity within this recorder.
108    pub execution: u64,
109    /// GPU counter frequency obtained from the native device.
110    pub frequency_hz: u64,
111    /// Ordered intervals in one device-clock domain.
112    pub intervals: Vec<DeviceInterval>,
113    /// Union of measured intervals, converted to milliseconds.
114    pub interval_union_ms: f64,
115    /// Time between the first marker and final marker.
116    pub span_ms: f64,
117    /// Gaps in that span outside measured intervals.
118    pub gaps_ms: f64,
119}
120
121pub(crate) struct ProfileCapture {
122    pub(super) buffer: Buffer,
123    pub(super) api: MarkerApi,
124    labels: Vec<String>,
125    queue: usize,
126    recorder: u64,
127    execution: u64,
128    previous_end: u64,
129}
130impl ProfileCapture {
131    pub(super) fn new(queue: &Queue, labels: &[String]) -> Result<Self> {
132        if labels.is_empty() {
133            return Err(Error::Message("empty profile".into()));
134        }
135        let bytes = labels
136            .len()
137            .checked_mul(16)
138            .ok_or_else(|| Error::Message("profile size overflow".into()))?;
139        let api = MarkerApi::load(queue.device())?;
140        let buffer = queue
141            .device()
142            .fabric()
143            .allocate_shared(bytes, std::slice::from_ref(queue.device()))?;
144        #[allow(deprecated, reason = "fetch_update supports the Rust 1.91 MSRV")]
145        let recorder = NEXT_RECORDER
146            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| v.checked_add(1))
147            .map_err(|_| Error::Message("profile recorder identity exhausted".into()))?;
148        Ok(Self {
149            buffer,
150            api,
151            labels: labels.to_vec(),
152            queue: queue.identity(),
153            recorder,
154            execution: 0,
155            previous_end: 0,
156        })
157    }
158    pub(crate) fn read(&mut self) -> Result<DeviceProfile> {
159        let mut bytes = vec![0; self.labels.len() * 16];
160        // Buffer ownership refuses reads until all device leases have retired.
161        self.buffer.read(0, &mut bytes)?;
162        let intervals = bytes
163            .as_chunks::<16>()
164            .0
165            .iter()
166            .zip(&self.labels)
167            .map(|(bytes, label)| DeviceInterval {
168                label: label.clone(),
169                start_tick: u64::from_le_bytes(bytes[..8].try_into().unwrap()),
170                end_tick: u64::from_le_bytes(bytes[8..].try_into().unwrap()),
171            })
172            .collect::<Vec<_>>();
173        let (union, span) = interval_totals(&intervals)?;
174        if intervals.iter().any(|i| i.start_tick <= self.previous_end) {
175            return Err(Error::Message("stale or reset GPU profile counter".into()));
176        }
177        self.previous_end = intervals.iter().map(|i| i.end_tick).max().unwrap_or(0);
178        self.execution = self
179            .execution
180            .checked_add(1)
181            .ok_or_else(|| Error::Message("profile sample identity exhausted".into()))?;
182        let ms = 1000. / self.api.frequency as f64;
183        Ok(DeviceProfile {
184            process: std::process::id(),
185            device: self.api.device,
186            queue: self.queue,
187            recorder: self.recorder,
188            execution: self.execution,
189            frequency_hz: self.api.frequency,
190            intervals,
191            interval_union_ms: union as f64 * ms,
192            span_ms: span as f64 * ms,
193            gaps_ms: (span - union) as f64 * ms,
194        })
195    }
196}
197fn interval_totals(intervals: &[DeviceInterval]) -> Result<(u64, u64)> {
198    if intervals.is_empty() {
199        return Ok((0, 0));
200    }
201    let mut ordered = intervals.iter().collect::<Vec<_>>();
202    ordered.sort_by_key(|i| i.start_tick);
203    if ordered
204        .iter()
205        .any(|i| i.start_tick == 0 || i.end_tick < i.start_tick)
206    {
207        return Err(Error::Message(
208            "unwritten, reversed, or wrapped GPU timestamps".into(),
209        ));
210    }
211    let first = ordered[0].start_tick;
212    let mut end = first;
213    let mut union = 0;
214    for i in ordered {
215        let start = end.max(i.start_tick);
216        if i.end_tick > start {
217            union += i.end_tick - start;
218        }
219        end = end.max(i.end_tick);
220    }
221    Ok((union, end - first))
222}
223
224/// An explicitly instrumented reusable native batch.
225/// Samples synchronize before reading and cannot overwrite an in-flight sample.
226pub struct ProfiledGpu {
227    pub(crate) commands: PreparedGpu,
228    pub(crate) capture: ProfileCapture,
229}
230impl ProfiledGpu {
231    /// Execute and wait for one sample.
232    ///
233    /// # Safety
234    /// The prepared kernels' contracts and cross-queue ordering must still hold.
235    pub unsafe fn run(&mut self) -> Result<DeviceProfile> {
236        unsafe { self.commands.dispatch()? }.wait()?;
237        self.capture.read()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    #[test]
245    fn union_does_not_double_count_overlap_and_detects_invalid_ticks() {
246        let interval = |start_tick, end_tick| DeviceInterval {
247            label: String::new(),
248            start_tick,
249            end_tick,
250        };
251        assert_eq!(
252            interval_totals(&[interval(20, 30), interval(10, 25), interval(40, 45)]).unwrap(),
253            (25, 35)
254        );
255        assert!(interval_totals(&[interval(0, 1)]).is_err());
256        assert!(interval_totals(&[interval(2, 1)]).is_err());
257    }
258}