qualia_core_db/inference/
inference_gpu_profiler.rs1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
21
22#[repr(usize)]
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Phase {
26 Embedding = 0,
28 Gemm = 1,
30 Attention = 2,
32 FusedBlock = 3,
34 OutputTopk = 4,
36}
37
38impl Phase {
39 pub const COUNT: usize = 5;
40 pub const ALL: [Phase; Self::COUNT] = [
41 Phase::Embedding,
42 Phase::Gemm,
43 Phase::Attention,
44 Phase::FusedBlock,
45 Phase::OutputTopk,
46 ];
47
48 #[inline]
49 pub fn label(self) -> &'static str {
50 match self {
51 Phase::Embedding => "embedding",
52 Phase::Gemm => "gemm",
53 Phase::Attention => "attention",
54 Phase::FusedBlock => "fused_block",
55 Phase::OutputTopk => "output_topk",
56 }
57 }
58}
59
60static ENABLED: AtomicBool = AtomicBool::new(false);
61static ACC_NS: [AtomicU64; Phase::COUNT] = [const { AtomicU64::new(0) }; Phase::COUNT];
62static ACC_CALLS: [AtomicU64; Phase::COUNT] = [const { AtomicU64::new(0) }; Phase::COUNT];
63
64#[inline]
66pub fn set_enabled(on: bool) {
67 ENABLED.store(on, Ordering::Relaxed);
68}
69
70fn env_opt_in() -> bool {
72 static ENV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
73 *ENV.get_or_init(|| {
74 std::env::var("QUALIA_LLM_GPU_PROFILE")
75 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
76 .unwrap_or(false)
77 })
78}
79
80#[inline]
82pub fn enabled() -> bool {
83 if !(ENABLED.load(Ordering::Relaxed) || env_opt_in()) {
84 return false;
85 }
86 #[cfg(not(target_arch = "wasm32"))]
87 {
88 crate::gpu_context::shared_gpu().timestamps_supported
89 }
90 #[cfg(target_arch = "wasm32")]
91 {
92 false
93 }
94}
95
96#[cfg(not(target_arch = "wasm32"))]
99struct TsResources {
100 qset: wgpu::QuerySet,
101 resolve: wgpu::Buffer,
102 staging: wgpu::Buffer,
103}
104
105#[cfg(not(target_arch = "wasm32"))]
106const TS_BYTES: u64 = 2 * std::mem::size_of::<u64>() as u64; #[cfg(not(target_arch = "wasm32"))]
109fn resources() -> &'static TsResources {
110 use std::sync::OnceLock;
111 static R: OnceLock<TsResources> = OnceLock::new();
112 R.get_or_init(|| {
113 let device = &crate::gpu_context::shared_gpu().device;
114 let qset = device.create_query_set(&wgpu::QuerySetDescriptor {
115 label: Some("llm-ts-qset"),
116 ty: wgpu::QueryType::Timestamp,
117 count: 2,
118 });
119 let resolve = device.create_buffer(&wgpu::BufferDescriptor {
120 label: Some("llm-ts-resolve"),
121 size: TS_BYTES,
122 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
123 mapped_at_creation: false,
124 });
125 let staging = device.create_buffer(&wgpu::BufferDescriptor {
126 label: Some("llm-ts-staging"),
127 size: TS_BYTES,
128 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
129 mapped_at_creation: false,
130 });
131 TsResources {
132 qset,
133 resolve,
134 staging,
135 }
136 })
137}
138
139#[cfg(not(target_arch = "wasm32"))]
142pub fn pass_writes_both() -> Option<wgpu::ComputePassTimestampWrites<'static>> {
143 if !enabled() {
144 return None;
145 }
146 Some(wgpu::ComputePassTimestampWrites {
147 query_set: &resources().qset,
148 beginning_of_pass_write_index: Some(0),
149 end_of_pass_write_index: Some(1),
150 })
151}
152
153#[cfg(not(target_arch = "wasm32"))]
155pub fn pass_writes_begin() -> Option<wgpu::ComputePassTimestampWrites<'static>> {
156 if !enabled() {
157 return None;
158 }
159 Some(wgpu::ComputePassTimestampWrites {
160 query_set: &resources().qset,
161 beginning_of_pass_write_index: Some(0),
162 end_of_pass_write_index: None,
163 })
164}
165
166#[cfg(not(target_arch = "wasm32"))]
168pub fn pass_writes_end() -> Option<wgpu::ComputePassTimestampWrites<'static>> {
169 if !enabled() {
170 return None;
171 }
172 Some(wgpu::ComputePassTimestampWrites {
173 query_set: &resources().qset,
174 beginning_of_pass_write_index: None,
175 end_of_pass_write_index: Some(1),
176 })
177}
178
179#[cfg(not(target_arch = "wasm32"))]
182pub fn resolve(encoder: &mut wgpu::CommandEncoder) {
183 if !enabled() {
184 return;
185 }
186 let r = resources();
187 encoder.resolve_query_set(&r.qset, 0..2, &r.resolve, 0);
188 encoder.copy_buffer_to_buffer(&r.resolve, 0, &r.staging, 0, TS_BYTES);
189}
190
191#[cfg(not(target_arch = "wasm32"))]
194pub fn accumulate(phase: Phase) {
195 if !enabled() {
196 return;
197 }
198 let ctx = crate::gpu_context::shared_gpu();
199 let r = resources();
200 let slice = r.staging.slice(..);
201 let (tx, rx) = std::sync::mpsc::channel();
202 slice.map_async(wgpu::MapMode::Read, move |res| {
203 let _ = tx.send(res);
204 });
205 let _ = ctx.device.poll(wgpu::PollType::wait_indefinitely());
207 if rx.recv().map(|r| r.is_ok()).unwrap_or(false) {
208 let data = slice
209 .get_mapped_range()
210 .expect("wgpu buffer map_range failed");
211 let ts: &[u64] = bytemuck::cast_slice(&data);
212 if ts.len() >= 2 {
213 let ticks = ts[1].saturating_sub(ts[0]);
214 let ns = (ticks as f64 * ctx.timestamp_period_ns as f64) as u64;
215 ACC_NS[phase as usize].fetch_add(ns, Ordering::Relaxed);
216 ACC_CALLS[phase as usize].fetch_add(1, Ordering::Relaxed);
217 }
218 drop(data);
219 }
220 r.staging.unmap();
221}
222
223#[cfg(target_arch = "wasm32")]
224pub fn pass_writes_both() -> Option<wgpu::ComputePassTimestampWrites<'static>> {
225 None
226}
227#[cfg(target_arch = "wasm32")]
228pub fn pass_writes_begin() -> Option<wgpu::ComputePassTimestampWrites<'static>> {
229 None
230}
231#[cfg(target_arch = "wasm32")]
232pub fn pass_writes_end() -> Option<wgpu::ComputePassTimestampWrites<'static>> {
233 None
234}
235#[cfg(target_arch = "wasm32")]
236pub fn resolve(_encoder: &mut wgpu::CommandEncoder) {}
237#[cfg(target_arch = "wasm32")]
238pub fn accumulate(_phase: Phase) {}
239
240#[derive(Debug, Clone, Copy)]
244pub struct PhaseTiming {
245 pub phase: Phase,
246 pub total_ns: u64,
247 pub calls: u64,
248}
249
250impl PhaseTiming {
251 #[inline]
252 pub fn micros(&self) -> f64 {
253 self.total_ns as f64 / 1000.0
254 }
255}
256
257pub fn snapshot() -> [PhaseTiming; Phase::COUNT] {
259 let mut out = [PhaseTiming {
260 phase: Phase::Embedding,
261 total_ns: 0,
262 calls: 0,
263 }; Phase::COUNT];
264 for (i, p) in Phase::ALL.iter().enumerate() {
265 out[i] = PhaseTiming {
266 phase: *p,
267 total_ns: ACC_NS[i].load(Ordering::Relaxed),
268 calls: ACC_CALLS[i].load(Ordering::Relaxed),
269 };
270 }
271 out
272}
273
274pub fn reset() {
276 for i in 0..Phase::COUNT {
277 ACC_NS[i].store(0, Ordering::Relaxed);
278 ACC_CALLS[i].store(0, Ordering::Relaxed);
279 }
280}
281
282pub fn any_recorded() -> bool {
284 (0..Phase::COUNT).any(|i| ACC_CALLS[i].load(Ordering::Relaxed) > 0)
285}
286
287#[cfg(all(test, not(target_arch = "wasm32")))]
288mod tests {
289 use super::*;
290
291 const SELF_TEST_WGSL: &str = r#"
293@group(0) @binding(0) var<storage, read_write> data: array<u32>;
294@compute @workgroup_size(64)
295fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
296 let i = gid.x;
297 if (i < arrayLength(&data)) {
298 var acc = data[i];
299 for (var k: u32 = 0u; k < 4096u; k = k + 1u) {
300 acc = acc * 1664525u + 1013904223u;
301 }
302 data[i] = acc;
303 }
304}
305"#;
306
307 #[test]
311 fn gpu_timestamp_self_test_records_nonzero() {
312 let ctx = crate::gpu_context::shared_gpu();
313 if !ctx.timestamps_supported {
314 eprintln!("SKIP gpu_timestamp_self_test: adapter has no TIMESTAMP_QUERY");
315 return;
316 }
317 let device = &ctx.device;
318 let queue = &ctx.queue;
319
320 set_enabled(true);
321 reset();
322
323 let n: u64 = 65_536;
324 let buf = device.create_buffer(&wgpu::BufferDescriptor {
325 label: Some("prof-self-test-buf"),
326 size: n * 4,
327 usage: wgpu::BufferUsages::STORAGE,
328 mapped_at_creation: false,
329 });
330 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
331 label: Some("prof-self-test-shader"),
332 source: wgpu::ShaderSource::Wgsl(SELF_TEST_WGSL.into()),
333 });
334 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
335 label: Some("prof-self-test-pipe"),
336 layout: None,
337 module: &shader,
338 entry_point: Some("main"),
339 compilation_options: wgpu::PipelineCompilationOptions::default(),
340 cache: None,
341 });
342 let bgl = pipeline.get_bind_group_layout(0);
343 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
344 label: Some("prof-self-test-bg"),
345 layout: &bgl,
346 entries: &[wgpu::BindGroupEntry {
347 binding: 0,
348 resource: buf.as_entire_binding(),
349 }],
350 });
351
352 let mut encoder =
353 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
354 {
355 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
356 label: Some("prof-self-test-pass"),
357 timestamp_writes: pass_writes_both(),
358 });
359 cpass.set_pipeline(&pipeline);
360 cpass.set_bind_group(0, &bind_group, &[]);
361 cpass.dispatch_workgroups((n as u32 + 63) / 64, 1, 1);
362 }
363 resolve(&mut encoder);
364 queue.submit(Some(encoder.finish()));
365 accumulate(Phase::Gemm);
366
367 assert!(
368 any_recorded(),
369 "profiler recorded no timestamps on a TIMESTAMP_QUERY device"
370 );
371 let snap = snapshot();
372 let gemm = snap
373 .iter()
374 .find(|t| matches!(t.phase, Phase::Gemm))
375 .copied()
376 .expect("gemm phase present");
377 eprintln!(
378 "gpu_timestamp_self_test: gemm pass = {:.3} µs ({} ns, {} call)",
379 gemm.micros(),
380 gemm.total_ns,
381 gemm.calls
382 );
383 assert_eq!(gemm.calls, 1, "expected exactly one recorded pass");
384 assert!(
385 gemm.total_ns > 0,
386 "expected non-zero GPU time for a 4096-iter kernel"
387 );
388
389 set_enabled(false);
390 }
391
392 #[test]
393 fn disabled_profiler_is_noop() {
394 set_enabled(false);
395 reset();
396 assert!(pass_writes_both().is_none());
398 assert!(pass_writes_begin().is_none());
399 assert!(pass_writes_end().is_none());
400 accumulate(Phase::Attention);
401 assert!(!any_recorded());
402 }
403}