1#![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#[derive(Debug)]
27pub struct LargeModelVisualizer {
28 config: LargeModelVisualizerConfig,
30 layer_cache: Arc<RwLock<HashMap<String, LayerMetadata>>>,
32 state: Arc<RwLock<VisualizationState>>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LargeModelVisualizerConfig {
39 pub enable_smart_sampling: bool,
41 pub max_full_layers: usize,
43 pub sampling_strategy: SamplingStrategy,
45 pub enable_hierarchical: bool,
47 pub enable_streaming: bool,
49 pub max_memory_mb: usize,
51 pub stream_chunk_size: usize,
53 pub enable_progressive_loading: bool,
55 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, stream_chunk_size: 10,
69 enable_progressive_loading: true,
70 output_format: VisualizationFormat::InteractiveSvg,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub enum SamplingStrategy {
78 Uniform,
80 Adaptive,
82 Representative,
84 ImportanceBased,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub enum VisualizationFormat {
91 StaticPng,
93 StaticSvg,
95 InteractiveSvg,
97 InteractiveHtml,
99 TextSummary,
101 JsonMetadata,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct LayerMetadata {
108 pub name: String,
110 pub index: usize,
112 pub layer_type: String,
114 pub param_count: usize,
116 pub memory_mb: f64,
118 pub compute_flops: u64,
120 pub input_shape: Vec<usize>,
122 pub output_shape: Vec<usize>,
124 pub is_sampled: bool,
126}
127
128#[derive(Debug, Clone, Default)]
130struct VisualizationState {
131 total_layers: usize,
133 loaded_layers: Vec<String>,
135 current_memory_mb: f64,
137 progress: f64,
139 is_complete: bool,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct VisualizationResult {
146 pub output_path: Option<String>,
148 pub inline_data: Option<Vec<u8>>,
150 pub stats: VisualizationStats,
152 pub sampled_layers: Vec<usize>,
154 pub model_stats: ModelStatistics,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct VisualizationStats {
161 pub layers_visualized: usize,
163 pub total_layers: usize,
165 pub sampling_ratio: f64,
167 pub memory_used_mb: f64,
169 pub time_taken_secs: f64,
171 pub output_size_bytes: usize,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ModelStatistics {
178 pub total_params: usize,
180 pub total_memory_mb: f64,
182 pub total_gflops: f64,
184 pub max_depth: usize,
186 pub layer_types: HashMap<String, usize>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct LayerGroup {
193 pub name: String,
195 pub layers: Vec<usize>,
197 pub collapsed: bool,
199 pub summary: GroupSummary,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct GroupSummary {
206 pub param_count: usize,
208 pub memory_mb: f64,
210 pub avg_compute_flops: u64,
212}
213
214impl LargeModelVisualizer {
215 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 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 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 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 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 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 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 if !layers.is_empty() {
306 sampled.push(0);
307 sampled.push(layers.len() - 1);
308 }
309
310 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 variances.sort_by_key(|item| std::cmp::Reverse(item.1));
320
321 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 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 sampled.extend(0..3.min(total_layers));
341
342 let mid = total_layers / 2;
344 sampled.extend((mid.saturating_sub(1))..=(mid + 1).min(total_layers - 1));
345
346 sampled.extend((total_layers.saturating_sub(3))..total_layers);
348
349 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 fn importance_sampling(&self, cache: &HashMap<String, LayerMetadata>) -> Vec<usize> {
364 let mut layers: Vec<_> = cache.values().collect();
365
366 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 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 return Ok(Vec::new());
385 }
386
387 let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
388
389 for metadata in cache.values() {
391 groups.entry(metadata.layer_type.clone()).or_default().push(metadata.index);
392 }
393
394 let mut layer_groups = Vec::new();
396
397 for (layer_type, indices) in groups {
398 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, summary: GroupSummary {
419 param_count,
420 memory_mb,
421 avg_compute_flops,
422 },
423 });
424 }
425
426 layer_groups.sort_by_key(|g| g.layers.first().copied().unwrap_or(0));
428
429 Ok(layer_groups)
430 }
431
432 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 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 let model_stats = self.calculate_model_stats()?;
455
456 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 #[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 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 }, 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 };
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 for pixel in img.pixels_mut() {
903 *pixel = Rgb([245u8, 245u8, 250u8]);
904 }
905
906 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 for (i, layer) in layers.iter().enumerate() {
915 let bar_top = 40 + i as u32 * row_height;
916
917 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); 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 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 pub fn get_progress(&self) -> f64 {
947 self.state.read().progress
948 }
949
950 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#[derive(Debug, Clone, Serialize, Deserialize)]
964pub struct MemoryStats {
965 pub current_mb: f64,
967 pub max_mb: f64,
969 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 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 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 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 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}