Skip to main content

trustformers_debug/
large_model_viz.rs

1//! Large Model Visualization with Memory Efficiency
2//!
3//! This module provides optimized visualization for large transformer models,
4//! using smart sampling, hierarchical rendering, and memory-efficient techniques
5//! to handle models with billions of parameters.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![allow(dead_code)]
10
11use anyhow::{Context, Result};
12use parking_lot::RwLock;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::Arc;
16use tracing::{debug, info, warn};
17
18/// Large model visualizer with memory-efficient rendering
19///
20/// Features:
21/// - Smart layer sampling (visualize representative layers)
22/// - Hierarchical graph rendering (collapse/expand sections)
23/// - Streaming visualization (process in chunks)
24/// - Memory-bounded caching
25/// - Progressive loading
26#[derive(Debug)]
27pub struct LargeModelVisualizer {
28    /// Configuration
29    config: LargeModelVisualizerConfig,
30    /// Cached layer metadata
31    layer_cache: Arc<RwLock<HashMap<String, LayerMetadata>>>,
32    /// Visualization state
33    state: Arc<RwLock<VisualizationState>>,
34}
35
36/// Configuration for large model visualization
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LargeModelVisualizerConfig {
39    /// Enable smart layer sampling
40    pub enable_smart_sampling: bool,
41    /// Maximum layers to visualize fully (rest are sampled)
42    pub max_full_layers: usize,
43    /// Sampling strategy
44    pub sampling_strategy: SamplingStrategy,
45    /// Enable hierarchical rendering
46    pub enable_hierarchical: bool,
47    /// Enable streaming mode for very large models
48    pub enable_streaming: bool,
49    /// Maximum memory for visualization (MB)
50    pub max_memory_mb: usize,
51    /// Chunk size for streaming (number of layers)
52    pub stream_chunk_size: usize,
53    /// Enable progressive detail loading
54    pub enable_progressive_loading: bool,
55    /// Visualization format
56    pub output_format: VisualizationFormat,
57}
58
59impl Default for LargeModelVisualizerConfig {
60    fn default() -> Self {
61        Self {
62            enable_smart_sampling: true,
63            max_full_layers: 50,
64            sampling_strategy: SamplingStrategy::Adaptive,
65            enable_hierarchical: true,
66            enable_streaming: true,
67            max_memory_mb: 1024, // 1 GB
68            stream_chunk_size: 10,
69            enable_progressive_loading: true,
70            output_format: VisualizationFormat::InteractiveSvg,
71        }
72    }
73}
74
75/// Layer sampling strategy for large models
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub enum SamplingStrategy {
78    /// Uniform sampling (evenly spaced layers)
79    Uniform,
80    /// Adaptive sampling (more samples where complexity varies)
81    Adaptive,
82    /// Representative sampling (first, middle, last + interesting layers)
83    Representative,
84    /// Importance-based (based on parameter count, compute cost)
85    ImportanceBased,
86}
87
88/// Visualization output format
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub enum VisualizationFormat {
91    /// Static PNG image (memory efficient)
92    StaticPng,
93    /// Static SVG (scalable but larger)
94    StaticSvg,
95    /// Interactive SVG with zoom/pan
96    InteractiveSvg,
97    /// Interactive HTML with JavaScript
98    InteractiveHtml,
99    /// Text-based summary (minimal memory)
100    TextSummary,
101    /// JSON metadata only
102    JsonMetadata,
103}
104
105/// Metadata about a model layer
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct LayerMetadata {
108    /// Layer name
109    pub name: String,
110    /// Layer index
111    pub index: usize,
112    /// Layer type
113    pub layer_type: String,
114    /// Number of parameters
115    pub param_count: usize,
116    /// Estimated memory (MB)
117    pub memory_mb: f64,
118    /// Estimated compute cost (FLOPS)
119    pub compute_flops: u64,
120    /// Input shape
121    pub input_shape: Vec<usize>,
122    /// Output shape
123    pub output_shape: Vec<usize>,
124    /// Is this layer sampled for visualization?
125    pub is_sampled: bool,
126}
127
128/// Current visualization state
129#[derive(Debug, Clone, Default)]
130struct VisualizationState {
131    /// Total layers in model
132    total_layers: usize,
133    /// Layers currently loaded
134    loaded_layers: Vec<String>,
135    /// Current memory usage (MB)
136    current_memory_mb: f64,
137    /// Visualization progress (0.0-1.0)
138    progress: f64,
139    /// Is visualization complete?
140    is_complete: bool,
141}
142
143/// Visualization result
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct VisualizationResult {
146    /// Output file path (if saved to file)
147    pub output_path: Option<String>,
148    /// Inline data (if small enough)
149    pub inline_data: Option<Vec<u8>>,
150    /// Visualization statistics
151    pub stats: VisualizationStats,
152    /// Sampled layer indices
153    pub sampled_layers: Vec<usize>,
154    /// Total model statistics
155    pub model_stats: ModelStatistics,
156}
157
158/// Statistics about the visualization
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct VisualizationStats {
161    /// Number of layers visualized
162    pub layers_visualized: usize,
163    /// Number of layers in model
164    pub total_layers: usize,
165    /// Sampling ratio
166    pub sampling_ratio: f64,
167    /// Memory used for visualization (MB)
168    pub memory_used_mb: f64,
169    /// Time taken (seconds)
170    pub time_taken_secs: f64,
171    /// Output size (bytes)
172    pub output_size_bytes: usize,
173}
174
175/// Overall model statistics
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ModelStatistics {
178    /// Total parameters
179    pub total_params: usize,
180    /// Total memory footprint (MB)
181    pub total_memory_mb: f64,
182    /// Total compute cost (GFLOPS)
183    pub total_gflops: f64,
184    /// Deepest layer index
185    pub max_depth: usize,
186    /// Layer type distribution
187    pub layer_types: HashMap<String, usize>,
188}
189
190/// Layer group for hierarchical visualization
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct LayerGroup {
193    /// Group name
194    pub name: String,
195    /// Layer indices in this group
196    pub layers: Vec<usize>,
197    /// Is this group collapsed?
198    pub collapsed: bool,
199    /// Summary statistics for group
200    pub summary: GroupSummary,
201}
202
203/// Summary for a layer group
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct GroupSummary {
206    /// Total parameters in group
207    pub param_count: usize,
208    /// Total memory (MB)
209    pub memory_mb: f64,
210    /// Average compute cost per layer
211    pub avg_compute_flops: u64,
212}
213
214impl LargeModelVisualizer {
215    /// Create a new large model visualizer
216    ///
217    /// # Arguments
218    /// * `config` - Visualizer configuration
219    ///
220    /// # Example
221    /// ```rust
222    /// use trustformers_debug::{LargeModelVisualizer, LargeModelVisualizerConfig};
223    ///
224    /// let config = LargeModelVisualizerConfig::default();
225    /// let visualizer = LargeModelVisualizer::new(config);
226    /// ```
227    pub fn new(config: LargeModelVisualizerConfig) -> Self {
228        info!("Initializing large model visualizer");
229        Self {
230            config,
231            layer_cache: Arc::new(RwLock::new(HashMap::new())),
232            state: Arc::new(RwLock::new(VisualizationState::default())),
233        }
234    }
235
236    /// Add layer metadata to the visualizer
237    ///
238    /// # Arguments
239    /// * `metadata` - Layer metadata
240    pub fn add_layer(&self, metadata: LayerMetadata) -> Result<()> {
241        let mut cache = self.layer_cache.write();
242        let mut state = self.state.write();
243
244        cache.insert(metadata.name.clone(), metadata.clone());
245        state.total_layers = cache.len();
246        state.current_memory_mb += metadata.memory_mb;
247
248        // Check memory limit
249        if state.current_memory_mb > self.config.max_memory_mb as f64 {
250            warn!(
251                "Memory limit exceeded: {:.1} MB > {} MB. Consider increasing max_memory_mb or enabling sampling",
252                state.current_memory_mb,
253                self.config.max_memory_mb
254            );
255        }
256
257        Ok(())
258    }
259
260    /// Analyze model and determine sampling strategy
261    ///
262    /// # Returns
263    /// Indices of layers to visualize in detail
264    pub fn determine_sampling(&self) -> Result<Vec<usize>> {
265        let cache = self.layer_cache.read();
266        let state = self.state.read();
267
268        if !self.config.enable_smart_sampling || state.total_layers <= self.config.max_full_layers {
269            // Visualize all layers
270            return Ok((0..state.total_layers).collect());
271        }
272
273        debug!(
274            "Applying {:?} sampling strategy for {} layers",
275            self.config.sampling_strategy, state.total_layers
276        );
277
278        let sampled_indices = match self.config.sampling_strategy {
279            SamplingStrategy::Uniform => self.uniform_sampling(state.total_layers),
280            SamplingStrategy::Adaptive => self.adaptive_sampling(&cache),
281            SamplingStrategy::Representative => self.representative_sampling(state.total_layers),
282            SamplingStrategy::ImportanceBased => self.importance_sampling(&cache),
283        };
284
285        Ok(sampled_indices)
286    }
287
288    /// Uniform sampling: evenly spaced layers
289    fn uniform_sampling(&self, total_layers: usize) -> Vec<usize> {
290        let max_layers = self.config.max_full_layers;
291        let step = (total_layers as f64 / max_layers as f64).ceil() as usize;
292
293        (0..total_layers).step_by(step).collect()
294    }
295
296    /// Adaptive sampling: more samples where complexity varies
297    fn adaptive_sampling(&self, cache: &HashMap<String, LayerMetadata>) -> Vec<usize> {
298        let mut layers: Vec<_> = cache.values().collect();
299        layers.sort_by_key(|l| l.index);
300
301        let mut sampled = Vec::new();
302        let max_layers = self.config.max_full_layers;
303
304        // Always include first and last layers
305        if !layers.is_empty() {
306            sampled.push(0);
307            sampled.push(layers.len() - 1);
308        }
309
310        // Calculate complexity variance between consecutive layers
311        let mut variances = Vec::new();
312        for i in 0..layers.len().saturating_sub(1) {
313            let complexity_diff =
314                (layers[i + 1].param_count as i64 - layers[i].param_count as i64).abs();
315            variances.push((i, complexity_diff));
316        }
317
318        // Sort by variance (descending)
319        variances.sort_by_key(|item| std::cmp::Reverse(item.1));
320
321        // Sample layers with highest variance
322        for (idx, _) in variances.iter().take(max_layers.saturating_sub(2)) {
323            sampled.push(*idx);
324        }
325
326        sampled.sort_unstable();
327        sampled.dedup();
328        sampled
329    }
330
331    /// Representative sampling: first, middle, last + interesting layers
332    fn representative_sampling(&self, total_layers: usize) -> Vec<usize> {
333        let mut sampled = Vec::new();
334
335        if total_layers == 0 {
336            return sampled;
337        }
338
339        // First layers
340        sampled.extend(0..3.min(total_layers));
341
342        // Middle layers
343        let mid = total_layers / 2;
344        sampled.extend((mid.saturating_sub(1))..=(mid + 1).min(total_layers - 1));
345
346        // Last layers
347        sampled.extend((total_layers.saturating_sub(3))..total_layers);
348
349        // Add evenly spaced samples in between
350        let remaining_budget = self.config.max_full_layers.saturating_sub(sampled.len());
351        let step = (total_layers as f64 / remaining_budget as f64).ceil() as usize;
352
353        for i in (0..total_layers).step_by(step) {
354            sampled.push(i);
355        }
356
357        sampled.sort_unstable();
358        sampled.dedup();
359        sampled
360    }
361
362    /// Importance-based sampling: prioritize large/complex layers
363    fn importance_sampling(&self, cache: &HashMap<String, LayerMetadata>) -> Vec<usize> {
364        let mut layers: Vec<_> = cache.values().collect();
365
366        // Calculate importance score (weighted sum of params and compute)
367        layers.sort_by(|a, b| {
368            let score_a = (a.param_count as f64) + (a.compute_flops as f64 / 1e9);
369            let score_b = (b.param_count as f64) + (b.compute_flops as f64 / 1e9);
370            score_b.partial_cmp(&score_a).unwrap_or(std::cmp::Ordering::Equal)
371        });
372
373        layers.iter().take(self.config.max_full_layers).map(|l| l.index).collect()
374    }
375
376    /// Create hierarchical layer groups
377    ///
378    /// Groups layers by type or sequential blocks for collapsible visualization
379    pub fn create_layer_groups(&self) -> Result<Vec<LayerGroup>> {
380        let cache = self.layer_cache.read();
381
382        if !self.config.enable_hierarchical || cache.len() < 20 {
383            // Not worth grouping small models
384            return Ok(Vec::new());
385        }
386
387        let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
388
389        // Group by layer type
390        for metadata in cache.values() {
391            groups.entry(metadata.layer_type.clone()).or_default().push(metadata.index);
392        }
393
394        // Create LayerGroup objects
395        let mut layer_groups = Vec::new();
396
397        for (layer_type, indices) in groups {
398            // Calculate summary
399            let group_layers: Vec<_> = indices
400                .iter()
401                .filter_map(|&idx| cache.values().find(|l| l.index == idx))
402                .collect();
403
404            let param_count: usize = group_layers.iter().map(|l| l.param_count).sum();
405            let memory_mb: f64 = group_layers.iter().map(|l| l.memory_mb).sum();
406            let avg_compute_flops = if !group_layers.is_empty() {
407                group_layers.iter().map(|l| l.compute_flops).sum::<u64>()
408                    / group_layers.len() as u64
409            } else {
410                0
411            };
412
413            let indices_len = indices.len();
414            layer_groups.push(LayerGroup {
415                name: format!("{} ({} layers)", layer_type, indices_len),
416                layers: indices,
417                collapsed: indices_len > 10, // Auto-collapse large groups
418                summary: GroupSummary {
419                    param_count,
420                    memory_mb,
421                    avg_compute_flops,
422                },
423            });
424        }
425
426        // Sort by first layer index
427        layer_groups.sort_by_key(|g| g.layers.first().copied().unwrap_or(0));
428
429        Ok(layer_groups)
430    }
431
432    /// Generate visualization with memory-efficient rendering
433    ///
434    /// # Arguments
435    /// * `output_path` - Optional output file path
436    ///
437    /// # Returns
438    /// Visualization result with statistics
439    pub fn visualize(&self, output_path: Option<String>) -> Result<VisualizationResult> {
440        info!("Starting large model visualization");
441
442        let start_time = std::time::Instant::now();
443
444        // Determine which layers to visualize
445        let sampled_layers = self.determine_sampling()?;
446
447        info!(
448            "Visualizing {} out of {} layers",
449            sampled_layers.len(),
450            self.state.read().total_layers
451        );
452
453        // Calculate model statistics
454        let model_stats = self.calculate_model_stats()?;
455
456        // Generate visualization based on format
457        let (output_data, output_size) = match self.config.output_format {
458            VisualizationFormat::TextSummary => self.generate_text_summary(&sampled_layers)?,
459            VisualizationFormat::JsonMetadata => self.generate_json_metadata(&sampled_layers)?,
460            VisualizationFormat::StaticSvg => self.generate_static_svg(&sampled_layers)?,
461            VisualizationFormat::InteractiveSvg => {
462                self.generate_interactive_svg(&sampled_layers)?
463            },
464            VisualizationFormat::InteractiveHtml => {
465                self.generate_interactive_html(&sampled_layers)?
466            },
467            VisualizationFormat::StaticPng => {
468                // PNG output needs the `image` crate, which is optional. Enable it
469                // with `--features image` (or `--features gif`, which turns it on
470                // too). Without that feature, fall back to a descriptive error so
471                // callers can switch to SVG/HTML output, which works without any
472                // extra features.
473                #[cfg(feature = "image")]
474                {
475                    self.generate_png(&sampled_layers)?
476                }
477                #[cfg(not(feature = "image"))]
478                {
479                    return Err(anyhow::anyhow!(
480                        "PNG generation requires the `image` feature. \
481                         Rebuild with `--features image` (or `--features gif`), or use \
482                         VisualizationFormat::StaticSvg / InteractiveHtml instead."
483                    ));
484                }
485            },
486        };
487
488        // Save to file if path provided
489        if let Some(ref path) = output_path {
490            std::fs::write(path, &output_data)
491                .with_context(|| format!("Failed to write visualization to {}", path))?;
492            info!("Saved visualization to {}", path);
493        }
494
495        let time_taken = start_time.elapsed().as_secs_f64();
496        let state = self.state.read();
497
498        Ok(VisualizationResult {
499            output_path,
500            inline_data: if output_size < 1024 * 1024 { Some(output_data) } else { None }, // Include inline if < 1MB
501            stats: VisualizationStats {
502                layers_visualized: sampled_layers.len(),
503                total_layers: state.total_layers,
504                sampling_ratio: sampled_layers.len() as f64 / state.total_layers as f64,
505                memory_used_mb: state.current_memory_mb,
506                time_taken_secs: time_taken,
507                output_size_bytes: output_size,
508            },
509            sampled_layers,
510            model_stats,
511        })
512    }
513
514    /// Calculate overall model statistics
515    fn calculate_model_stats(&self) -> Result<ModelStatistics> {
516        let cache = self.layer_cache.read();
517
518        let total_params: usize = cache.values().map(|l| l.param_count).sum();
519        let total_memory_mb: f64 = cache.values().map(|l| l.memory_mb).sum();
520        let total_gflops: f64 = cache.values().map(|l| l.compute_flops).sum::<u64>() as f64 / 1e9;
521        let max_depth = cache.values().map(|l| l.index).max().unwrap_or(0);
522
523        let mut layer_types: HashMap<String, usize> = HashMap::new();
524        for metadata in cache.values() {
525            *layer_types.entry(metadata.layer_type.clone()).or_insert(0) += 1;
526        }
527
528        Ok(ModelStatistics {
529            total_params,
530            total_memory_mb,
531            total_gflops,
532            max_depth,
533            layer_types,
534        })
535    }
536
537    /// Generate text summary (minimal memory)
538    fn generate_text_summary(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
539        let cache = self.layer_cache.read();
540
541        let mut summary = String::from("=== Large Model Visualization Summary ===\n\n");
542
543        summary.push_str(&format!(
544            "Total Layers: {}\n",
545            self.state.read().total_layers
546        ));
547        summary.push_str(&format!("Visualized Layers: {}\n\n", sampled_layers.len()));
548
549        summary.push_str("Layer Details:\n");
550        for &idx in sampled_layers {
551            if let Some(layer) = cache.values().find(|l| l.index == idx) {
552                summary.push_str(&format!(
553                    "  [{}] {} - {} params, {:.2} MB, {:.1} GFLOPS\n",
554                    layer.index,
555                    layer.name,
556                    layer.param_count,
557                    layer.memory_mb,
558                    layer.compute_flops as f64 / 1e9
559                ));
560            }
561        }
562
563        let bytes = summary.into_bytes();
564        let size = bytes.len();
565        Ok((bytes, size))
566    }
567
568    /// Generate JSON metadata
569    fn generate_json_metadata(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
570        let cache = self.layer_cache.read();
571
572        let layers: Vec<_> = sampled_layers
573            .iter()
574            .filter_map(|&idx| cache.values().find(|l| l.index == idx).cloned())
575            .collect();
576
577        let json = serde_json::to_string_pretty(&layers)?;
578        let bytes = json.into_bytes();
579        let size = bytes.len();
580        Ok((bytes, size))
581    }
582
583    /// Generate static SVG
584    fn generate_static_svg(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
585        let cache = self.layer_cache.read();
586
587        let mut svg = String::from(
588            r#"<?xml version="1.0" encoding="UTF-8"?>
589<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
590<style>
591.layer { fill: #4a90e2; stroke: #2c5aa0; stroke-width: 2; }
592.layer-text { fill: white; font-family: Arial, sans-serif; font-size: 12px; }
593.title { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; }
594</style>
595<text x="600" y="30" class="title" text-anchor="middle">Model Architecture</text>
596"#,
597        );
598
599        let layer_height = 60;
600        let layer_width = 200;
601        let x_offset = 500;
602        let y_start = 60;
603
604        for (i, &idx) in sampled_layers.iter().enumerate() {
605            if let Some(layer) = cache.values().find(|l| l.index == idx) {
606                let y = y_start + i * (layer_height + 20);
607
608                svg.push_str(&format!(
609                    r#"<rect x="{}" y="{}" width="{}" height="{}" class="layer" />
610<text x="{}" y="{}" class="layer-text" text-anchor="middle">{}</text>
611<text x="{}" y="{}" class="layer-text" text-anchor="middle">{:.1}M params</text>
612"#,
613                    x_offset,
614                    y,
615                    layer_width,
616                    layer_height,
617                    x_offset + layer_width / 2,
618                    y + 25,
619                    layer.name,
620                    x_offset + layer_width / 2,
621                    y + 45,
622                    layer.param_count as f64 / 1e6
623                ));
624            }
625        }
626
627        svg.push_str("</svg>");
628
629        let bytes = svg.into_bytes();
630        let size = bytes.len();
631        Ok((bytes, size))
632    }
633
634    /// Generate interactive SVG with zoom/pan via embedded ECMAScript
635    fn generate_interactive_svg(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
636        let cache = self.layer_cache.read();
637
638        let layer_height = 60usize;
639        let layer_width = 200usize;
640        let x_offset = 500usize;
641        let y_start = 60usize;
642        let svg_height = y_start + sampled_layers.len() * (layer_height + 20) + 40;
643        let svg_width = 1200usize;
644
645        // Build the inner layer elements first
646        let mut layer_elems = String::new();
647        for (i, &idx) in sampled_layers.iter().enumerate() {
648            if let Some(layer) = cache.values().find(|l| l.index == idx) {
649                let y = y_start + i * (layer_height + 20);
650                layer_elems.push_str(&format!(
651                    r#"<rect x="{x}" y="{y}" width="{w}" height="{h}" class="layer" />
652<text x="{cx}" y="{ty}" class="layer-text" text-anchor="middle">{name}</text>
653<text x="{cx}" y="{py}" class="layer-text" text-anchor="middle">{params:.1}M params</text>
654"#,
655                    x = x_offset,
656                    y = y,
657                    w = layer_width,
658                    h = layer_height,
659                    cx = x_offset + layer_width / 2,
660                    ty = y + 25,
661                    py = y + 45,
662                    name = layer.name,
663                    params = layer.param_count as f64 / 1e6
664                ));
665            }
666        }
667
668        // Compose full SVG with embedded pan/zoom JavaScript.
669        // The <script> block uses an SVG foreignObject-free approach: it attaches
670        // pointer-event listeners directly to the root <svg> element and manipulates
671        // a <g id="viewport"> transform, which is valid SVG+JS in any modern browser.
672        let svg = format!(
673            r#"<?xml version="1.0" encoding="UTF-8"?>
674<svg xmlns="http://www.w3.org/2000/svg"
675     xmlns:xlink="http://www.w3.org/1999/xlink"
676     id="svg-root"
677     width="{width}" height="{height}"
678     viewBox="0 0 {width} {height}"
679     style="cursor:grab;user-select:none;">
680<style>
681.layer {{ fill: #4a90e2; stroke: #2c5aa0; stroke-width: 2; }}
682.layer-text {{ fill: white; font-family: Arial, sans-serif; font-size: 12px; }}
683.title {{ font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; }}
684</style>
685<text x="{title_x}" y="30" class="title" text-anchor="middle">Model Architecture (interactive)</text>
686<g id="viewport">
687{layers}
688</g>
689<script type="text/javascript"><![CDATA[
690(function() {{
691  var svg   = document.getElementById('svg-root');
692  var vp    = document.getElementById('viewport');
693  var tx = 0, ty = 0, scale = 1.0;
694  var dragging = false;
695  var startX = 0, startY = 0;
696
697  function applyTransform() {{
698    vp.setAttribute('transform',
699      'translate(' + tx + ',' + ty + ') scale(' + scale + ')');
700  }}
701
702  // Pan: mousedown / mousemove / mouseup
703  svg.addEventListener('mousedown', function(e) {{
704    dragging = true;
705    startX = e.clientX - tx;
706    startY = e.clientY - ty;
707    svg.style.cursor = 'grabbing';
708    e.preventDefault();
709  }});
710  window.addEventListener('mousemove', function(e) {{
711    if (!dragging) return;
712    tx = e.clientX - startX;
713    ty = e.clientY - startY;
714    applyTransform();
715  }});
716  window.addEventListener('mouseup', function() {{
717    dragging = false;
718    svg.style.cursor = 'grab';
719  }});
720
721  // Touch pan
722  var lastTouch = null;
723  svg.addEventListener('touchstart', function(e) {{
724    if (e.touches.length === 1) {{
725      lastTouch = e.touches[0];
726    }}
727    e.preventDefault();
728  }}, {{ passive: false }});
729  svg.addEventListener('touchmove', function(e) {{
730    if (e.touches.length === 1 && lastTouch) {{
731      var t = e.touches[0];
732      tx += t.clientX - lastTouch.clientX;
733      ty += t.clientY - lastTouch.clientY;
734      lastTouch = t;
735      applyTransform();
736    }}
737    e.preventDefault();
738  }}, {{ passive: false }});
739  svg.addEventListener('touchend', function() {{ lastTouch = null; }});
740
741  // Zoom: mousewheel
742  svg.addEventListener('wheel', function(e) {{
743    e.preventDefault();
744    var delta = e.deltaY > 0 ? 0.9 : 1.1;
745    // Zoom towards cursor position
746    var rect  = svg.getBoundingClientRect();
747    var mx = e.clientX - rect.left;
748    var my = e.clientY - rect.top;
749    tx = mx - (mx - tx) * delta;
750    ty = my - (my - ty) * delta;
751    scale = Math.max(0.1, Math.min(10.0, scale * delta));
752    applyTransform();
753  }}, {{ passive: false }});
754
755  // Double-click to reset
756  svg.addEventListener('dblclick', function() {{
757    tx = 0; ty = 0; scale = 1.0;
758    applyTransform();
759  }});
760}})();
761]]></script>
762</svg>"#,
763            width = svg_width,
764            height = svg_height,
765            title_x = svg_width / 2,
766            layers = layer_elems,
767        );
768
769        let bytes = svg.into_bytes();
770        let size = bytes.len();
771        Ok((bytes, size))
772    }
773
774    /// Generate interactive HTML with JavaScript
775    fn generate_interactive_html(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
776        // `calculate_model_stats` takes its own `self.layer_cache.read()`
777        // internally and drops it before returning; computed here, before
778        // this function's own `cache` guard below is acquired, so the two
779        // reads never overlap. Acquiring `cache` first (the original order)
780        // held it across this call, which -- parking_lot's `RwLock` gives no
781        // reentrancy guarantee either -- could deadlock a same-thread
782        // recursive read against a writer queued in between.
783        let model_stats = self.calculate_model_stats()?;
784        let cache = self.layer_cache.read();
785
786        let mut html = String::from(
787            r#"<!DOCTYPE html>
788<html>
789<head>
790<meta charset="UTF-8">
791<title>Large Model Visualization</title>
792<style>
793body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
794.container { max-width: 1200px; margin: 0 auto; }
795.header { background: #4a90e2; color: white; padding: 20px; border-radius: 8px; }
796.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
797.stat-card { background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
798.layer-list { background: white; padding: 20px; border-radius: 8px; }
799.layer { padding: 10px; margin: 5px 0; background: #f9f9f9; border-left: 4px solid #4a90e2; }
800</style>
801</head>
802<body>
803<div class="container">
804<div class="header">
805<h1>Large Model Visualization</h1>
806<p>Interactive view of model architecture</p>
807</div>
808<div class="stats">
809"#,
810        );
811
812        // Add stats cards
813        html.push_str(&format!(
814            r#"<div class="stat-card">
815<h3>{:.1}M</h3>
816<p>Total Parameters</p>
817</div>
818<div class="stat-card">
819<h3>{:.1} GB</h3>
820<p>Total Memory</p>
821</div>
822<div class="stat-card">
823<h3>{}</h3>
824<p>Total Layers</p>
825</div>
826<div class="stat-card">
827<h3>{}/{}</h3>
828<p>Visualized/Total</p>
829</div>
830"#,
831            model_stats.total_params as f64 / 1e6,
832            model_stats.total_memory_mb / 1024.0,
833            model_stats.max_depth + 1,
834            sampled_layers.len(),
835            self.state.read().total_layers
836        ));
837
838        html.push_str("</div><div class=\"layer-list\"><h2>Layer Details</h2>");
839
840        // Add layer details
841        for &idx in sampled_layers {
842            if let Some(layer) = cache.values().find(|l| l.index == idx) {
843                html.push_str(&format!(
844                    r#"<div class="layer">
845<strong>[{}] {}</strong><br>
846Type: {} | Parameters: {:.1}M | Memory: {:.2} MB | Compute: {:.1} GFLOPS
847</div>
848"#,
849                    layer.index,
850                    layer.name,
851                    layer.layer_type,
852                    layer.param_count as f64 / 1e6,
853                    layer.memory_mb,
854                    layer.compute_flops as f64 / 1e9
855                ));
856            }
857        }
858
859        html.push_str("</div></div></body></html>");
860
861        let bytes = html.into_bytes();
862        let size = bytes.len();
863        Ok((bytes, size))
864    }
865
866    /// Generate a static PNG heatmap visualization of the sampled layers.
867    ///
868    /// Each layer is rendered as a horizontal bar whose width is proportional to
869    /// `param_count` and whose colour encodes `memory_mb` (blue → red gradient).
870    /// The resulting image is PNG-encoded and returned as a raw byte vector.
871    ///
872    /// Requires the optional `image` dependency (`--features image`, or
873    /// `--features gif`, which enables it as well).
874    #[cfg(feature = "image")]
875    fn generate_png(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
876        use image::{ImageBuffer, Rgb};
877        use std::io::Cursor;
878
879        let cache = self.layer_cache.read();
880
881        // Gather layers in index order.
882        let mut layers: Vec<&LayerMetadata> = sampled_layers
883            .iter()
884            .filter_map(|&idx| cache.values().find(|l| l.index == idx))
885            .collect();
886        layers.sort_by_key(|l| l.index);
887
888        // Image layout constants.
889        const IMG_WIDTH: u32 = 1200;
890        const BAR_HEIGHT: u32 = 30;
891        const BAR_PADDING: u32 = 6;
892        const LEFT_MARGIN: u32 = 20;
893        const RIGHT_MARGIN: u32 = 20;
894
895        let row_height = BAR_HEIGHT + BAR_PADDING;
896        let img_height = if layers.is_empty() {
897            100
898        } else {
899            layers.len() as u32 * row_height + 2 * BAR_PADDING + 40 // +40 for title row
900        };
901
902        let max_params = layers.iter().map(|l| l.param_count).max().unwrap_or(1).max(1);
903
904        let max_memory = layers.iter().map(|l| l.memory_mb).fold(0.0_f64, f64::max).max(1.0);
905
906        let available_width = IMG_WIDTH - LEFT_MARGIN - RIGHT_MARGIN;
907
908        let mut img = ImageBuffer::<Rgb<u8>, Vec<u8>>::new(IMG_WIDTH, img_height);
909
910        // Background: near-white.
911        for pixel in img.pixels_mut() {
912            *pixel = Rgb([245u8, 245u8, 250u8]);
913        }
914
915        // Title bar.
916        for x in 0..IMG_WIDTH {
917            for y in 0..36 {
918                img.put_pixel(x, y, Rgb([74u8, 144u8, 226u8]));
919            }
920        }
921
922        // Draw each layer as a horizontal heatmap bar.
923        for (i, layer) in layers.iter().enumerate() {
924            let bar_top = 40 + i as u32 * row_height;
925
926            // Bar width proportional to param_count.
927            let bar_w = ((layer.param_count as f64 / max_params as f64) * available_width as f64)
928                .round() as u32;
929            let bar_w = bar_w.max(4); // always visible
930
931            // Colour: blue (low memory) → red (high memory) gradient.
932            let t = (layer.memory_mb / max_memory).clamp(0.0, 1.0) as f32;
933            let r = (t * 220.0) as u8;
934            let g = ((1.0 - t) * 100.0 + 40.0) as u8;
935            let b = ((1.0 - t) * 220.0) as u8;
936            let bar_colour = Rgb([r, g, b]);
937
938            for x in LEFT_MARGIN..(LEFT_MARGIN + bar_w).min(IMG_WIDTH - RIGHT_MARGIN) {
939                for y in bar_top..(bar_top + BAR_HEIGHT).min(img_height) {
940                    img.put_pixel(x, y, bar_colour);
941                }
942            }
943        }
944
945        // Encode as PNG into an in-memory buffer.
946        let mut png_bytes: Vec<u8> = Vec::new();
947        img.write_to(&mut Cursor::new(&mut png_bytes), image::ImageFormat::Png)
948            .with_context(|| "Failed to PNG-encode large model visualization")?;
949
950        let size = png_bytes.len();
951        Ok((png_bytes, size))
952    }
953
954    /// Get current visualization progress (0.0-1.0)
955    pub fn get_progress(&self) -> f64 {
956        self.state.read().progress
957    }
958
959    /// Get memory usage statistics
960    pub fn get_memory_stats(&self) -> MemoryStats {
961        let state = self.state.read();
962        MemoryStats {
963            current_mb: state.current_memory_mb,
964            max_mb: self.config.max_memory_mb as f64,
965            utilization_pct: (state.current_memory_mb / self.config.max_memory_mb as f64 * 100.0)
966                .min(100.0),
967        }
968    }
969}
970
971/// Memory usage statistics
972#[derive(Debug, Clone, Serialize, Deserialize)]
973pub struct MemoryStats {
974    /// Current memory usage (MB)
975    pub current_mb: f64,
976    /// Maximum allowed memory (MB)
977    pub max_mb: f64,
978    /// Utilization percentage
979    pub utilization_pct: f64,
980}
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985
986    #[test]
987    fn test_visualizer_creation() {
988        let config = LargeModelVisualizerConfig::default();
989        let _visualizer = LargeModelVisualizer::new(config);
990    }
991
992    #[test]
993    fn test_add_layers() -> Result<()> {
994        let config = LargeModelVisualizerConfig::default();
995        let visualizer = LargeModelVisualizer::new(config);
996
997        for i in 0..10 {
998            let metadata = LayerMetadata {
999                name: format!("layer_{}", i),
1000                index: i,
1001                layer_type: "Linear".to_string(),
1002                param_count: 1024 * 1024,
1003                memory_mb: 4.0,
1004                compute_flops: 1_000_000_000,
1005                input_shape: vec![512],
1006                output_shape: vec![512],
1007                is_sampled: false,
1008            };
1009            visualizer.add_layer(metadata)?;
1010        }
1011
1012        let stats = visualizer.get_memory_stats();
1013        assert_eq!(stats.current_mb, 40.0);
1014
1015        Ok(())
1016    }
1017
1018    #[test]
1019    fn test_uniform_sampling() -> Result<()> {
1020        let config = LargeModelVisualizerConfig {
1021            max_full_layers: 5,
1022            sampling_strategy: SamplingStrategy::Uniform,
1023            ..Default::default()
1024        };
1025
1026        let visualizer = LargeModelVisualizer::new(config);
1027
1028        // Add 20 layers
1029        for i in 0..20 {
1030            let metadata = LayerMetadata {
1031                name: format!("layer_{}", i),
1032                index: i,
1033                layer_type: "Linear".to_string(),
1034                param_count: 1024 * 1024,
1035                memory_mb: 4.0,
1036                compute_flops: 1_000_000_000,
1037                input_shape: vec![512],
1038                output_shape: vec![512],
1039                is_sampled: false,
1040            };
1041            visualizer.add_layer(metadata)?;
1042        }
1043
1044        let sampled = visualizer.determine_sampling()?;
1045        assert_eq!(sampled.len(), 5);
1046
1047        Ok(())
1048    }
1049
1050    #[cfg(feature = "image")]
1051    #[test]
1052    fn test_png_visualization() -> Result<()> {
1053        let config = LargeModelVisualizerConfig {
1054            output_format: VisualizationFormat::StaticPng,
1055            ..Default::default()
1056        };
1057
1058        let visualizer = LargeModelVisualizer::new(config);
1059
1060        for i in 0..5_usize {
1061            let metadata = LayerMetadata {
1062                name: format!("layer_{}", i),
1063                index: i,
1064                layer_type: "Linear".to_string(),
1065                param_count: 1024 * (i + 1),
1066                memory_mb: 2.0 * (i + 1) as f64,
1067                compute_flops: 500_000_000 * (i + 1) as u64,
1068                input_shape: vec![512],
1069                output_shape: vec![512],
1070                is_sampled: false,
1071            };
1072            visualizer.add_layer(metadata)?;
1073        }
1074
1075        let result = visualizer.visualize(None)?;
1076
1077        // Basic sanity checks
1078        assert_eq!(result.stats.layers_visualized, 5);
1079        assert!(
1080            result.stats.output_size_bytes > 0,
1081            "PNG output must be non-empty"
1082        );
1083
1084        // Verify PNG magic bytes: 0x89 P N G
1085        let data = result.inline_data.expect("inline data should be present for small PNG");
1086        assert!(
1087            data.starts_with(&[0x89, 0x50, 0x4E, 0x47]),
1088            "Output must start with PNG magic bytes"
1089        );
1090
1091        Ok(())
1092    }
1093
1094    #[test]
1095    fn test_text_visualization() -> Result<()> {
1096        let config = LargeModelVisualizerConfig {
1097            output_format: VisualizationFormat::TextSummary,
1098            ..Default::default()
1099        };
1100
1101        let visualizer = LargeModelVisualizer::new(config);
1102
1103        // Add a few layers
1104        for i in 0..5 {
1105            let metadata = LayerMetadata {
1106                name: format!("layer_{}", i),
1107                index: i,
1108                layer_type: "Linear".to_string(),
1109                param_count: 1024 * 1024 * (i + 1),
1110                memory_mb: 4.0 * (i + 1) as f64,
1111                compute_flops: 1_000_000_000 * (i + 1) as u64,
1112                input_shape: vec![512],
1113                output_shape: vec![512],
1114                is_sampled: false,
1115            };
1116            visualizer.add_layer(metadata)?;
1117        }
1118
1119        let result = visualizer.visualize(None)?;
1120
1121        assert_eq!(result.stats.layers_visualized, 5);
1122        assert!(result.stats.output_size_bytes > 0);
1123
1124        Ok(())
1125    }
1126
1127    /// Regression: `generate_interactive_html` used to hold its own
1128    /// `self.layer_cache.read()` guard across the call to
1129    /// `calculate_model_stats`, which also reads `self.layer_cache`.
1130    /// `parking_lot::RwLock` documents a task-fair policy that blocks new
1131    /// readers once a writer is queued (to avoid writer starvation), so a
1132    /// same-thread recursive read can block forever once a concurrent
1133    /// `add_layer` (`self.layer_cache.write()`) call is queued in between.
1134    /// This hammers both sides of that race under a bounded timeout: a real
1135    /// deadlock hangs instead of erroring.
1136    #[test]
1137    fn test_interactive_html_does_not_deadlock_against_concurrent_add_layer() -> Result<()> {
1138        use std::sync::mpsc;
1139        use std::time::Duration;
1140
1141        let config = LargeModelVisualizerConfig {
1142            output_format: VisualizationFormat::InteractiveHtml,
1143            ..Default::default()
1144        };
1145        let visualizer = Arc::new(LargeModelVisualizer::new(config));
1146
1147        for i in 0..5 {
1148            visualizer.add_layer(LayerMetadata {
1149                name: format!("layer_{i}"),
1150                index: i,
1151                layer_type: "Linear".to_string(),
1152                param_count: 1024,
1153                memory_mb: 1.0,
1154                compute_flops: 1_000_000,
1155                input_shape: vec![512],
1156                output_shape: vec![512],
1157                is_sampled: false,
1158            })?;
1159        }
1160
1161        let (tx, rx) = mpsc::channel();
1162        let writer_viz = Arc::clone(&visualizer);
1163        let writer = std::thread::spawn(move || {
1164            for i in 5..205 {
1165                let _ = writer_viz.add_layer(LayerMetadata {
1166                    name: format!("layer_{i}"),
1167                    index: i,
1168                    layer_type: "Linear".to_string(),
1169                    param_count: 1024,
1170                    memory_mb: 1.0,
1171                    compute_flops: 1_000_000,
1172                    input_shape: vec![512],
1173                    output_shape: vec![512],
1174                    is_sampled: false,
1175                });
1176            }
1177        });
1178
1179        let reader_viz = Arc::clone(&visualizer);
1180        let reader = std::thread::spawn(move || {
1181            for _ in 0..200 {
1182                let _ = reader_viz.visualize(None);
1183            }
1184            let _ = tx.send(());
1185        });
1186
1187        rx.recv_timeout(Duration::from_secs(15))
1188            .expect("visualize (InteractiveHtml) must not deadlock against concurrent add_layer");
1189        writer.join().expect("writer thread panicked");
1190        reader.join().expect("reader thread panicked");
1191
1192        Ok(())
1193    }
1194}