/home/noah/src/realizar/src/brick/tracer.rs
Line | Count | Source |
1 | | //! BrickTracer: The Golden Trace for GPU/CPU Parity Debugging (Phase 14) |
2 | | //! |
3 | | //! This module provides automated divergence detection between CPU and GPU |
4 | | //! inference paths by logging tensor checksums at each computational step. |
5 | | //! |
6 | | //! # Purpose |
7 | | //! |
8 | | //! When GPU output diverges from CPU (e.g., "1+1=" yields "2." on CPU but "10" on GPU), |
9 | | //! the tracer identifies the EXACT point of divergence by comparing: |
10 | | //! - L2 norm (cheap fingerprint) |
11 | | //! - First N elements (for debugging) |
12 | | //! - Full tensor (if verbose mode enabled) |
13 | | //! |
14 | | //! # Usage |
15 | | //! |
16 | | //! ```rust,ignore |
17 | | //! use realizar::brick::BrickTracer; |
18 | | //! |
19 | | //! let mut tracer = BrickTracer::new(); |
20 | | //! |
21 | | //! // Log at each computation step |
22 | | //! tracer.log("embedding", &embedding_output); |
23 | | //! tracer.log("layer0_attn_norm", &normed); |
24 | | //! tracer.log("layer0_qkv", &qkv); |
25 | | //! tracer.log("layer0_rope_q", &q_rope); |
26 | | //! tracer.log("layer0_rope_k", &k_rope); |
27 | | //! tracer.log("layer0_attention", &attn_out); |
28 | | //! tracer.log("layer0_o_proj", &o_proj); |
29 | | //! tracer.log("layer0_ffn_norm", &ffn_normed); |
30 | | //! tracer.log("layer0_ffn", &ffn_out); |
31 | | //! // ... more layers ... |
32 | | //! tracer.log("final_norm", &final_normed); |
33 | | //! tracer.log("logits", &logits); |
34 | | //! |
35 | | //! // Compare CPU vs GPU traces |
36 | | //! let divergence = BrickTracer::compare(&cpu_tracer, &gpu_tracer); |
37 | | //! if let Some(first_diff) = divergence.first_divergence() { |
38 | | //! eprintln!("Divergence at: {} (CPU L2={}, GPU L2={})", |
39 | | //! first_diff.name, first_diff.cpu_l2, first_diff.gpu_l2); |
40 | | //! } |
41 | | //! ``` |
42 | | //! |
43 | | //! # References |
44 | | //! |
45 | | //! - Phase 14: "The New Doctrine: The Golden Trace" |
46 | | //! - PMAT-106: APR GPU Adapter + Integration Tests |
47 | | |
48 | | use std::collections::HashMap; |
49 | | use std::fmt; |
50 | | |
51 | | /// A single trace event capturing tensor state at a computation step |
52 | | #[derive(Debug, Clone)] |
53 | | pub struct TraceEvent { |
54 | | /// Name of the computation step (e.g., "layer0_rope_q") |
55 | | pub name: String, |
56 | | /// Position/sequence index when this event was logged |
57 | | pub position: usize, |
58 | | /// L2 norm of the tensor (cheap fingerprint) |
59 | | pub l2_norm: f32, |
60 | | /// Mean value |
61 | | pub mean: f32, |
62 | | /// Min value |
63 | | pub min: f32, |
64 | | /// Max value |
65 | | pub max: f32, |
66 | | /// First 8 elements (for quick debugging) |
67 | | pub head: [f32; 8], |
68 | | /// Full tensor data (only if verbose mode enabled) |
69 | | pub full_data: Option<Vec<f32>>, |
70 | | /// Tensor length |
71 | | pub len: usize, |
72 | | } |
73 | | |
74 | | impl TraceEvent { |
75 | | /// Create a new trace event from tensor data |
76 | 24 | pub fn new(name: &str, tensor: &[f32], position: usize, verbose: bool) -> Self { |
77 | 24 | let len = tensor.len(); |
78 | | |
79 | | // Compute L2 norm |
80 | 53 | let l2_norm24 = tensor24 .iter24 ().map24 (|x| x * x).sum24 ::<f32>().sqrt24 (); |
81 | | |
82 | | // Compute statistics |
83 | 24 | let mean = if len > 0 { |
84 | 23 | tensor.iter().sum::<f32>() / len as f32 |
85 | | } else { |
86 | 1 | 0.0 |
87 | | }; |
88 | 24 | let min = tensor.iter().cloned().fold(f32::INFINITY, f32::min); |
89 | 24 | let max = tensor.iter().cloned().fold(f32::NEG_INFINITY, f32::max); |
90 | | |
91 | | // Copy first 8 elements |
92 | 24 | let mut head = [0.0f32; 8]; |
93 | 53 | for (i, &v) in tensor24 .iter24 ().take24 (8).enumerate24 () { |
94 | 53 | head[i] = v; |
95 | 53 | } |
96 | | |
97 | | // Optionally store full tensor |
98 | 24 | let full_data = if verbose { |
99 | 1 | Some(tensor.to_vec()) |
100 | | } else { |
101 | 23 | None |
102 | | }; |
103 | | |
104 | 24 | Self { |
105 | 24 | name: name.to_string(), |
106 | 24 | position, |
107 | 24 | l2_norm, |
108 | 24 | mean, |
109 | 24 | min, |
110 | 24 | max, |
111 | 24 | head, |
112 | 24 | full_data, |
113 | 24 | len, |
114 | 24 | } |
115 | 24 | } |
116 | | |
117 | | /// Check if two events are approximately equal within tolerance |
118 | 8 | pub fn approx_eq(&self, other: &Self, tolerance: f32) -> bool { |
119 | | // Check L2 norm |
120 | 8 | let l2_diff = (self.l2_norm - other.l2_norm).abs(); |
121 | 8 | let l2_rel = if self.l2_norm.abs() > 1e-10 { |
122 | 8 | l2_diff / self.l2_norm.abs() |
123 | | } else { |
124 | 0 | l2_diff |
125 | | }; |
126 | | |
127 | 8 | l2_rel <= tolerance |
128 | 8 | } |
129 | | |
130 | | /// Compute relative difference between two events |
131 | 2 | pub fn relative_diff(&self, other: &Self) -> f32 { |
132 | 2 | let l2_diff = (self.l2_norm - other.l2_norm).abs(); |
133 | 2 | if self.l2_norm.abs() > 1e-10 { |
134 | 2 | l2_diff / self.l2_norm.abs() |
135 | | } else { |
136 | 0 | l2_diff |
137 | | } |
138 | 2 | } |
139 | | } |
140 | | |
141 | | impl fmt::Display for TraceEvent { |
142 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
143 | 0 | write!( |
144 | 0 | f, |
145 | 0 | "{} [pos={}]: L2={:.6}, mean={:.6}, range=[{:.6}, {:.6}], len={}, head=[{:.4}, {:.4}, {:.4}, {:.4}...]", |
146 | | self.name, |
147 | | self.position, |
148 | | self.l2_norm, |
149 | | self.mean, |
150 | | self.min, |
151 | | self.max, |
152 | | self.len, |
153 | 0 | self.head[0], |
154 | 0 | self.head[1], |
155 | 0 | self.head[2], |
156 | 0 | self.head[3], |
157 | | ) |
158 | 0 | } |
159 | | } |
160 | | |
161 | | /// Result of comparing two trace events |
162 | | #[derive(Debug, Clone)] |
163 | | pub struct TraceDiff { |
164 | | /// Name of the computation step |
165 | | pub name: String, |
166 | | /// Position where divergence occurred |
167 | | pub position: usize, |
168 | | /// CPU L2 norm |
169 | | pub cpu_l2: f32, |
170 | | /// GPU L2 norm |
171 | | pub gpu_l2: f32, |
172 | | /// Relative difference (|cpu - gpu| / |cpu|) |
173 | | pub relative_diff: f32, |
174 | | /// CPU head values |
175 | | pub cpu_head: [f32; 8], |
176 | | /// GPU head values |
177 | | pub gpu_head: [f32; 8], |
178 | | } |
179 | | |
180 | | impl fmt::Display for TraceDiff { |
181 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
182 | 0 | write!( |
183 | 0 | f, |
184 | 0 | "{} [pos={}]: CPU L2={:.6} vs GPU L2={:.6} (diff={:.2}%)\n CPU head: [{:.4}, {:.4}, {:.4}, {:.4}...]\n GPU head: [{:.4}, {:.4}, {:.4}, {:.4}...]", |
185 | | self.name, |
186 | | self.position, |
187 | | self.cpu_l2, |
188 | | self.gpu_l2, |
189 | 0 | self.relative_diff * 100.0, |
190 | 0 | self.cpu_head[0], self.cpu_head[1], self.cpu_head[2], self.cpu_head[3], |
191 | 0 | self.gpu_head[0], self.gpu_head[1], self.gpu_head[2], self.gpu_head[3], |
192 | | ) |
193 | 0 | } |
194 | | } |
195 | | |
196 | | /// Comparison result between CPU and GPU traces |
197 | | #[derive(Debug, Clone)] |
198 | | pub struct TraceComparison { |
199 | | /// All differences found |
200 | | pub diffs: Vec<TraceDiff>, |
201 | | /// Tolerance used for comparison |
202 | | pub tolerance: f32, |
203 | | } |
204 | | |
205 | | impl TraceComparison { |
206 | | /// Get the first divergence point |
207 | 1 | pub fn first_divergence(&self) -> Option<&TraceDiff> { |
208 | 1 | self.diffs.first() |
209 | 1 | } |
210 | | |
211 | | /// Check if traces are equivalent (no divergence) |
212 | 2 | pub fn is_equivalent(&self) -> bool { |
213 | 2 | self.diffs.is_empty() |
214 | 2 | } |
215 | | |
216 | | /// Get summary of divergence |
217 | 0 | pub fn summary(&self) -> String { |
218 | 0 | if self.diffs.is_empty() { |
219 | 0 | "No divergence detected".to_string() |
220 | | } else { |
221 | 0 | let first = &self.diffs[0]; |
222 | 0 | format!( |
223 | 0 | "First divergence at '{}' (pos={}): {:.2}% L2 diff ({} total divergences)", |
224 | | first.name, |
225 | | first.position, |
226 | 0 | first.relative_diff * 100.0, |
227 | 0 | self.diffs.len() |
228 | | ) |
229 | | } |
230 | 0 | } |
231 | | } |
232 | | |
233 | | impl fmt::Display for TraceComparison { |
234 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
235 | 0 | writeln!(f, "=== TRACE COMPARISON ===")?; |
236 | 0 | writeln!(f, "Tolerance: {:.4}%", self.tolerance * 100.0)?; |
237 | 0 | writeln!(f, "Divergences: {}", self.diffs.len())?; |
238 | | |
239 | 0 | if self.diffs.is_empty() { |
240 | 0 | writeln!(f, "Result: MATCH")?; |
241 | | } else { |
242 | 0 | writeln!(f, "Result: DIVERGENCE DETECTED")?; |
243 | 0 | writeln!(f, "\n--- First Divergence ---")?; |
244 | 0 | if let Some(first) = self.diffs.first() { |
245 | 0 | writeln!(f, "{first}")?; |
246 | 0 | } |
247 | | |
248 | 0 | if self.diffs.len() > 1 { |
249 | 0 | writeln!(f, "\n--- All Divergences ---")?; |
250 | 0 | for diff in &self.diffs { |
251 | 0 | writeln!(f, " {}: {:.2}% diff", diff.name, diff.relative_diff * 100.0)?; |
252 | | } |
253 | 0 | } |
254 | | } |
255 | | |
256 | 0 | Ok(()) |
257 | 0 | } |
258 | | } |
259 | | |
260 | | /// BrickTracer: Collects trace events for GPU/CPU parity debugging |
261 | | /// |
262 | | /// The tracer logs tensor state at each computation step, enabling |
263 | | /// automated detection of where GPU output diverges from CPU. |
264 | | #[derive(Debug, Clone)] |
265 | | pub struct BrickTracer { |
266 | | /// Collected trace events in order |
267 | | events: Vec<TraceEvent>, |
268 | | /// Current position being traced |
269 | | position: usize, |
270 | | /// Whether to store full tensor data (expensive) |
271 | | verbose: bool, |
272 | | /// Event index by name for fast lookup |
273 | | index: HashMap<String, usize>, |
274 | | } |
275 | | |
276 | | impl Default for BrickTracer { |
277 | 0 | fn default() -> Self { |
278 | 0 | Self::new() |
279 | 0 | } |
280 | | } |
281 | | |
282 | | impl BrickTracer { |
283 | | /// Create a new tracer |
284 | 7 | pub fn new() -> Self { |
285 | 7 | Self { |
286 | 7 | events: Vec::new(), |
287 | 7 | position: 0, |
288 | 7 | verbose: false, |
289 | 7 | index: HashMap::new(), |
290 | 7 | } |
291 | 7 | } |
292 | | |
293 | | /// Create a tracer with verbose mode (stores full tensors) |
294 | 0 | pub fn verbose() -> Self { |
295 | 0 | Self { |
296 | 0 | events: Vec::new(), |
297 | 0 | position: 0, |
298 | 0 | verbose: true, |
299 | 0 | index: HashMap::new(), |
300 | 0 | } |
301 | 0 | } |
302 | | |
303 | | /// Set the current position being traced |
304 | 2 | pub fn set_position(&mut self, position: usize) { |
305 | 2 | self.position = position; |
306 | 2 | } |
307 | | |
308 | | /// Log a tensor at the current computation step |
309 | 15 | pub fn log(&mut self, name: &str, tensor: &[f32]) { |
310 | 15 | let event = TraceEvent::new(name, tensor, self.position, self.verbose); |
311 | 15 | let idx = self.events.len(); |
312 | 15 | self.index.insert(name.to_string(), idx); |
313 | 15 | self.events.push(event); |
314 | 15 | } |
315 | | |
316 | | /// Log a tensor with explicit position |
317 | 1 | pub fn log_at(&mut self, name: &str, tensor: &[f32], position: usize) { |
318 | 1 | let event = TraceEvent::new(name, tensor, position, self.verbose); |
319 | 1 | let idx = self.events.len(); |
320 | 1 | self.index.insert(name.to_string(), idx); |
321 | 1 | self.events.push(event); |
322 | 1 | } |
323 | | |
324 | | /// Get all trace events |
325 | 3 | pub fn events(&self) -> &[TraceEvent] { |
326 | 3 | &self.events |
327 | 3 | } |
328 | | |
329 | | /// Get event by name |
330 | 11 | pub fn get(&self, name: &str) -> Option<&TraceEvent> { |
331 | 11 | self.index.get(name).map(|&idx| &self.events[idx]10 ) |
332 | 11 | } |
333 | | |
334 | | /// Clear all events |
335 | 1 | pub fn clear(&mut self) { |
336 | 1 | self.events.clear(); |
337 | 1 | self.index.clear(); |
338 | 1 | self.position = 0; |
339 | 1 | } |
340 | | |
341 | | /// Compare two tracers (CPU vs GPU) |
342 | | /// |
343 | | /// # Arguments |
344 | | /// |
345 | | /// * `cpu` - CPU tracer |
346 | | /// * `gpu` - GPU tracer |
347 | | /// * `tolerance` - Relative tolerance for L2 norm comparison (e.g., 0.01 = 1%) |
348 | | /// |
349 | | /// # Returns |
350 | | /// |
351 | | /// Comparison result with all divergences |
352 | 2 | pub fn compare(cpu: &Self, gpu: &Self, tolerance: f32) -> TraceComparison { |
353 | 2 | let mut diffs = Vec::new(); |
354 | | |
355 | | // Compare events in order |
356 | 7 | for cpu_event5 in &cpu.events { |
357 | 5 | if let Some(gpu_event) = gpu.get(&cpu_event.name) { |
358 | 5 | if !cpu_event.approx_eq(gpu_event, tolerance) { |
359 | 1 | diffs.push(TraceDiff { |
360 | 1 | name: cpu_event.name.clone(), |
361 | 1 | position: cpu_event.position, |
362 | 1 | cpu_l2: cpu_event.l2_norm, |
363 | 1 | gpu_l2: gpu_event.l2_norm, |
364 | 1 | relative_diff: cpu_event.relative_diff(gpu_event), |
365 | 1 | cpu_head: cpu_event.head, |
366 | 1 | gpu_head: gpu_event.head, |
367 | 1 | }); |
368 | 4 | } |
369 | 0 | } |
370 | | // Skip if GPU doesn't have this event (may have different instrumentation) |
371 | | } |
372 | | |
373 | 2 | TraceComparison { diffs, tolerance } |
374 | 2 | } |
375 | | |
376 | | /// Print all events to stderr for debugging |
377 | 0 | pub fn dump(&self) { |
378 | 0 | eprintln!("=== BRICK TRACE ({} events) ===", self.events.len()); |
379 | 0 | for event in &self.events { |
380 | 0 | eprintln!(" {event}"); |
381 | 0 | } |
382 | 0 | } |
383 | | |
384 | | /// Print summary statistics |
385 | 0 | pub fn summary(&self) { |
386 | 0 | eprintln!("=== TRACE SUMMARY ==="); |
387 | 0 | eprintln!("Events: {}", self.events.len()); |
388 | 0 | if let Some(first) = self.events.first() { |
389 | 0 | eprintln!("First: {}", first.name); |
390 | 0 | } |
391 | 0 | if let Some(last) = self.events.last() { |
392 | 0 | eprintln!("Last: {}", last.name); |
393 | 0 | } |
394 | | |
395 | | // Find largest L2 norm |
396 | 0 | if let Some(max_event) = self.events.iter().max_by(|a, b| { |
397 | 0 | a.l2_norm.partial_cmp(&b.l2_norm).unwrap_or(std::cmp::Ordering::Equal) |
398 | 0 | }) { |
399 | 0 | eprintln!("Max L2: {} = {:.6}", max_event.name, max_event.l2_norm); |
400 | 0 | } |
401 | 0 | } |
402 | | } |
403 | | |
404 | | // Global tracers for CPU and GPU paths (thread-local to avoid contention). |
405 | | // Used by trace_cpu! and trace_gpu! macros when "trace" feature is enabled. |
406 | | #[cfg(feature = "trace")] |
407 | | #[allow(missing_docs)] |
408 | | thread_local! { |
409 | | /// Thread-local tracer for CPU inference path |
410 | | pub static CPU_TRACER: std::cell::RefCell<BrickTracer> = std::cell::RefCell::new(BrickTracer::new()); |
411 | | /// Thread-local tracer for GPU inference path |
412 | | pub static GPU_TRACER: std::cell::RefCell<BrickTracer> = std::cell::RefCell::new(BrickTracer::new()); |
413 | | } |
414 | | |
415 | | /// Log a tensor to the CPU tracer for parity debugging. |
416 | | /// |
417 | | /// Only active when the "trace" feature is enabled. When disabled, this macro |
418 | | /// compiles to nothing (zero overhead). |
419 | | /// |
420 | | /// # Arguments |
421 | | /// |
422 | | /// * `$name` - Name of the computation step (e.g., "layer0_rope_q") |
423 | | /// * `$tensor` - Slice of f32 values to log |
424 | | /// * `$pos` - (Optional) Explicit position index |
425 | | /// |
426 | | /// # Example |
427 | | /// |
428 | | /// ```rust,ignore |
429 | | /// trace_cpu!("embedding", &embedding_output); |
430 | | /// trace_cpu!("layer0_attn", &attn_out, position); |
431 | | /// ``` |
432 | | #[macro_export] |
433 | | #[cfg(feature = "trace")] |
434 | | macro_rules! trace_cpu { |
435 | | ($name:expr, $tensor:expr) => { |
436 | | $crate::brick::tracer::CPU_TRACER.with(|t| { |
437 | | t.borrow_mut().log($name, $tensor); |
438 | | }); |
439 | | }; |
440 | | ($name:expr, $tensor:expr, $pos:expr) => { |
441 | | $crate::brick::tracer::CPU_TRACER.with(|t| { |
442 | | t.borrow_mut().log_at($name, $tensor, $pos); |
443 | | }); |
444 | | }; |
445 | | } |
446 | | |
447 | | /// No-op version of trace_cpu when "trace" feature is disabled. |
448 | | #[macro_export] |
449 | | #[cfg(not(feature = "trace"))] |
450 | | macro_rules! trace_cpu { |
451 | | ($name:expr, $tensor:expr) => {}; |
452 | | ($name:expr, $tensor:expr, $pos:expr) => {}; |
453 | | } |
454 | | |
455 | | /// Log a tensor to the GPU tracer for parity debugging. |
456 | | /// |
457 | | /// Only active when the "trace" feature is enabled. When disabled, this macro |
458 | | /// compiles to nothing (zero overhead). |
459 | | /// |
460 | | /// # Arguments |
461 | | /// |
462 | | /// * `$name` - Name of the computation step (e.g., "layer0_rope_q") |
463 | | /// * `$tensor` - Slice of f32 values to log (must be downloaded from GPU first) |
464 | | /// * `$pos` - (Optional) Explicit position index |
465 | | /// |
466 | | /// # Example |
467 | | /// |
468 | | /// ```rust,ignore |
469 | | /// // After D2H copy from GPU buffer |
470 | | /// trace_gpu!("embedding", &embedding_output); |
471 | | /// trace_gpu!("layer0_attn", &attn_out, position); |
472 | | /// ``` |
473 | | #[macro_export] |
474 | | #[cfg(feature = "trace")] |
475 | | macro_rules! trace_gpu { |
476 | | ($name:expr, $tensor:expr) => { |
477 | | $crate::brick::tracer::GPU_TRACER.with(|t| { |
478 | | t.borrow_mut().log($name, $tensor); |
479 | | }); |
480 | | }; |
481 | | ($name:expr, $tensor:expr, $pos:expr) => { |
482 | | $crate::brick::tracer::GPU_TRACER.with(|t| { |
483 | | t.borrow_mut().log_at($name, $tensor, $pos); |
484 | | }); |
485 | | }; |
486 | | } |
487 | | |
488 | | /// No-op version of trace_gpu when "trace" feature is disabled. |
489 | | #[macro_export] |
490 | | #[cfg(not(feature = "trace"))] |
491 | | macro_rules! trace_gpu { |
492 | | ($name:expr, $tensor:expr) => {}; |
493 | | ($name:expr, $tensor:expr, $pos:expr) => {}; |
494 | | } |
495 | | |
496 | | // ============================================================================ |
497 | | // Tests |
498 | | // ============================================================================ |
499 | | |
500 | | #[cfg(test)] |
501 | | mod tests { |
502 | | use super::*; |
503 | | |
504 | | #[test] |
505 | 1 | fn test_trace_event_creation() { |
506 | 1 | let tensor = vec![1.0, 2.0, 3.0, 4.0, 5.0]; |
507 | 1 | let event = TraceEvent::new("test", &tensor, 0, false); |
508 | | |
509 | 1 | assert_eq!(event.name, "test"); |
510 | 1 | assert_eq!(event.position, 0); |
511 | 1 | assert_eq!(event.len, 5); |
512 | 1 | assert!((event.l2_norm - 7.416198).abs() < 0.001); // sqrt(55) |
513 | 1 | assert!((event.mean - 3.0).abs() < 0.001); |
514 | 1 | assert!((event.min - 1.0).abs() < 0.001); |
515 | 1 | assert!((event.max - 5.0).abs() < 0.001); |
516 | 1 | assert!(event.full_data.is_none()); |
517 | 1 | } |
518 | | |
519 | | #[test] |
520 | 1 | fn test_trace_event_verbose() { |
521 | 1 | let tensor = vec![1.0, 2.0, 3.0]; |
522 | 1 | let event = TraceEvent::new("test", &tensor, 0, true); |
523 | | |
524 | 1 | assert!(event.full_data.is_some()); |
525 | 1 | assert_eq!(event.full_data.unwrap(), tensor); |
526 | 1 | } |
527 | | |
528 | | #[test] |
529 | 1 | fn test_tracer_log() { |
530 | 1 | let mut tracer = BrickTracer::new(); |
531 | | |
532 | 1 | tracer.log("step1", &[1.0, 2.0, 3.0]); |
533 | 1 | tracer.log("step2", &[4.0, 5.0, 6.0]); |
534 | | |
535 | 1 | assert_eq!(tracer.events().len(), 2); |
536 | 1 | assert!(tracer.get("step1").is_some()); |
537 | 1 | assert!(tracer.get("step2").is_some()); |
538 | 1 | assert!(tracer.get("step3").is_none()); |
539 | 1 | } |
540 | | |
541 | | #[test] |
542 | 1 | fn test_tracer_comparison_match() { |
543 | 1 | let mut cpu = BrickTracer::new(); |
544 | 1 | let mut gpu = BrickTracer::new(); |
545 | | |
546 | 1 | cpu.log("embedding", &[1.0, 2.0, 3.0]); |
547 | 1 | cpu.log("layer0_norm", &[0.5, 1.0, 1.5]); |
548 | | |
549 | 1 | gpu.log("embedding", &[1.0, 2.0, 3.0]); |
550 | 1 | gpu.log("layer0_norm", &[0.5, 1.0, 1.5]); |
551 | | |
552 | 1 | let comparison = BrickTracer::compare(&cpu, &gpu, 0.01); |
553 | 1 | assert!(comparison.is_equivalent()); |
554 | 1 | } |
555 | | |
556 | | #[test] |
557 | 1 | fn test_tracer_comparison_diverge() { |
558 | 1 | let mut cpu = BrickTracer::new(); |
559 | 1 | let mut gpu = BrickTracer::new(); |
560 | | |
561 | 1 | cpu.log("embedding", &[1.0, 2.0, 3.0]); |
562 | 1 | cpu.log("layer0_norm", &[0.5, 1.0, 1.5]); |
563 | 1 | cpu.log("layer0_attn", &[0.1, 0.2, 0.3]); |
564 | | |
565 | 1 | gpu.log("embedding", &[1.0, 2.0, 3.0]); // Match |
566 | 1 | gpu.log("layer0_norm", &[0.5, 1.0, 1.5]); // Match |
567 | 1 | gpu.log("layer0_attn", &[0.5, 0.6, 0.7]); // DIVERGE! |
568 | | |
569 | 1 | let comparison = BrickTracer::compare(&cpu, &gpu, 0.01); |
570 | 1 | assert!(!comparison.is_equivalent()); |
571 | | |
572 | 1 | let first = comparison.first_divergence().unwrap(); |
573 | 1 | assert_eq!(first.name, "layer0_attn"); |
574 | 1 | } |
575 | | |
576 | | #[test] |
577 | 1 | fn test_tracer_clear() { |
578 | 1 | let mut tracer = BrickTracer::new(); |
579 | 1 | tracer.log("step1", &[1.0]); |
580 | 1 | assert_eq!(tracer.events().len(), 1); |
581 | | |
582 | 1 | tracer.clear(); |
583 | 1 | assert_eq!(tracer.events().len(), 0); |
584 | 1 | } |
585 | | |
586 | | #[test] |
587 | 1 | fn test_tracer_position() { |
588 | 1 | let mut tracer = BrickTracer::new(); |
589 | | |
590 | 1 | tracer.set_position(0); |
591 | 1 | tracer.log("pos0", &[1.0]); |
592 | | |
593 | 1 | tracer.set_position(1); |
594 | 1 | tracer.log("pos1", &[2.0]); |
595 | | |
596 | 1 | tracer.log_at("explicit", &[3.0], 5); |
597 | | |
598 | 1 | assert_eq!(tracer.get("pos0").unwrap().position, 0); |
599 | 1 | assert_eq!(tracer.get("pos1").unwrap().position, 1); |
600 | 1 | assert_eq!(tracer.get("explicit").unwrap().position, 5); |
601 | 1 | } |
602 | | |
603 | | #[test] |
604 | 1 | fn test_relative_diff() { |
605 | 1 | let event1 = TraceEvent::new("a", &[10.0], 0, false); |
606 | 1 | let event2 = TraceEvent::new("b", &[11.0], 0, false); |
607 | | |
608 | 1 | let diff = event1.relative_diff(&event2); |
609 | 1 | assert!((diff - 0.1).abs() < 0.001); // 10% difference |
610 | 1 | } |
611 | | |
612 | | #[test] |
613 | 1 | fn test_approx_eq() { |
614 | 1 | let event1 = TraceEvent::new("a", &[10.0], 0, false); |
615 | 1 | let event2 = TraceEvent::new("b", &[10.05], 0, false); |
616 | 1 | let event3 = TraceEvent::new("c", &[11.0], 0, false); |
617 | | |
618 | 1 | assert!(event1.approx_eq(&event2, 0.01)); // 1% tolerance |
619 | 1 | assert!(!event1.approx_eq(&event3, 0.01)); // 10% diff > 1% tolerance |
620 | 1 | assert!(event1.approx_eq(&event3, 0.15)); // 10% diff < 15% tolerance |
621 | 1 | } |
622 | | |
623 | | #[test] |
624 | 1 | fn test_empty_tensor() { |
625 | 1 | let event = TraceEvent::new("empty", &[], 0, false); |
626 | 1 | assert_eq!(event.len, 0); |
627 | 1 | assert_eq!(event.l2_norm, 0.0); |
628 | 1 | assert_eq!(event.mean, 0.0); |
629 | 1 | } |
630 | | } |