1use anyhow::Result;
4use scirs2_core::ndarray::{ArrayD, IxDyn};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use uuid::Uuid;
10
11use crate::activation_visualizer::ActivationVisualizer;
12use crate::tensor_inspector::TensorInspector;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub enum HookTrigger {
17 EveryForward,
19 EveryBackward,
21 EveryNSteps(usize),
23 Conditional(HookCondition),
25 Once,
27 LayerSpecific(Vec<String>),
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub enum HookCondition {
34 LossThreshold {
36 threshold: f64,
37 comparison: Comparison,
38 },
39 GradientNormThreshold {
41 threshold: f64,
42 comparison: Comparison,
43 },
44 MemoryThreshold { threshold_mb: f64 },
46 StepRange { start: usize, end: usize },
48 Custom(String),
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum Comparison {
55 Greater,
56 Less,
57 Equal,
58 GreaterEqual,
59 LessEqual,
60}
61
62impl Comparison {
63 fn apply(&self, value: f64, threshold: f64) -> bool {
68 match self {
69 Comparison::Greater => value > threshold,
70 Comparison::Less => value < threshold,
71 Comparison::GreaterEqual => value >= threshold,
72 Comparison::LessEqual => value <= threshold,
73 Comparison::Equal => (value - threshold).abs() <= 1e-9_f64.max(threshold.abs() * 1e-9),
74 }
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub enum HookAction {
81 InspectTensor,
83 TrackGradients,
85 RecordActivations,
87 SaveSnapshot { path: String },
89 Alert {
91 message: String,
92 severity: AlertSeverity,
93 },
94 CustomCallback { name: String },
96 PauseTraining,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub enum AlertSeverity {
102 Info,
103 Warning,
104 Critical,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct HookConfig {
110 pub id: Uuid,
111 pub name: String,
112 pub trigger: HookTrigger,
113 pub actions: Vec<HookAction>,
114 pub enabled: bool,
115 pub max_executions: Option<usize>,
116 pub layer_patterns: Vec<String>, }
118
119#[derive(Debug)]
121pub struct HookContext {
122 pub step: usize,
123 pub layer_name: String,
124 pub tensor_shape: Vec<usize>,
125 pub is_forward: bool,
126 pub metadata: HashMap<String, String>,
127 pub loss: Option<f64>,
133 pub grad_norm: Option<f64>,
137 pub memory_mb: Option<f64>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct HookStats {
145 pub hook_id: Uuid,
146 pub hook_name: String,
147 pub total_executions: usize,
148 pub last_execution_step: Option<usize>,
149 pub total_execution_time_ms: f64,
150 pub avg_execution_time_ms: f64,
151 pub errors: usize,
152}
153
154#[derive(Debug)]
156pub enum HookResult {
157 Success,
158 Error(String),
159 Skipped(String),
160}
161
162pub type HookCallback = Box<dyn Fn(&HookContext, &[u8]) -> Result<()> + Send + Sync>;
164
165pub struct HookManager {
167 hooks: HashMap<Uuid, HookConfig>,
168 hook_stats: HashMap<Uuid, HookStats>,
169 callbacks: HashMap<String, HookCallback>,
170 execution_count: HashMap<Uuid, usize>,
171 global_step: usize,
172 enabled: bool,
173 current_loss: Option<f64>,
181 current_grad_norm: Option<f64>,
182 current_memory_mb: Option<f64>,
183 tensor_inspector: TensorInspector,
189 activation_visualizer: ActivationVisualizer,
194 layer_tensor_ids: HashMap<String, Uuid>,
201 pause_flag: Arc<AtomicBool>,
204}
205
206impl std::fmt::Debug for HookManager {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 f.debug_struct("HookManager")
209 .field("hooks", &self.hooks)
210 .field("hook_stats", &self.hook_stats)
211 .field("execution_count", &self.execution_count)
212 .field("global_step", &self.global_step)
213 .field("enabled", &self.enabled)
214 .field("callbacks", &format!("{} callbacks", self.callbacks.len()))
215 .field("current_loss", &self.current_loss)
216 .field("current_grad_norm", &self.current_grad_norm)
217 .field("current_memory_mb", &self.current_memory_mb)
218 .field("tensor_inspector", &self.tensor_inspector)
219 .field("activation_visualizer", &self.activation_visualizer)
220 .field("layer_tensor_ids", &self.layer_tensor_ids)
221 .field("paused", &self.pause_flag.load(Ordering::Relaxed))
222 .finish()
223 }
224}
225
226impl HookManager {
227 pub fn new() -> Self {
229 Self {
230 hooks: HashMap::new(),
231 hook_stats: HashMap::new(),
232 callbacks: HashMap::new(),
233 execution_count: HashMap::new(),
234 global_step: 0,
235 enabled: true,
236 current_loss: None,
237 current_grad_norm: None,
238 current_memory_mb: None,
239 tensor_inspector: TensorInspector::new(&crate::DebugConfig::default()),
240 activation_visualizer: ActivationVisualizer::new(),
241 layer_tensor_ids: HashMap::new(),
242 pause_flag: Arc::new(AtomicBool::new(false)),
243 }
244 }
245
246 pub fn register_hook(&mut self, config: HookConfig) -> Result<Uuid> {
248 let hook_id = config.id;
249
250 self.hook_stats.insert(
252 hook_id,
253 HookStats {
254 hook_id,
255 hook_name: config.name.clone(),
256 total_executions: 0,
257 last_execution_step: None,
258 total_execution_time_ms: 0.0,
259 avg_execution_time_ms: 0.0,
260 errors: 0,
261 },
262 );
263
264 self.execution_count.insert(hook_id, 0);
265 self.hooks.insert(hook_id, config);
266
267 tracing::debug!("Registered hook {}", hook_id);
268 Ok(hook_id)
269 }
270
271 pub fn register_callback(&mut self, name: String, callback: HookCallback) {
273 self.callbacks.insert(name, callback);
274 }
275
276 pub fn remove_hook(&mut self, hook_id: Uuid) -> Option<HookConfig> {
278 self.hook_stats.remove(&hook_id);
279 self.execution_count.remove(&hook_id);
280 self.hooks.remove(&hook_id)
281 }
282
283 pub fn set_hook_enabled(&mut self, hook_id: Uuid, enabled: bool) -> Result<()> {
285 if let Some(hook) = self.hooks.get_mut(&hook_id) {
286 hook.enabled = enabled;
287 Ok(())
288 } else {
289 Err(anyhow::anyhow!("Hook {} not found", hook_id))
290 }
291 }
292
293 pub fn set_enabled(&mut self, enabled: bool) {
295 self.enabled = enabled;
296 }
297
298 pub fn set_step(&mut self, step: usize) {
300 self.global_step = step;
301 }
302
303 pub fn set_loss(&mut self, loss: f64) {
308 self.current_loss = Some(loss);
309 }
310
311 pub fn set_gradient_norm(&mut self, grad_norm: f64) {
315 self.current_grad_norm = Some(grad_norm);
316 }
317
318 pub fn set_memory_mb(&mut self, memory_mb: f64) {
321 self.current_memory_mb = Some(memory_mb);
322 }
323
324 pub fn tensor_inspector(&self) -> &TensorInspector {
329 &self.tensor_inspector
330 }
331
332 pub fn tensor_inspector_mut(&mut self) -> &mut TensorInspector {
335 &mut self.tensor_inspector
336 }
337
338 pub fn activation_visualizer(&self) -> &ActivationVisualizer {
341 &self.activation_visualizer
342 }
343
344 pub fn activation_visualizer_mut(&mut self) -> &mut ActivationVisualizer {
346 &mut self.activation_visualizer
347 }
348
349 pub fn pause_flag(&self) -> Arc<AtomicBool> {
376 Arc::clone(&self.pause_flag)
377 }
378
379 pub fn is_paused(&self) -> bool {
383 self.pause_flag.load(Ordering::SeqCst)
384 }
385
386 pub fn resume_training(&self) {
389 self.pause_flag.store(false, Ordering::SeqCst);
390 }
391
392 pub fn execute_hooks<T>(
394 &mut self,
395 layer_name: &str,
396 tensor_data: &[T],
397 tensor_shape: &[usize],
398 is_forward: bool,
399 metadata: Option<HashMap<String, String>>,
400 ) -> Vec<(Uuid, HookResult)>
401 where
402 T: Clone + Into<f64> + 'static,
403 {
404 if !self.enabled {
405 return Vec::new();
406 }
407
408 let context = HookContext {
409 step: self.global_step,
410 layer_name: layer_name.to_string(),
411 tensor_shape: tensor_shape.to_vec(),
412 is_forward,
413 metadata: metadata.unwrap_or_default(),
414 loss: self.current_loss,
415 grad_norm: self.current_grad_norm,
416 memory_mb: self.current_memory_mb,
417 };
418
419 let mut results = Vec::new();
420
421 let tensor_bytes = unsafe {
424 std::slice::from_raw_parts(
425 tensor_data.as_ptr() as *const u8,
426 std::mem::size_of_val(tensor_data),
427 )
428 };
429 let tensor_values: Vec<f64> = tensor_data.iter().cloned().map(Into::into).collect();
434
435 let hooks_to_execute: Vec<(Uuid, HookConfig)> =
437 self.hooks.iter().map(|(id, config)| (*id, config.clone())).collect();
438
439 for (hook_id, hook_config) in hooks_to_execute {
440 if !hook_config.enabled {
441 continue;
442 }
443
444 if let Some(should_execute) = self.should_execute_hook(&hook_config, &context) {
446 if !should_execute {
447 results.push((
448 hook_id,
449 HookResult::Skipped("Condition not met".to_string()),
450 ));
451 continue;
452 }
453 }
454
455 let current_count = self.execution_count.get(&hook_id).copied().unwrap_or(0);
457 if let Some(max_executions) = hook_config.max_executions {
458 if current_count >= max_executions {
459 results.push((
460 hook_id,
461 HookResult::Skipped("Max executions reached".to_string()),
462 ));
463 continue;
464 }
465 }
466
467 let start_time = std::time::Instant::now();
469 let result =
470 self.execute_single_hook(&hook_config, &context, tensor_bytes, &tensor_values);
471 let execution_time = start_time.elapsed().as_millis() as f64;
472
473 if let Some(stats) = self.hook_stats.get_mut(&hook_id) {
475 stats.total_executions += 1;
476 stats.last_execution_step = Some(self.global_step);
477 stats.total_execution_time_ms += execution_time;
478 stats.avg_execution_time_ms =
479 stats.total_execution_time_ms / stats.total_executions as f64;
480
481 if matches!(result, HookResult::Error(_)) {
482 stats.errors += 1;
483 }
484 }
485
486 if let Some(count) = self.execution_count.get_mut(&hook_id) {
488 *count += 1;
489 }
490
491 results.push((hook_id, result));
492 }
493
494 results
495 }
496
497 pub fn get_hook(&self, hook_id: Uuid) -> Option<&HookConfig> {
499 self.hooks.get(&hook_id)
500 }
501
502 pub fn get_all_hooks(&self) -> Vec<&HookConfig> {
504 self.hooks.values().collect()
505 }
506
507 pub fn get_hook_stats(&self, hook_id: Uuid) -> Option<&HookStats> {
509 self.hook_stats.get(&hook_id)
510 }
511
512 pub fn get_all_stats(&self) -> Vec<&HookStats> {
514 self.hook_stats.values().collect()
515 }
516
517 pub fn clear_hooks(&mut self) {
519 self.hooks.clear();
520 self.hook_stats.clear();
521 self.execution_count.clear();
522 self.callbacks.clear();
523 }
524
525 pub fn create_tensor_inspection_hook(&mut self, layer_patterns: Vec<String>) -> Result<Uuid> {
527 let config = HookConfig {
528 id: Uuid::new_v4(),
529 name: "Tensor Inspector".to_string(),
530 trigger: HookTrigger::EveryForward,
531 actions: vec![HookAction::InspectTensor],
532 enabled: true,
533 max_executions: None,
534 layer_patterns,
535 };
536
537 self.register_hook(config)
538 }
539
540 pub fn create_gradient_tracking_hook(&mut self, layer_patterns: Vec<String>) -> Result<Uuid> {
542 let config = HookConfig {
543 id: Uuid::new_v4(),
544 name: "Gradient Tracker".to_string(),
545 trigger: HookTrigger::EveryBackward,
546 actions: vec![HookAction::TrackGradients],
547 enabled: true,
548 max_executions: None,
549 layer_patterns,
550 };
551
552 self.register_hook(config)
553 }
554
555 pub fn create_alert_hook(
557 &mut self,
558 condition: HookCondition,
559 message: String,
560 severity: AlertSeverity,
561 ) -> Result<Uuid> {
562 let config = HookConfig {
563 id: Uuid::new_v4(),
564 name: "Alert Hook".to_string(),
565 trigger: HookTrigger::Conditional(condition),
566 actions: vec![HookAction::Alert { message, severity }],
567 enabled: true,
568 max_executions: None,
569 layer_patterns: vec![".*".to_string()], };
571
572 self.register_hook(config)
573 }
574
575 fn should_execute_hook(&self, hook: &HookConfig, context: &HookContext) -> Option<bool> {
578 if !hook.layer_patterns.is_empty() {
580 let matches_pattern = hook.layer_patterns.iter().any(|pattern| {
581 regex::Regex::new(pattern)
582 .map(|re| re.is_match(&context.layer_name))
583 .unwrap_or(false)
584 });
585
586 if !matches_pattern {
587 return Some(false);
588 }
589 }
590
591 match &hook.trigger {
592 HookTrigger::EveryForward => Some(context.is_forward),
593 HookTrigger::EveryBackward => Some(!context.is_forward),
594 HookTrigger::EveryNSteps(n) => Some(context.step.is_multiple_of(*n)),
595 HookTrigger::Conditional(condition) => {
596 Some(self.evaluate_condition(condition, context))
597 },
598 HookTrigger::Once => {
599 let count = self.execution_count.get(&hook.id).copied().unwrap_or(0);
600 Some(count == 0)
601 },
602 HookTrigger::LayerSpecific(layers) => Some(layers.contains(&context.layer_name)),
603 }
604 }
605
606 fn evaluate_condition(&self, condition: &HookCondition, context: &HookContext) -> bool {
619 match condition {
620 HookCondition::StepRange { start, end } => {
621 context.step >= *start && context.step <= *end
622 },
623 HookCondition::Custom(name) => {
624 context.metadata.contains_key(name)
629 },
630 HookCondition::LossThreshold {
631 threshold,
632 comparison,
633 } => context.loss.map(|loss| comparison.apply(loss, *threshold)).unwrap_or(false),
634 HookCondition::GradientNormThreshold {
635 threshold,
636 comparison,
637 } => context
638 .grad_norm
639 .map(|grad_norm| comparison.apply(grad_norm, *threshold))
640 .unwrap_or(false),
641 HookCondition::MemoryThreshold { threshold_mb } => {
642 context.memory_mb.map(|memory_mb| memory_mb > *threshold_mb).unwrap_or(false)
643 },
644 }
645 }
646
647 fn execute_single_hook(
648 &mut self,
649 hook: &HookConfig,
650 context: &HookContext,
651 tensor_data: &[u8],
652 tensor_values: &[f64],
653 ) -> HookResult {
654 for action in &hook.actions {
655 match self.execute_action(action, context, tensor_data, tensor_values) {
656 Ok(()) => continue,
657 Err(e) => return HookResult::Error(e.to_string()),
658 }
659 }
660 HookResult::Success
661 }
662
663 fn build_array(context: &HookContext, tensor_values: &[f64]) -> Result<ArrayD<f64>> {
668 ArrayD::from_shape_vec(IxDyn(&context.tensor_shape), tensor_values.to_vec()).map_err(|e| {
669 anyhow::anyhow!(
670 "hook tensor shape {:?} does not match {} data element(s) reported for \
671 layer '{}': {}",
672 context.tensor_shape,
673 tensor_values.len(),
674 context.layer_name,
675 e
676 )
677 })
678 }
679
680 fn inspect_and_track(
687 &mut self,
688 context: &HookContext,
689 tensor_values: &[f64],
690 operation: &str,
691 ) -> Result<Uuid> {
692 let array = Self::build_array(context, tensor_values)?;
693 let id = self.tensor_inspector.inspect_tensor(
694 &array,
695 &context.layer_name,
696 Some(context.layer_name.as_str()),
697 Some(operation),
698 )?;
699 self.layer_tensor_ids.insert(context.layer_name.clone(), id);
700 Ok(id)
701 }
702
703 fn execute_action(
704 &mut self,
705 action: &HookAction,
706 context: &HookContext,
707 tensor_data: &[u8],
708 tensor_values: &[f64],
709 ) -> Result<()> {
710 match action {
711 HookAction::InspectTensor => {
712 let id = self.inspect_and_track(context, tensor_values, "hook: InspectTensor")?;
713 tracing::debug!(
714 "Inspected tensor in layer '{}' at step {} -> tensor id {}",
715 context.layer_name,
716 context.step,
717 id
718 );
719 Ok(())
720 },
721 HookAction::TrackGradients => {
722 let id = if let Some(&existing) = self.layer_tensor_ids.get(&context.layer_name) {
728 let array = Self::build_array(context, tensor_values)?;
729 self.tensor_inspector.inspect_gradients(existing, &array)?;
730 existing
731 } else {
732 self.inspect_and_track(
733 context,
734 tensor_values,
735 "hook: TrackGradients (no prior forward tensor tracked for this layer)",
736 )?
737 };
738 tracing::debug!(
739 "Tracked gradients in layer '{}' at step {} -> tensor id {}",
740 context.layer_name,
741 context.step,
742 id
743 );
744 Ok(())
745 },
746 HookAction::RecordActivations => {
747 let values_f32: Vec<f32> = tensor_values.iter().map(|&v| v as f32).collect();
748 self.activation_visualizer.register(
749 &context.layer_name,
750 values_f32,
751 context.tensor_shape.clone(),
752 )?;
753 tracing::debug!(
754 "Recorded {} activation value(s) in layer '{}' at step {}",
755 tensor_values.len(),
756 context.layer_name,
757 context.step
758 );
759 Ok(())
760 },
761 HookAction::SaveSnapshot { path } => {
762 let file_path =
763 format!("{}_{}_step_{}.bin", path, context.layer_name, context.step);
764 std::fs::write(&file_path, tensor_data)?;
765 tracing::info!("Saved tensor snapshot to {}", file_path);
766 Ok(())
767 },
768 HookAction::Alert { message, severity } => {
769 match severity {
770 AlertSeverity::Info => tracing::info!("Hook Alert: {}", message),
771 AlertSeverity::Warning => tracing::warn!("Hook Alert: {}", message),
772 AlertSeverity::Critical => tracing::error!("Hook Alert: {}", message),
773 }
774 Ok(())
775 },
776 HookAction::CustomCallback { name } => {
777 if let Some(callback) = self.callbacks.get(name) {
778 callback(context, tensor_data)?;
779 } else {
780 return Err(anyhow::anyhow!("Callback '{}' not found", name));
781 }
782 Ok(())
783 },
784 HookAction::PauseTraining => {
785 self.pause_flag.store(true, Ordering::SeqCst);
790 tracing::warn!(
791 "Training paused by hook at step {} in layer '{}' -- poll \
792 HookManager::is_paused()/pause_flag() from the training loop to observe \
793 this, and call HookManager::resume_training() to clear it",
794 context.step,
795 context.layer_name
796 );
797 Ok(())
798 },
799 }
800 }
801}
802
803impl Default for HookManager {
804 fn default() -> Self {
805 Self::new()
806 }
807}
808
809pub struct HookBuilder {
811 config: HookConfig,
812}
813
814impl HookBuilder {
815 pub fn new(name: &str) -> Self {
816 Self {
817 config: HookConfig {
818 id: Uuid::new_v4(),
819 name: name.to_string(),
820 trigger: HookTrigger::EveryForward,
821 actions: Vec::new(),
822 enabled: true,
823 max_executions: None,
824 layer_patterns: Vec::new(),
825 },
826 }
827 }
828
829 pub fn trigger(mut self, trigger: HookTrigger) -> Self {
830 self.config.trigger = trigger;
831 self
832 }
833
834 pub fn action(mut self, action: HookAction) -> Self {
835 self.config.actions.push(action);
836 self
837 }
838
839 pub fn actions(mut self, actions: Vec<HookAction>) -> Self {
840 self.config.actions = actions;
841 self
842 }
843
844 pub fn max_executions(mut self, max: usize) -> Self {
845 self.config.max_executions = Some(max);
846 self
847 }
848
849 pub fn layer_patterns(mut self, patterns: Vec<String>) -> Self {
850 self.config.layer_patterns = patterns;
851 self
852 }
853
854 pub fn enabled(mut self, enabled: bool) -> Self {
855 self.config.enabled = enabled;
856 self
857 }
858
859 pub fn build(self) -> HookConfig {
860 self.config
861 }
862}
863
864#[macro_export]
866macro_rules! tensor_hook {
867 ($name:expr, $patterns:expr) => {
868 HookBuilder::new($name)
869 .trigger(HookTrigger::EveryForward)
870 .action(HookAction::InspectTensor)
871 .layer_patterns($patterns)
872 .build()
873 };
874}
875
876#[macro_export]
877macro_rules! gradient_hook {
878 ($name:expr, $patterns:expr) => {
879 HookBuilder::new($name)
880 .trigger(HookTrigger::EveryBackward)
881 .action(HookAction::TrackGradients)
882 .layer_patterns($patterns)
883 .build()
884 };
885}
886
887#[macro_export]
888macro_rules! alert_hook {
889 ($condition:expr, $message:expr, $severity:expr) => {
890 HookBuilder::new("Alert Hook")
891 .trigger(HookTrigger::Conditional($condition))
892 .action(HookAction::Alert {
893 message: $message.to_string(),
894 severity: $severity,
895 })
896 .build()
897 };
898}
899
900#[cfg(test)]
905mod tests {
906 use super::*;
907
908 fn make_hook_config(name: &str, trigger: HookTrigger) -> HookConfig {
909 HookConfig {
910 id: Uuid::new_v4(),
911 name: name.to_string(),
912 trigger,
913 actions: vec![HookAction::InspectTensor],
914 enabled: true,
915 max_executions: None,
916 layer_patterns: vec![],
917 }
918 }
919
920 #[test]
923 fn test_hook_manager_new_defaults() {
924 let mgr = HookManager::new();
925 assert!(mgr.enabled);
926 assert_eq!(mgr.global_step, 0);
927 assert!(mgr.get_all_hooks().is_empty());
928 assert!(mgr.get_all_stats().is_empty());
929 }
930
931 #[test]
932 fn test_hook_manager_default_equals_new() {
933 let mgr = HookManager::default();
934 assert!(mgr.enabled);
935 }
936
937 #[test]
940 fn test_register_hook_returns_uuid() {
941 let mut mgr = HookManager::new();
942 let config = make_hook_config("test", HookTrigger::EveryForward);
943 let id = config.id;
944 let returned = mgr.register_hook(config).expect("register should succeed");
945 assert_eq!(returned, id);
946 }
947
948 #[test]
949 fn test_register_multiple_hooks() {
950 let mut mgr = HookManager::new();
951 for i in 0..5 {
952 let cfg = make_hook_config(&format!("h{}", i), HookTrigger::EveryForward);
953 mgr.register_hook(cfg).expect("register should succeed");
954 }
955 assert_eq!(mgr.get_all_hooks().len(), 5);
956 }
957
958 #[test]
959 fn test_hook_stats_initialized_on_register() {
960 let mut mgr = HookManager::new();
961 let cfg = make_hook_config("h0", HookTrigger::EveryForward);
962 let id = mgr.register_hook(cfg).expect("register should succeed");
963 let stats = mgr.get_hook_stats(id).expect("stats should exist");
964 assert_eq!(stats.total_executions, 0);
965 assert_eq!(stats.errors, 0);
966 }
967
968 #[test]
971 fn test_remove_hook_returns_config() {
972 let mut mgr = HookManager::new();
973 let cfg = make_hook_config("remove_me", HookTrigger::EveryBackward);
974 let id = mgr.register_hook(cfg).expect("register");
975 let removed = mgr.remove_hook(id);
976 assert!(removed.is_some());
977 assert_eq!(removed.expect("should be some").name, "remove_me");
978 }
979
980 #[test]
981 fn test_remove_nonexistent_hook_returns_none() {
982 let mut mgr = HookManager::new();
983 let id = Uuid::new_v4();
984 assert!(mgr.remove_hook(id).is_none());
985 }
986
987 #[test]
990 fn test_set_hook_enabled_ok() {
991 let mut mgr = HookManager::new();
992 let cfg = make_hook_config("h", HookTrigger::EveryForward);
993 let id = mgr.register_hook(cfg).expect("register");
994 mgr.set_hook_enabled(id, false).expect("should succeed");
995 let hook = mgr.get_hook(id).expect("hook should exist");
996 assert!(!hook.enabled);
997 mgr.set_hook_enabled(id, true).expect("re-enable");
998 let hook = mgr.get_hook(id).expect("hook should exist");
999 assert!(hook.enabled);
1000 }
1001
1002 #[test]
1003 fn test_set_hook_enabled_nonexistent_errors() {
1004 let mut mgr = HookManager::new();
1005 let result = mgr.set_hook_enabled(Uuid::new_v4(), true);
1006 assert!(result.is_err());
1007 }
1008
1009 #[test]
1012 fn test_global_disable_stops_execution() {
1013 let mut mgr = HookManager::new();
1014 mgr.set_enabled(false);
1015 mgr.register_hook(make_hook_config("h", HookTrigger::EveryForward))
1016 .expect("register");
1017 let results = mgr.execute_hooks("layer", &[1u8, 2u8], &[2], true, None);
1018 assert!(
1019 results.is_empty(),
1020 "globally disabled manager should execute nothing"
1021 );
1022 }
1023
1024 #[test]
1027 fn test_set_step_updates_counter() {
1028 let mut mgr = HookManager::new();
1029 mgr.set_step(42);
1030 assert_eq!(mgr.global_step, 42);
1031 }
1032
1033 #[test]
1036 fn test_execute_hooks_disabled_hook_skipped() {
1037 let mut mgr = HookManager::new();
1038 let mut cfg = make_hook_config("h", HookTrigger::EveryForward);
1039 cfg.enabled = false;
1040 mgr.register_hook(cfg).expect("register");
1041 let results = mgr.execute_hooks("layer", &[0u8], &[1], true, None);
1042 assert_eq!(results.len(), 0);
1044 }
1045
1046 #[test]
1047 fn test_execute_hooks_every_forward_fires_on_forward() {
1048 let mut mgr = HookManager::new();
1049 mgr.register_hook(make_hook_config("h", HookTrigger::EveryForward))
1050 .expect("register");
1051 let results = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1052 assert_eq!(results.len(), 1);
1053 }
1054
1055 #[test]
1056 fn test_execute_hooks_every_forward_skipped_on_backward() {
1057 let mut mgr = HookManager::new();
1058 let cfg = make_hook_config("h", HookTrigger::EveryForward);
1060 mgr.register_hook(cfg).expect("register");
1061 let results = mgr.execute_hooks("layer", &[1u8], &[1], false, None);
1062 assert_eq!(results.len(), 1);
1064 let (_, ref outcome) = results[0];
1065 assert!(matches!(outcome, HookResult::Skipped(_)));
1066 }
1067
1068 #[test]
1069 fn test_execute_hooks_max_executions_respected() {
1070 let mut mgr = HookManager::new();
1071 let mut cfg = make_hook_config("once", HookTrigger::EveryForward);
1072 cfg.max_executions = Some(1);
1073 mgr.register_hook(cfg).expect("register");
1074
1075 let r1 = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1077 assert_eq!(r1.len(), 1);
1078 assert!(matches!(r1[0].1, HookResult::Success));
1079
1080 let r2 = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1082 assert_eq!(r2.len(), 1);
1083 assert!(matches!(r2[0].1, HookResult::Skipped(_)));
1084 }
1085
1086 fn make_conditional_hook_config(condition: HookCondition) -> HookConfig {
1095 HookConfig {
1096 id: Uuid::new_v4(),
1097 name: "conditional".to_string(),
1098 trigger: HookTrigger::Conditional(condition),
1099 actions: vec![HookAction::InspectTensor],
1100 enabled: true,
1101 max_executions: None,
1102 layer_patterns: vec![],
1103 }
1104 }
1105
1106 #[test]
1107 fn test_loss_threshold_does_not_fire_without_reported_loss() {
1108 let mut mgr = HookManager::new();
1109 let cond = HookCondition::LossThreshold {
1110 threshold: 1.0,
1111 comparison: Comparison::Greater,
1112 };
1113 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1114
1115 let results = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1118 assert_eq!(results.len(), 1);
1119 assert!(
1120 matches!(results[0].1, HookResult::Skipped(_)),
1121 "must not fire when no loss has been reported, got {:?}",
1122 results[0].1
1123 );
1124 }
1125
1126 #[test]
1127 fn test_loss_threshold_fires_when_exceeded() {
1128 let mut mgr = HookManager::new();
1129 let cond = HookCondition::LossThreshold {
1130 threshold: 1.0,
1131 comparison: Comparison::Greater,
1132 };
1133 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1134
1135 mgr.set_loss(5.0); let results = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1137 assert_eq!(results.len(), 1);
1138 assert!(matches!(results[0].1, HookResult::Success));
1139 }
1140
1141 #[test]
1142 fn test_loss_threshold_does_not_fire_when_below_threshold() {
1143 let mut mgr = HookManager::new();
1144 let cond = HookCondition::LossThreshold {
1145 threshold: 1.0,
1146 comparison: Comparison::Greater,
1147 };
1148 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1149
1150 mgr.set_loss(0.1); let results = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1152 assert_eq!(results.len(), 1);
1153 assert!(
1154 matches!(results[0].1, HookResult::Skipped(_)),
1155 "must not fire when loss is below the threshold, got {:?}",
1156 results[0].1
1157 );
1158 }
1159
1160 #[test]
1161 fn test_gradient_norm_threshold_does_not_fire_without_reported_norm() {
1162 let mut mgr = HookManager::new();
1163 let cond = HookCondition::GradientNormThreshold {
1164 threshold: 10.0,
1165 comparison: Comparison::Greater,
1166 };
1167 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1168
1169 let results = mgr.execute_hooks("layer", &[1u8], &[1], false, None);
1170 assert_eq!(results.len(), 1);
1171 assert!(matches!(results[0].1, HookResult::Skipped(_)));
1172 }
1173
1174 #[test]
1175 fn test_gradient_norm_threshold_fires_on_explosion() {
1176 let mut mgr = HookManager::new();
1177 let cond = HookCondition::GradientNormThreshold {
1178 threshold: 10.0,
1179 comparison: Comparison::Greater,
1180 };
1181 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1182
1183 mgr.set_gradient_norm(1000.0); let results = mgr.execute_hooks("layer", &[1u8], &[1], false, None);
1185 assert_eq!(results.len(), 1);
1186 assert!(matches!(results[0].1, HookResult::Success));
1187 }
1188
1189 #[test]
1190 fn test_gradient_norm_threshold_respects_less_comparison() {
1191 let mut mgr = HookManager::new();
1192 let cond = HookCondition::GradientNormThreshold {
1194 threshold: 1e-6,
1195 comparison: Comparison::Less,
1196 };
1197 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1198
1199 mgr.set_gradient_norm(0.5); let healthy = mgr.execute_hooks("layer", &[1u8], &[1], false, None);
1201 assert!(matches!(healthy[0].1, HookResult::Skipped(_)));
1202
1203 mgr.set_gradient_norm(1e-9); let vanished = mgr.execute_hooks("layer", &[1u8], &[1], false, None);
1205 assert!(matches!(vanished[0].1, HookResult::Success));
1206 }
1207
1208 #[test]
1209 fn test_memory_threshold_does_not_fire_without_reported_memory() {
1210 let mut mgr = HookManager::new();
1211 let cond = HookCondition::MemoryThreshold {
1212 threshold_mb: 1000.0,
1213 };
1214 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1215
1216 let results = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1217 assert_eq!(results.len(), 1);
1218 assert!(matches!(results[0].1, HookResult::Skipped(_)));
1219 }
1220
1221 #[test]
1222 fn test_memory_threshold_fires_over_limit() {
1223 let mut mgr = HookManager::new();
1224 let cond = HookCondition::MemoryThreshold {
1225 threshold_mb: 1000.0,
1226 };
1227 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1228
1229 mgr.set_memory_mb(4096.0);
1230 let results = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1231 assert_eq!(results.len(), 1);
1232 assert!(matches!(results[0].1, HookResult::Success));
1233 }
1234
1235 #[test]
1236 fn test_memory_threshold_does_not_fire_under_limit() {
1237 let mut mgr = HookManager::new();
1238 let cond = HookCondition::MemoryThreshold {
1239 threshold_mb: 1000.0,
1240 };
1241 mgr.register_hook(make_conditional_hook_config(cond)).expect("register");
1242
1243 mgr.set_memory_mb(50.0);
1244 let results = mgr.execute_hooks("layer", &[1u8], &[1], true, None);
1245 assert_eq!(results.len(), 1);
1246 assert!(matches!(results[0].1, HookResult::Skipped(_)));
1247 }
1248
1249 #[test]
1250 fn test_comparison_apply_all_variants() {
1251 assert!(Comparison::Greater.apply(2.0, 1.0));
1252 assert!(!Comparison::Greater.apply(1.0, 1.0));
1253 assert!(Comparison::Less.apply(0.5, 1.0));
1254 assert!(!Comparison::Less.apply(1.0, 1.0));
1255 assert!(Comparison::GreaterEqual.apply(1.0, 1.0));
1256 assert!(Comparison::LessEqual.apply(1.0, 1.0));
1257 assert!(Comparison::Equal.apply(1.0, 1.0));
1258 assert!(!Comparison::Equal.apply(1.5, 1.0));
1259 }
1260
1261 #[test]
1264 fn test_clear_hooks_empties_everything() {
1265 let mut mgr = HookManager::new();
1266 mgr.register_hook(make_hook_config("h0", HookTrigger::EveryForward))
1267 .expect("register");
1268 mgr.register_hook(make_hook_config("h1", HookTrigger::EveryBackward))
1269 .expect("register");
1270 mgr.clear_hooks();
1271 assert!(mgr.get_all_hooks().is_empty());
1272 assert!(mgr.get_all_stats().is_empty());
1273 }
1274
1275 #[test]
1278 fn test_create_tensor_inspection_hook() {
1279 let mut mgr = HookManager::new();
1280 let id = mgr
1281 .create_tensor_inspection_hook(vec!["attention.*".to_string()])
1282 .expect("should succeed");
1283 assert!(mgr.get_hook(id).is_some());
1284 }
1285
1286 #[test]
1287 fn test_create_gradient_tracking_hook() {
1288 let mut mgr = HookManager::new();
1289 let id = mgr
1290 .create_gradient_tracking_hook(vec!["fc.*".to_string()])
1291 .expect("should succeed");
1292 let hook = mgr.get_hook(id).expect("should exist");
1293 assert!(matches!(hook.trigger, HookTrigger::EveryBackward));
1294 }
1295
1296 #[test]
1297 fn test_create_alert_hook() {
1298 let mut mgr = HookManager::new();
1299 let cond = HookCondition::StepRange { start: 0, end: 100 };
1300 let id = mgr
1301 .create_alert_hook(cond, "loss exploded".to_string(), AlertSeverity::Critical)
1302 .expect("should succeed");
1303 let hook = mgr.get_hook(id).expect("should exist");
1304 assert!(matches!(hook.trigger, HookTrigger::Conditional(_)));
1305 }
1306
1307 #[test]
1310 fn test_hook_builder_basic() {
1311 let cfg = HookBuilder::new("my_hook")
1312 .trigger(HookTrigger::EveryNSteps(10))
1313 .action(HookAction::TrackGradients)
1314 .max_executions(50)
1315 .layer_patterns(vec!["norm".to_string()])
1316 .enabled(true)
1317 .build();
1318
1319 assert_eq!(cfg.name, "my_hook");
1320 assert!(matches!(cfg.trigger, HookTrigger::EveryNSteps(10)));
1321 assert_eq!(cfg.max_executions, Some(50));
1322 assert!(cfg.enabled);
1323 }
1324
1325 #[test]
1328 fn test_hook_trigger_variants() {
1329 let triggers: Vec<String> = vec![
1330 format!("{:?}", HookTrigger::EveryForward),
1331 format!("{:?}", HookTrigger::EveryBackward),
1332 format!("{:?}", HookTrigger::EveryNSteps(5)),
1333 format!("{:?}", HookTrigger::Once),
1334 format!("{:?}", HookTrigger::LayerSpecific(vec![])),
1335 ];
1336 for t in &triggers {
1337 assert!(!t.is_empty());
1338 }
1339 }
1340
1341 #[test]
1342 fn test_hook_action_variants() {
1343 let actions: Vec<String> = vec![
1344 format!("{:?}", HookAction::InspectTensor),
1345 format!("{:?}", HookAction::TrackGradients),
1346 format!("{:?}", HookAction::RecordActivations),
1347 format!(
1348 "{:?}",
1349 HookAction::SaveSnapshot {
1350 path: "/tmp".to_string()
1351 }
1352 ),
1353 format!(
1354 "{:?}",
1355 HookAction::Alert {
1356 message: "x".to_string(),
1357 severity: AlertSeverity::Info
1358 }
1359 ),
1360 format!(
1361 "{:?}",
1362 HookAction::CustomCallback {
1363 name: "cb".to_string()
1364 }
1365 ),
1366 format!("{:?}", HookAction::PauseTraining),
1367 ];
1368 for a in &actions {
1369 assert!(!a.is_empty());
1370 }
1371 }
1372
1373 #[test]
1374 fn test_alert_severity_variants() {
1375 let severities = [
1376 AlertSeverity::Info,
1377 AlertSeverity::Warning,
1378 AlertSeverity::Critical,
1379 ];
1380 for s in &severities {
1381 assert!(!format!("{:?}", s).is_empty());
1382 }
1383 }
1384
1385 #[test]
1386 fn test_comparison_variants() {
1387 let comps = [
1388 Comparison::Greater,
1389 Comparison::Less,
1390 Comparison::Equal,
1391 Comparison::GreaterEqual,
1392 Comparison::LessEqual,
1393 ];
1394 for c in &comps {
1395 assert!(!format!("{:?}", c).is_empty());
1396 }
1397 }
1398
1399 #[test]
1400 fn test_hook_stats_fields() {
1401 let id = Uuid::new_v4();
1402 let stats = HookStats {
1403 hook_id: id,
1404 hook_name: "perf_hook".to_string(),
1405 total_executions: 100,
1406 last_execution_step: Some(99),
1407 total_execution_time_ms: 500.0,
1408 avg_execution_time_ms: 5.0,
1409 errors: 2,
1410 };
1411 assert_eq!(stats.total_executions, 100);
1412 assert_eq!(stats.errors, 2);
1413 assert_eq!(stats.last_execution_step, Some(99));
1414 }
1415
1416 fn make_action_hook(action: HookAction, trigger: HookTrigger) -> HookConfig {
1425 HookConfig {
1426 id: Uuid::new_v4(),
1427 name: "action_hook".to_string(),
1428 trigger,
1429 actions: vec![action],
1430 enabled: true,
1431 max_executions: None,
1432 layer_patterns: vec![],
1433 }
1434 }
1435
1436 #[test]
1437 fn test_inspect_tensor_computes_real_statistics() {
1438 let mut mgr = HookManager::new();
1439 mgr.register_hook(make_action_hook(
1440 HookAction::InspectTensor,
1441 HookTrigger::EveryForward,
1442 ))
1443 .expect("register");
1444
1445 let data = [2.0f64, 4.0, 6.0, 8.0];
1446 let results = mgr.execute_hooks("dense", &data, &[4], true, None);
1447 assert_eq!(results.len(), 1);
1448 assert!(
1449 matches!(results[0].1, HookResult::Success),
1450 "got {:?}",
1451 results[0].1
1452 );
1453
1454 let tracked = mgr.tensor_inspector().get_all_tensors();
1455 assert_eq!(
1456 tracked.len(),
1457 1,
1458 "InspectTensor must actually register a tracked tensor"
1459 );
1460 let info = tracked[0];
1461 assert_eq!(info.layer_name.as_deref(), Some("dense"));
1462 assert!((info.stats.mean - 5.0).abs() < 1e-9);
1464 assert_eq!(info.stats.min, 2.0);
1465 assert_eq!(info.stats.max, 8.0);
1466 assert_eq!(info.stats.total_elements, 4);
1467 }
1468
1469 #[test]
1470 fn test_inspect_tensor_flags_real_nan_alert() {
1471 let mut mgr = HookManager::new();
1472 mgr.register_hook(make_action_hook(
1473 HookAction::InspectTensor,
1474 HookTrigger::EveryForward,
1475 ))
1476 .expect("register");
1477
1478 let data = [1.0f64, f64::NAN, 3.0];
1479 let results = mgr.execute_hooks("nan_layer", &data, &[3], true, None);
1480 assert!(matches!(results[0].1, HookResult::Success));
1481
1482 let alerts = mgr.tensor_inspector().get_alerts();
1483 assert!(
1484 alerts.iter().any(|a| a.tensor_name == "nan_layer"
1485 && matches!(
1486 a.alert_type,
1487 crate::tensor_inspector::TensorAlertType::NaNValues
1488 )),
1489 "a real NaN in the tensor must produce a real NaN alert, got {:?}",
1490 alerts
1491 );
1492 }
1493
1494 #[test]
1495 fn test_track_gradients_links_real_stats_to_prior_forward_tensor() {
1496 let mut mgr = HookManager::new();
1497 let inspect_id = mgr
1501 .register_hook(make_action_hook(
1502 HookAction::InspectTensor,
1503 HookTrigger::EveryForward,
1504 ))
1505 .expect("register forward hook");
1506 let grad_id = mgr
1507 .register_hook(make_action_hook(
1508 HookAction::TrackGradients,
1509 HookTrigger::EveryBackward,
1510 ))
1511 .expect("register backward hook");
1512
1513 let activations = [1.0f64, 2.0, 3.0];
1515 let fwd = mgr.execute_hooks("linear", &activations, &[3], true, None);
1516 let fwd_result = fwd.iter().find(|(id, _)| *id == inspect_id).map(|(_, r)| r);
1517 assert!(
1518 matches!(fwd_result, Some(HookResult::Success)),
1519 "got {:?}",
1520 fwd_result
1521 );
1522
1523 let gradients = [0.1f64, 0.2, 0.3];
1527 let bwd = mgr.execute_hooks("linear", &gradients, &[3], false, None);
1528 let bwd_result = bwd.iter().find(|(id, _)| *id == grad_id).map(|(_, r)| r);
1529 assert!(
1530 matches!(bwd_result, Some(HookResult::Success)),
1531 "got {:?}",
1532 bwd_result
1533 );
1534
1535 let tracked = mgr.tensor_inspector().get_all_tensors();
1536 assert_eq!(
1537 tracked.len(),
1538 1,
1539 "gradient stats must attach to the existing forward tensor, not spawn a second one"
1540 );
1541 let grad_stats = tracked[0]
1542 .gradient_stats
1543 .as_ref()
1544 .expect("TrackGradients must populate gradient_stats with real data");
1545 assert!(
1546 (grad_stats.mean - 0.2).abs() < 1e-9,
1547 "gradient mean must reflect the real gradient values, got {}",
1548 grad_stats.mean
1549 );
1550 assert!((tracked[0].stats.mean - 2.0).abs() < 1e-9);
1552 }
1553
1554 #[test]
1555 fn test_track_gradients_without_prior_forward_still_tracks_real_data() {
1556 let mut mgr = HookManager::new();
1557 mgr.register_hook(make_action_hook(
1558 HookAction::TrackGradients,
1559 HookTrigger::EveryBackward,
1560 ))
1561 .expect("register");
1562
1563 let gradients = [10.0f64, 20.0, 30.0];
1566 let results = mgr.execute_hooks("orphan", &gradients, &[3], false, None);
1567 assert!(matches!(results[0].1, HookResult::Success));
1568
1569 let tracked = mgr.tensor_inspector().get_all_tensors();
1570 assert_eq!(tracked.len(), 1);
1571 assert!((tracked[0].stats.mean - 20.0).abs() < 1e-9);
1572 }
1573
1574 #[test]
1575 fn test_record_activations_computes_real_statistics() {
1576 let mut mgr = HookManager::new();
1577 mgr.register_hook(make_action_hook(
1578 HookAction::RecordActivations,
1579 HookTrigger::EveryForward,
1580 ))
1581 .expect("register");
1582
1583 let data = [0.0f64, 1.0, 2.0, 3.0];
1584 let results = mgr.execute_hooks("relu_1", &data, &[4], true, None);
1585 assert!(matches!(results[0].1, HookResult::Success));
1586
1587 let recorded = mgr
1588 .activation_visualizer()
1589 .get_activations("relu_1")
1590 .expect("RecordActivations must register real activation data");
1591 assert_eq!(recorded.values, vec![0.0f32, 1.0, 2.0, 3.0]);
1592 assert!((recorded.statistics.mean - 1.5).abs() < 1e-6);
1593 assert_eq!(recorded.shape, vec![4]);
1594 }
1595
1596 #[test]
1597 fn test_pause_training_sets_shared_flag_and_resume_clears_it() {
1598 let mut mgr = HookManager::new();
1599 let flag = mgr.pause_flag();
1600 assert!(!mgr.is_paused());
1601 assert!(!flag.load(Ordering::SeqCst));
1602
1603 mgr.register_hook(make_action_hook(
1604 HookAction::PauseTraining,
1605 HookTrigger::EveryForward,
1606 ))
1607 .expect("register");
1608
1609 let results = mgr.execute_hooks("any_layer", &[0.0f64], &[1], true, None);
1610 assert!(matches!(results[0].1, HookResult::Success));
1611
1612 assert!(
1615 flag.load(Ordering::SeqCst),
1616 "PauseTraining must set the real shared pause flag"
1617 );
1618 assert!(mgr.is_paused());
1619
1620 mgr.resume_training();
1621 assert!(!mgr.is_paused());
1622 assert!(
1623 !flag.load(Ordering::SeqCst),
1624 "resume_training must clear the SAME shared flag"
1625 );
1626 }
1627
1628 #[test]
1629 fn test_inspect_tensor_shape_mismatch_is_a_structured_error_not_silent_success() {
1630 let mut mgr = HookManager::new();
1631 mgr.register_hook(make_action_hook(
1632 HookAction::InspectTensor,
1633 HookTrigger::EveryForward,
1634 ))
1635 .expect("register");
1636
1637 let data = [1.0f64, 2.0, 3.0];
1640 let results = mgr.execute_hooks("mismatched", &data, &[10], true, None);
1641 assert_eq!(results.len(), 1);
1642 match &results[0].1 {
1643 HookResult::Error(msg) => {
1644 assert!(
1645 msg.contains("mismatched"),
1646 "error should name the layer: {}",
1647 msg
1648 );
1649 },
1650 other => panic!("expected a structured Error, got {:?}", other),
1651 }
1652 assert!(
1653 mgr.tensor_inspector().get_all_tensors().is_empty(),
1654 "a shape mismatch must not fabricate a tracked tensor"
1655 );
1656 }
1657}