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 requires the `video` or `gif` feature which gates the `image` crate.
469                // To enable: rebuild with `--features video` (or `--features gif`).
470                // Without that feature, fall back to a descriptive error so callers can
471                // switch to SVG/HTML output which works without any extra features.
472                #[cfg(feature = "video")]
473                {
474                    self.generate_png(&sampled_layers)?
475                }
476                #[cfg(not(feature = "video"))]
477                {
478                    return Err(anyhow::anyhow!(
479                        "PNG generation requires the `video` feature. \
480                         Rebuild with `--features video`, or use \
481                         VisualizationFormat::StaticSvg / InteractiveHtml instead."
482                    ));
483                }
484            },
485        };
486
487        // Save to file if path provided
488        if let Some(ref path) = output_path {
489            std::fs::write(path, &output_data)
490                .with_context(|| format!("Failed to write visualization to {}", path))?;
491            info!("Saved visualization to {}", path);
492        }
493
494        let time_taken = start_time.elapsed().as_secs_f64();
495        let state = self.state.read();
496
497        Ok(VisualizationResult {
498            output_path,
499            inline_data: if output_size < 1024 * 1024 { Some(output_data) } else { None }, // Include inline if < 1MB
500            stats: VisualizationStats {
501                layers_visualized: sampled_layers.len(),
502                total_layers: state.total_layers,
503                sampling_ratio: sampled_layers.len() as f64 / state.total_layers as f64,
504                memory_used_mb: state.current_memory_mb,
505                time_taken_secs: time_taken,
506                output_size_bytes: output_size,
507            },
508            sampled_layers,
509            model_stats,
510        })
511    }
512
513    /// Calculate overall model statistics
514    fn calculate_model_stats(&self) -> Result<ModelStatistics> {
515        let cache = self.layer_cache.read();
516
517        let total_params: usize = cache.values().map(|l| l.param_count).sum();
518        let total_memory_mb: f64 = cache.values().map(|l| l.memory_mb).sum();
519        let total_gflops: f64 = cache.values().map(|l| l.compute_flops).sum::<u64>() as f64 / 1e9;
520        let max_depth = cache.values().map(|l| l.index).max().unwrap_or(0);
521
522        let mut layer_types: HashMap<String, usize> = HashMap::new();
523        for metadata in cache.values() {
524            *layer_types.entry(metadata.layer_type.clone()).or_insert(0) += 1;
525        }
526
527        Ok(ModelStatistics {
528            total_params,
529            total_memory_mb,
530            total_gflops,
531            max_depth,
532            layer_types,
533        })
534    }
535
536    /// Generate text summary (minimal memory)
537    fn generate_text_summary(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
538        let cache = self.layer_cache.read();
539
540        let mut summary = String::from("=== Large Model Visualization Summary ===\n\n");
541
542        summary.push_str(&format!(
543            "Total Layers: {}\n",
544            self.state.read().total_layers
545        ));
546        summary.push_str(&format!("Visualized Layers: {}\n\n", sampled_layers.len()));
547
548        summary.push_str("Layer Details:\n");
549        for &idx in sampled_layers {
550            if let Some(layer) = cache.values().find(|l| l.index == idx) {
551                summary.push_str(&format!(
552                    "  [{}] {} - {} params, {:.2} MB, {:.1} GFLOPS\n",
553                    layer.index,
554                    layer.name,
555                    layer.param_count,
556                    layer.memory_mb,
557                    layer.compute_flops as f64 / 1e9
558                ));
559            }
560        }
561
562        let bytes = summary.into_bytes();
563        let size = bytes.len();
564        Ok((bytes, size))
565    }
566
567    /// Generate JSON metadata
568    fn generate_json_metadata(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
569        let cache = self.layer_cache.read();
570
571        let layers: Vec<_> = sampled_layers
572            .iter()
573            .filter_map(|&idx| cache.values().find(|l| l.index == idx).cloned())
574            .collect();
575
576        let json = serde_json::to_string_pretty(&layers)?;
577        let bytes = json.into_bytes();
578        let size = bytes.len();
579        Ok((bytes, size))
580    }
581
582    /// Generate static SVG
583    fn generate_static_svg(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
584        let cache = self.layer_cache.read();
585
586        let mut svg = String::from(
587            r#"<?xml version="1.0" encoding="UTF-8"?>
588<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
589<style>
590.layer { fill: #4a90e2; stroke: #2c5aa0; stroke-width: 2; }
591.layer-text { fill: white; font-family: Arial, sans-serif; font-size: 12px; }
592.title { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; }
593</style>
594<text x="600" y="30" class="title" text-anchor="middle">Model Architecture</text>
595"#,
596        );
597
598        let layer_height = 60;
599        let layer_width = 200;
600        let x_offset = 500;
601        let y_start = 60;
602
603        for (i, &idx) in sampled_layers.iter().enumerate() {
604            if let Some(layer) = cache.values().find(|l| l.index == idx) {
605                let y = y_start + i * (layer_height + 20);
606
607                svg.push_str(&format!(
608                    r#"<rect x="{}" y="{}" width="{}" height="{}" class="layer" />
609<text x="{}" y="{}" class="layer-text" text-anchor="middle">{}</text>
610<text x="{}" y="{}" class="layer-text" text-anchor="middle">{:.1}M params</text>
611"#,
612                    x_offset,
613                    y,
614                    layer_width,
615                    layer_height,
616                    x_offset + layer_width / 2,
617                    y + 25,
618                    layer.name,
619                    x_offset + layer_width / 2,
620                    y + 45,
621                    layer.param_count as f64 / 1e6
622                ));
623            }
624        }
625
626        svg.push_str("</svg>");
627
628        let bytes = svg.into_bytes();
629        let size = bytes.len();
630        Ok((bytes, size))
631    }
632
633    /// Generate interactive SVG with zoom/pan via embedded ECMAScript
634    fn generate_interactive_svg(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
635        let cache = self.layer_cache.read();
636
637        let layer_height = 60usize;
638        let layer_width = 200usize;
639        let x_offset = 500usize;
640        let y_start = 60usize;
641        let svg_height = y_start + sampled_layers.len() * (layer_height + 20) + 40;
642        let svg_width = 1200usize;
643
644        // Build the inner layer elements first
645        let mut layer_elems = String::new();
646        for (i, &idx) in sampled_layers.iter().enumerate() {
647            if let Some(layer) = cache.values().find(|l| l.index == idx) {
648                let y = y_start + i * (layer_height + 20);
649                layer_elems.push_str(&format!(
650                    r#"<rect x="{x}" y="{y}" width="{w}" height="{h}" class="layer" />
651<text x="{cx}" y="{ty}" class="layer-text" text-anchor="middle">{name}</text>
652<text x="{cx}" y="{py}" class="layer-text" text-anchor="middle">{params:.1}M params</text>
653"#,
654                    x = x_offset,
655                    y = y,
656                    w = layer_width,
657                    h = layer_height,
658                    cx = x_offset + layer_width / 2,
659                    ty = y + 25,
660                    py = y + 45,
661                    name = layer.name,
662                    params = layer.param_count as f64 / 1e6
663                ));
664            }
665        }
666
667        // Compose full SVG with embedded pan/zoom JavaScript.
668        // The <script> block uses an SVG foreignObject-free approach: it attaches
669        // pointer-event listeners directly to the root <svg> element and manipulates
670        // a <g id="viewport"> transform, which is valid SVG+JS in any modern browser.
671        let svg = format!(
672            r#"<?xml version="1.0" encoding="UTF-8"?>
673<svg xmlns="http://www.w3.org/2000/svg"
674     xmlns:xlink="http://www.w3.org/1999/xlink"
675     id="svg-root"
676     width="{width}" height="{height}"
677     viewBox="0 0 {width} {height}"
678     style="cursor:grab;user-select:none;">
679<style>
680.layer {{ fill: #4a90e2; stroke: #2c5aa0; stroke-width: 2; }}
681.layer-text {{ fill: white; font-family: Arial, sans-serif; font-size: 12px; }}
682.title {{ font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; }}
683</style>
684<text x="{title_x}" y="30" class="title" text-anchor="middle">Model Architecture (interactive)</text>
685<g id="viewport">
686{layers}
687</g>
688<script type="text/javascript"><![CDATA[
689(function() {{
690  var svg   = document.getElementById('svg-root');
691  var vp    = document.getElementById('viewport');
692  var tx = 0, ty = 0, scale = 1.0;
693  var dragging = false;
694  var startX = 0, startY = 0;
695
696  function applyTransform() {{
697    vp.setAttribute('transform',
698      'translate(' + tx + ',' + ty + ') scale(' + scale + ')');
699  }}
700
701  // Pan: mousedown / mousemove / mouseup
702  svg.addEventListener('mousedown', function(e) {{
703    dragging = true;
704    startX = e.clientX - tx;
705    startY = e.clientY - ty;
706    svg.style.cursor = 'grabbing';
707    e.preventDefault();
708  }});
709  window.addEventListener('mousemove', function(e) {{
710    if (!dragging) return;
711    tx = e.clientX - startX;
712    ty = e.clientY - startY;
713    applyTransform();
714  }});
715  window.addEventListener('mouseup', function() {{
716    dragging = false;
717    svg.style.cursor = 'grab';
718  }});
719
720  // Touch pan
721  var lastTouch = null;
722  svg.addEventListener('touchstart', function(e) {{
723    if (e.touches.length === 1) {{
724      lastTouch = e.touches[0];
725    }}
726    e.preventDefault();
727  }}, {{ passive: false }});
728  svg.addEventListener('touchmove', function(e) {{
729    if (e.touches.length === 1 && lastTouch) {{
730      var t = e.touches[0];
731      tx += t.clientX - lastTouch.clientX;
732      ty += t.clientY - lastTouch.clientY;
733      lastTouch = t;
734      applyTransform();
735    }}
736    e.preventDefault();
737  }}, {{ passive: false }});
738  svg.addEventListener('touchend', function() {{ lastTouch = null; }});
739
740  // Zoom: mousewheel
741  svg.addEventListener('wheel', function(e) {{
742    e.preventDefault();
743    var delta = e.deltaY > 0 ? 0.9 : 1.1;
744    // Zoom towards cursor position
745    var rect  = svg.getBoundingClientRect();
746    var mx = e.clientX - rect.left;
747    var my = e.clientY - rect.top;
748    tx = mx - (mx - tx) * delta;
749    ty = my - (my - ty) * delta;
750    scale = Math.max(0.1, Math.min(10.0, scale * delta));
751    applyTransform();
752  }}, {{ passive: false }});
753
754  // Double-click to reset
755  svg.addEventListener('dblclick', function() {{
756    tx = 0; ty = 0; scale = 1.0;
757    applyTransform();
758  }});
759}})();
760]]></script>
761</svg>"#,
762            width = svg_width,
763            height = svg_height,
764            title_x = svg_width / 2,
765            layers = layer_elems,
766        );
767
768        let bytes = svg.into_bytes();
769        let size = bytes.len();
770        Ok((bytes, size))
771    }
772
773    /// Generate interactive HTML with JavaScript
774    fn generate_interactive_html(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
775        let cache = self.layer_cache.read();
776        let model_stats = self.calculate_model_stats()?;
777
778        let mut html = String::from(
779            r#"<!DOCTYPE html>
780<html>
781<head>
782<meta charset="UTF-8">
783<title>Large Model Visualization</title>
784<style>
785body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
786.container { max-width: 1200px; margin: 0 auto; }
787.header { background: #4a90e2; color: white; padding: 20px; border-radius: 8px; }
788.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
789.stat-card { background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
790.layer-list { background: white; padding: 20px; border-radius: 8px; }
791.layer { padding: 10px; margin: 5px 0; background: #f9f9f9; border-left: 4px solid #4a90e2; }
792</style>
793</head>
794<body>
795<div class="container">
796<div class="header">
797<h1>Large Model Visualization</h1>
798<p>Interactive view of model architecture</p>
799</div>
800<div class="stats">
801"#,
802        );
803
804        // Add stats cards
805        html.push_str(&format!(
806            r#"<div class="stat-card">
807<h3>{:.1}M</h3>
808<p>Total Parameters</p>
809</div>
810<div class="stat-card">
811<h3>{:.1} GB</h3>
812<p>Total Memory</p>
813</div>
814<div class="stat-card">
815<h3>{}</h3>
816<p>Total Layers</p>
817</div>
818<div class="stat-card">
819<h3>{}/{}</h3>
820<p>Visualized/Total</p>
821</div>
822"#,
823            model_stats.total_params as f64 / 1e6,
824            model_stats.total_memory_mb / 1024.0,
825            model_stats.max_depth + 1,
826            sampled_layers.len(),
827            self.state.read().total_layers
828        ));
829
830        html.push_str("</div><div class=\"layer-list\"><h2>Layer Details</h2>");
831
832        // Add layer details
833        for &idx in sampled_layers {
834            if let Some(layer) = cache.values().find(|l| l.index == idx) {
835                html.push_str(&format!(
836                    r#"<div class="layer">
837<strong>[{}] {}</strong><br>
838Type: {} | Parameters: {:.1}M | Memory: {:.2} MB | Compute: {:.1} GFLOPS
839</div>
840"#,
841                    layer.index,
842                    layer.name,
843                    layer.layer_type,
844                    layer.param_count as f64 / 1e6,
845                    layer.memory_mb,
846                    layer.compute_flops as f64 / 1e9
847                ));
848            }
849        }
850
851        html.push_str("</div></div></body></html>");
852
853        let bytes = html.into_bytes();
854        let size = bytes.len();
855        Ok((bytes, size))
856    }
857
858    /// Generate a static PNG heatmap visualization of the sampled layers.
859    ///
860    /// Each layer is rendered as a horizontal bar whose width is proportional to
861    /// `param_count` and whose colour encodes `memory_mb` (blue → red gradient).
862    /// The resulting image is PNG-encoded and returned as a raw byte vector.
863    ///
864    /// Requires the `video` feature (which enables the `image` crate).
865    #[cfg(feature = "video")]
866    fn generate_png(&self, sampled_layers: &[usize]) -> Result<(Vec<u8>, usize)> {
867        use image::{ImageBuffer, Rgb};
868        use std::io::Cursor;
869
870        let cache = self.layer_cache.read();
871
872        // Gather layers in index order.
873        let mut layers: Vec<&LayerMetadata> = sampled_layers
874            .iter()
875            .filter_map(|&idx| cache.values().find(|l| l.index == idx))
876            .collect();
877        layers.sort_by_key(|l| l.index);
878
879        // Image layout constants.
880        const IMG_WIDTH: u32 = 1200;
881        const BAR_HEIGHT: u32 = 30;
882        const BAR_PADDING: u32 = 6;
883        const LEFT_MARGIN: u32 = 20;
884        const RIGHT_MARGIN: u32 = 20;
885
886        let row_height = BAR_HEIGHT + BAR_PADDING;
887        let img_height = if layers.is_empty() {
888            100
889        } else {
890            layers.len() as u32 * row_height + 2 * BAR_PADDING + 40 // +40 for title row
891        };
892
893        let max_params = layers.iter().map(|l| l.param_count).max().unwrap_or(1).max(1);
894
895        let max_memory = layers.iter().map(|l| l.memory_mb).fold(0.0_f64, f64::max).max(1.0);
896
897        let available_width = IMG_WIDTH - LEFT_MARGIN - RIGHT_MARGIN;
898
899        let mut img = ImageBuffer::<Rgb<u8>, Vec<u8>>::new(IMG_WIDTH, img_height);
900
901        // Background: near-white.
902        for pixel in img.pixels_mut() {
903            *pixel = Rgb([245u8, 245u8, 250u8]);
904        }
905
906        // Title bar.
907        for x in 0..IMG_WIDTH {
908            for y in 0..36 {
909                img.put_pixel(x, y, Rgb([74u8, 144u8, 226u8]));
910            }
911        }
912
913        // Draw each layer as a horizontal heatmap bar.
914        for (i, layer) in layers.iter().enumerate() {
915            let bar_top = 40 + i as u32 * row_height;
916
917            // Bar width proportional to param_count.
918            let bar_w = ((layer.param_count as f64 / max_params as f64) * available_width as f64)
919                .round() as u32;
920            let bar_w = bar_w.max(4); // always visible
921
922            // Colour: blue (low memory) → red (high memory) gradient.
923            let t = (layer.memory_mb / max_memory).clamp(0.0, 1.0) as f32;
924            let r = (t * 220.0) as u8;
925            let g = ((1.0 - t) * 100.0 + 40.0) as u8;
926            let b = ((1.0 - t) * 220.0) as u8;
927            let bar_colour = Rgb([r, g, b]);
928
929            for x in LEFT_MARGIN..(LEFT_MARGIN + bar_w).min(IMG_WIDTH - RIGHT_MARGIN) {
930                for y in bar_top..(bar_top + BAR_HEIGHT).min(img_height) {
931                    img.put_pixel(x, y, bar_colour);
932                }
933            }
934        }
935
936        // Encode as PNG into an in-memory buffer.
937        let mut png_bytes: Vec<u8> = Vec::new();
938        img.write_to(&mut Cursor::new(&mut png_bytes), image::ImageFormat::Png)
939            .with_context(|| "Failed to PNG-encode large model visualization")?;
940
941        let size = png_bytes.len();
942        Ok((png_bytes, size))
943    }
944
945    /// Get current visualization progress (0.0-1.0)
946    pub fn get_progress(&self) -> f64 {
947        self.state.read().progress
948    }
949
950    /// Get memory usage statistics
951    pub fn get_memory_stats(&self) -> MemoryStats {
952        let state = self.state.read();
953        MemoryStats {
954            current_mb: state.current_memory_mb,
955            max_mb: self.config.max_memory_mb as f64,
956            utilization_pct: (state.current_memory_mb / self.config.max_memory_mb as f64 * 100.0)
957                .min(100.0),
958        }
959    }
960}
961
962/// Memory usage statistics
963#[derive(Debug, Clone, Serialize, Deserialize)]
964pub struct MemoryStats {
965    /// Current memory usage (MB)
966    pub current_mb: f64,
967    /// Maximum allowed memory (MB)
968    pub max_mb: f64,
969    /// Utilization percentage
970    pub utilization_pct: f64,
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976
977    #[test]
978    fn test_visualizer_creation() {
979        let config = LargeModelVisualizerConfig::default();
980        let _visualizer = LargeModelVisualizer::new(config);
981    }
982
983    #[test]
984    fn test_add_layers() -> Result<()> {
985        let config = LargeModelVisualizerConfig::default();
986        let visualizer = LargeModelVisualizer::new(config);
987
988        for i in 0..10 {
989            let metadata = LayerMetadata {
990                name: format!("layer_{}", i),
991                index: i,
992                layer_type: "Linear".to_string(),
993                param_count: 1024 * 1024,
994                memory_mb: 4.0,
995                compute_flops: 1_000_000_000,
996                input_shape: vec![512],
997                output_shape: vec![512],
998                is_sampled: false,
999            };
1000            visualizer.add_layer(metadata)?;
1001        }
1002
1003        let stats = visualizer.get_memory_stats();
1004        assert_eq!(stats.current_mb, 40.0);
1005
1006        Ok(())
1007    }
1008
1009    #[test]
1010    fn test_uniform_sampling() -> Result<()> {
1011        let config = LargeModelVisualizerConfig {
1012            max_full_layers: 5,
1013            sampling_strategy: SamplingStrategy::Uniform,
1014            ..Default::default()
1015        };
1016
1017        let visualizer = LargeModelVisualizer::new(config);
1018
1019        // Add 20 layers
1020        for i in 0..20 {
1021            let metadata = LayerMetadata {
1022                name: format!("layer_{}", i),
1023                index: i,
1024                layer_type: "Linear".to_string(),
1025                param_count: 1024 * 1024,
1026                memory_mb: 4.0,
1027                compute_flops: 1_000_000_000,
1028                input_shape: vec![512],
1029                output_shape: vec![512],
1030                is_sampled: false,
1031            };
1032            visualizer.add_layer(metadata)?;
1033        }
1034
1035        let sampled = visualizer.determine_sampling()?;
1036        assert_eq!(sampled.len(), 5);
1037
1038        Ok(())
1039    }
1040
1041    #[cfg(feature = "video")]
1042    #[test]
1043    fn test_png_visualization() -> Result<()> {
1044        let config = LargeModelVisualizerConfig {
1045            output_format: VisualizationFormat::StaticPng,
1046            ..Default::default()
1047        };
1048
1049        let visualizer = LargeModelVisualizer::new(config);
1050
1051        for i in 0..5_usize {
1052            let metadata = LayerMetadata {
1053                name: format!("layer_{}", i),
1054                index: i,
1055                layer_type: "Linear".to_string(),
1056                param_count: 1024 * (i + 1),
1057                memory_mb: 2.0 * (i + 1) as f64,
1058                compute_flops: 500_000_000 * (i + 1) as u64,
1059                input_shape: vec![512],
1060                output_shape: vec![512],
1061                is_sampled: false,
1062            };
1063            visualizer.add_layer(metadata)?;
1064        }
1065
1066        let result = visualizer.visualize(None)?;
1067
1068        // Basic sanity checks
1069        assert_eq!(result.stats.layers_visualized, 5);
1070        assert!(
1071            result.stats.output_size_bytes > 0,
1072            "PNG output must be non-empty"
1073        );
1074
1075        // Verify PNG magic bytes: 0x89 P N G
1076        let data = result.inline_data.expect("inline data should be present for small PNG");
1077        assert!(
1078            data.starts_with(&[0x89, 0x50, 0x4E, 0x47]),
1079            "Output must start with PNG magic bytes"
1080        );
1081
1082        Ok(())
1083    }
1084
1085    #[test]
1086    fn test_text_visualization() -> Result<()> {
1087        let config = LargeModelVisualizerConfig {
1088            output_format: VisualizationFormat::TextSummary,
1089            ..Default::default()
1090        };
1091
1092        let visualizer = LargeModelVisualizer::new(config);
1093
1094        // Add a few layers
1095        for i in 0..5 {
1096            let metadata = LayerMetadata {
1097                name: format!("layer_{}", i),
1098                index: i,
1099                layer_type: "Linear".to_string(),
1100                param_count: 1024 * 1024 * (i + 1),
1101                memory_mb: 4.0 * (i + 1) as f64,
1102                compute_flops: 1_000_000_000 * (i + 1) as u64,
1103                input_shape: vec![512],
1104                output_shape: vec![512],
1105                is_sampled: false,
1106            };
1107            visualizer.add_layer(metadata)?;
1108        }
1109
1110        let result = visualizer.visualize(None)?;
1111
1112        assert_eq!(result.stats.layers_visualized, 5);
1113        assert!(result.stats.output_size_bytes > 0);
1114
1115        Ok(())
1116    }
1117}