barrosoluqueroberto commited on
Commit
56bbc8d
Β·
verified Β·
1 Parent(s): 86d35f2

Update model inspector

Browse files
Makefile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: setup install clean lint test format dev
2
+
3
+ setup:
4
+ @echo "Setting up local environment..."
5
+ @../scripts/install_uv.sh
6
+ @uv python install 3.11
7
+ @../scripts/create_venv.sh
8
+ @. .venv/bin/activate && make install
9
+
10
+ install:
11
+ @echo "Installing dependencies..."
12
+ uv pip install -e .
13
+
14
+ clean:
15
+ @echo "Cleaning up..."
16
+ rm -rf .venv
17
+ rm -rf dist
18
+ rm -rf *.egg-info
19
+ find . -type d -name __pycache__ -exec rm -rf {} +
20
+ find . -type d -name .pytest_cache -exec rm -rf {} +
21
+ find . -type d -name .ipynb_checkpoints -exec rm -rf {} +
README.md CHANGED
@@ -1,14 +1,4 @@
1
  ---
2
- title: Model Inspector
3
- emoji: 😻
4
- colorFrom: green
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 5.41.0
8
- app_file: app.py
9
  pinned: false
10
- license: mit
11
- short_description: 'Interactive tool to compare any LLM architecture '
12
  ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
 
 
 
 
2
  sdk: gradio
 
 
3
  pinned: false
 
 
4
  ---
 
 
pyproject.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "llm_architect"
3
+ version = "0.1.0"
4
+ description = ""
5
+ requires-python = ">=3.11"
6
+
7
+ [tool.setuptools.dynamic]
8
+ dependencies = {file = ["requirements.txt"]}
9
+
10
+ [tool.setuptools.packages.find]
11
+ where = ["src"]
12
+ namespaces = false
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ transformers==4.55.0
2
+ gradio==5.41.0
3
+ gradio-client==1.11.0
4
+ ipython==9.4.0
5
+ plotly==6.2.0
scripts/create_venv.sh ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Exit on any error
4
+ set -e
5
+
6
+ # 3. Create a virtual environment if not already created
7
+ VENV_DIR=".venv"
8
+ if [ ! -d "$VENV_DIR" ]; then
9
+ echo "Virtual environment not found. Creating a new virtual environment..."
10
+ uv venv --python 3.11
11
+ else
12
+ echo "Virtual environment already exists."
13
+ fi
scripts/install_uv.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Exit on any error
4
+ set -e
5
+
6
+ # Function to check if a command exists
7
+ command_exists() {
8
+ command -v "$1" >/dev/null 2>&1
9
+ }
10
+
11
+ # 1. Check if 'uv' is installed
12
+ if ! command_exists uv; then
13
+ echo "'uv' is not installed. Installing 'uv'..."
14
+ # Install 'uv'
15
+ curl -LsSf https://astral.sh/uv/install.sh | sh
16
+ source ~/.cargo/env
17
+ else
18
+ echo "'uv' is already installed."
19
+ fi
scripts/setup_ssl.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+
5
+ def check_openssl():
6
+ """Check if OpenSSL is available."""
7
+ try:
8
+ subprocess.run(["openssl", "version"], capture_output=True, check=True)
9
+ return True
10
+ except (subprocess.CalledProcessError, FileNotFoundError):
11
+ return False
12
+
13
+
14
+ def generate_ssl_certificates():
15
+ """Generate self-signed SSL certificates for local development."""
16
+ cert_dir = Path("")
17
+ key_file = cert_dir / "key.pem"
18
+ cert_file = cert_dir / "cert.pem"
19
+
20
+ # Check if certificates already exist
21
+ if key_file.exists() and cert_file.exists():
22
+ print("βœ… SSL certificates already exist!")
23
+ return True
24
+
25
+ if not check_openssl():
26
+ print("❌ OpenSSL not found. Please install OpenSSL first.")
27
+ print("On Ubuntu/Debian: sudo apt-get install openssl")
28
+ print("On macOS: brew install openssl")
29
+ print(
30
+ "On Windows: Download from https://slproweb.com/products/Win32OpenSSL.html"
31
+ )
32
+ return False
33
+
34
+ print("πŸ” Generating self-signed SSL certificates...")
35
+
36
+ # Generate private key
37
+ key_cmd = ["openssl", "genrsa", "-out", str(key_file), "2048"]
38
+
39
+ # Generate certificate
40
+ cert_cmd = [
41
+ "openssl",
42
+ "req",
43
+ "-new",
44
+ "-x509",
45
+ "-key",
46
+ str(key_file),
47
+ "-out",
48
+ str(cert_file),
49
+ "-days",
50
+ "365",
51
+ "-subj",
52
+ "/C=US/ST=CA/L=Local/O=Dev/CN=localhost",
53
+ ]
54
+
55
+ try:
56
+ subprocess.run(key_cmd, check=True)
57
+ subprocess.run(cert_cmd, check=True)
58
+ print("βœ… SSL certificates generated successfully!")
59
+ print(f" Private key: {key_file}")
60
+ print(f" Certificate: {cert_file}")
61
+ return True
62
+ except subprocess.CalledProcessError as e:
63
+ print(f"❌ Failed to generate SSL certificates: {e}")
64
+ return False
65
+
66
+
67
+ def main():
68
+ """Main function to set up SSL certificates."""
69
+ print("πŸš€ Setting up SSL certificates for local HTTPS...")
70
+
71
+ if generate_ssl_certificates():
72
+ print("\nπŸ“‹ Next steps:")
73
+ print("1. Run your Gradio app with the generated certificates")
74
+ print("2. Open https://localhost:7860 in your browser")
75
+ print(
76
+ "3. Accept the security warning (click 'Advanced' β†’ 'Proceed to localhost')"
77
+ )
78
+ print("4. Allow microphone access when prompted")
79
+ print(
80
+ "\n⚠️ Note: You'll see a security warning because these are self-signed certificates."
81
+ )
82
+ print(
83
+ " This is normal for local development - just click through the warning."
84
+ )
85
+ else:
86
+ print("\n❌ SSL setup failed. Alternative options:")
87
+ print(
88
+ "1. Use Chrome with: --unsafely-treat-insecure-origin-as-secure=http://localhost:7860"
89
+ )
90
+ print(
91
+ "2. Use Firefox and set media.devices.insecure.enabled=true in about:config"
92
+ )
93
+ print("3. Deploy to a server with proper SSL certificates")
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (194 Bytes). View file
 
src/app.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from typing import Dict, Any, Optional, List, Tuple, Union
3
+ import plotly.graph_objects as go
4
+ from plotly.subplots import make_subplots
5
+ from transformers import AutoConfig
6
+ import logging
7
+
8
+ # Set up logging
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class PlotlyModelArchitectureVisualizer:
14
+ def __init__(self, hf_token: Optional[str] = None):
15
+ self.config = None
16
+ self.hf_token = hf_token
17
+
18
+ # Universal color scheme - consistent across all models
19
+ self.universal_colors = {
20
+ 'embedding': '#f8f9fa', # Light gray for embeddings
21
+ 'layer_norm': '#e9ecef', # Light gray for layer norms
22
+ 'attention': '#495057', # Dark gray for attention
23
+ 'output': '#f8f9fa', # Light gray for output layers
24
+ 'text': '#212529', # Dark text
25
+ 'container_outer': '#dee2e6', # Outer container
26
+ 'moe_inner': '#d4edda', # Green background for MoE models
27
+ 'dense_inner': '#f8d7da', # Red background for dense models
28
+ 'feedforward_moe': '#28a745', # Green for MoE FFN
29
+ 'feedforward_dense': '#dc3545', # Red for dense FFN
30
+ 'router': '#fd7e14', # Orange for router
31
+ 'expert': '#20c997', # Teal for experts
32
+ 'callout_bg': 'rgba(255,255,255,0.9)',
33
+ 'accent_blue': '#007bff',
34
+ 'accent_green': '#28a745',
35
+ 'accent_red': '#dc3545'
36
+ }
37
+
38
+ def get_model_config(self, model_name: str) -> Dict[str, Any]:
39
+ """Fetch model configuration from Hugging Face"""
40
+ try:
41
+ logger.info(f"Fetching config for {model_name}")
42
+ config = AutoConfig.from_pretrained(model_name, token=self.hf_token, trust_remote_code=True)
43
+ return config.to_dict()
44
+ except Exception as e:
45
+ logger.error(f"Error fetching config for {model_name}: {e}")
46
+ return {}
47
+
48
+ def extract_config_values(self, config: Dict[str, Any]) -> Dict[str, Any]:
49
+ """Extract and normalize configuration values with architecture detection"""
50
+
51
+ # Detect model architecture type
52
+ model_type = config.get('model_type', 'unknown').lower()
53
+ is_moe = any(key in config for key in [
54
+ 'num_experts', 'n_routed_experts', 'moe_intermediate_size',
55
+ 'num_experts_per_tok', 'router_aux_loss_coef'
56
+ ])
57
+
58
+ # Extract MoE-specific parameters
59
+ moe_params = {}
60
+ if is_moe:
61
+ moe_params = {
62
+ 'num_experts': config.get('num_experts', config.get('n_routed_experts', 'N/A')),
63
+ 'experts_per_token': config.get('num_experts_per_tok', 'N/A'),
64
+ 'moe_intermediate_size': config.get('moe_intermediate_size', 'N/A'),
65
+ 'router_aux_loss': config.get('router_aux_loss_coef', config.get('aux_loss_alpha', 'N/A')),
66
+ 'shared_experts': config.get('n_shared_experts', 0)
67
+ }
68
+
69
+ # Calculate model size estimate (simplified)
70
+ hidden_size = config.get('hidden_size', config.get('d_model', config.get('n_embd', 0)))
71
+ num_layers = config.get('num_hidden_layers', config.get('n_layer', config.get('num_layers', 0)))
72
+ vocab_size = config.get('vocab_size', 0)
73
+
74
+ if isinstance(hidden_size, int) and isinstance(num_layers, int) and isinstance(vocab_size, int):
75
+ # Very rough parameter count estimation
76
+ if is_moe:
77
+ # MoE models are much larger but use fewer parameters per token
78
+ estimated_params = (hidden_size * num_layers * vocab_size) // 1000000 # Simplified
79
+ size_suffix = "B" if estimated_params > 1000 else "M"
80
+ estimated_params = estimated_params // 1000 if estimated_params > 1000 else estimated_params
81
+ else:
82
+ estimated_params = (hidden_size * num_layers * vocab_size) // 1000000
83
+ size_suffix = "B" if estimated_params > 1000 else "M"
84
+ estimated_params = estimated_params // 1000 if estimated_params > 1000 else estimated_params
85
+ else:
86
+ estimated_params = "Unknown"
87
+ size_suffix = ""
88
+
89
+ return {
90
+ 'model_type': config.get('model_type', 'unknown'),
91
+ 'hidden_size': hidden_size if hidden_size != 0 else 'N/A',
92
+ 'num_layers': num_layers if num_layers != 0 else 'N/A',
93
+ 'num_heads': config.get('num_attention_heads', config.get('n_head', config.get('num_heads', 'N/A'))),
94
+ 'vocab_size': vocab_size if vocab_size != 0 else 'N/A',
95
+ 'max_position': config.get('max_position_embeddings',
96
+ config.get('n_positions', config.get('max_seq_len', 'N/A'))),
97
+ 'intermediate_size': config.get('intermediate_size',
98
+ config.get('d_ff', hidden_size if hidden_size != 0 else 'N/A')),
99
+ 'is_moe': is_moe,
100
+ 'moe_params': moe_params,
101
+ 'estimated_size': f"{estimated_params}{size_suffix}" if estimated_params != "Unknown" else "Unknown",
102
+ 'kv_heads': config.get('num_key_value_heads', config.get('num_heads', 'N/A')),
103
+ 'head_dim': config.get('head_dim', config.get('qk_nope_head_dim', 'N/A')),
104
+ 'activation': config.get('hidden_act', config.get('activation_function', 'N/A'))
105
+ }
106
+
107
+ def add_container(self, fig: go.Figure, x: float, y: float, width: float, height: float,
108
+ color: str, line_width: int = 1, row: int = 1, col: int = 1) -> None:
109
+ """Add a container/background box"""
110
+ fig.add_shape(
111
+ type="rect",
112
+ x0=x, y0=y, x1=x + width, y1=y + height,
113
+ fillcolor=color,
114
+ line=dict(color='black', width=line_width),
115
+ layer="below",
116
+ row=row, col=col
117
+ )
118
+
119
+ def add_layer_box(self, fig: go.Figure, x: float, y: float, width: float, height: float,
120
+ text: str, color: str, hover_text: str = None, row: int = 1, col: int = 1,
121
+ text_size: int = 7) -> None:
122
+ """Add a rounded rectangle representing a layer"""
123
+
124
+ # Add the box shape
125
+ fig.add_shape(
126
+ type="rect",
127
+ x0=x, y0=y, x1=x + width, y1=y + height,
128
+ fillcolor=color,
129
+ line=dict(color='black', width=1),
130
+ layer="below",
131
+ row=row, col=col
132
+ )
133
+
134
+ # Add text label
135
+ fig.add_annotation(
136
+ x=x + width / 2,
137
+ y=y + height / 2,
138
+ text=text,
139
+ showarrow=False,
140
+ font=dict(size=text_size, color=self.universal_colors['text']),
141
+ bgcolor=self.universal_colors['callout_bg'],
142
+ bordercolor="black",
143
+ borderwidth=1,
144
+ row=row, col=col
145
+ )
146
+
147
+ # Add invisible scatter point for hover functionality
148
+ if hover_text:
149
+ fig.add_trace(go.Scatter(
150
+ x=[x + width / 2],
151
+ y=[y + height / 2],
152
+ mode='markers',
153
+ marker=dict(size=12, opacity=0),
154
+ hovertemplate=f"<b>{text}</b><br>{hover_text}<extra></extra>",
155
+ showlegend=False,
156
+ name=text
157
+ ), row=row, col=col)
158
+
159
+ def add_moe_router_visualization(self, fig: go.Figure, x: float, y: float,
160
+ config_values: Dict[str, Any], row: int = 1, col: int = 1) -> None:
161
+ """Add MoE router and expert visualization with improved layout"""
162
+
163
+ moe_params = config_values['moe_params']
164
+
165
+ # Router box - positioned more centrally
166
+ router_width, router_height = 0.4, 0.12
167
+ router_x = x + 0.2 # Center it better within the available space
168
+
169
+ self.add_layer_box(
170
+ fig, router_x, y, router_width, router_height,
171
+ "Router", self.universal_colors['router'],
172
+ f"{moe_params['experts_per_token']} experts activated <br>from {moe_params['num_experts']} total",
173
+ row, col, 6
174
+ )
175
+
176
+ # Expert boxes - positioned with better spacing
177
+ expert_y = y - 0.25 # Closer to router
178
+ expert_width, expert_height = 0.18, 0.1
179
+ experts_to_show = min(3, int(moe_params['experts_per_token']) if isinstance(moe_params['experts_per_token'],
180
+ int) else 3)
181
+
182
+ # Center the experts under the router
183
+ total_expert_width = experts_to_show * expert_width + (experts_to_show - 1) * 0.04
184
+ experts_start_x = router_x + (router_width - total_expert_width) / 2
185
+
186
+ for i in range(experts_to_show):
187
+ expert_x = experts_start_x + i * (expert_width + 0.04)
188
+ self.add_layer_box(
189
+ fig, expert_x, expert_y, expert_width, expert_height,
190
+ f"Expert\n{i + 1}", self.universal_colors['expert'],
191
+ f"MoE intermediate size: {moe_params['moe_intermediate_size']}",
192
+ row, col, 5
193
+ )
194
+
195
+ # Arrow from router to expert - pointing downward
196
+ self.add_connection_arrow(
197
+ fig, router_x + router_width / 2, y,
198
+ expert_x + expert_width / 2, expert_y + expert_height, row, col
199
+ )
200
+
201
+ # Add "..." if more experts exist - positioned to the right
202
+ if experts_to_show < int(moe_params['experts_per_token']) if isinstance(moe_params['experts_per_token'],
203
+ int) else False:
204
+ fig.add_annotation(
205
+ x=experts_start_x + experts_to_show * (expert_width + 0.04) + 0.05,
206
+ y=expert_y + expert_height / 2,
207
+ text="...",
208
+ showarrow=False,
209
+ font=dict(size=8, color=self.universal_colors['text']),
210
+ row=row, col=col
211
+ )
212
+
213
+ def add_side_panel(self, fig: go.Figure, x: float, y: float, width: float, height: float,
214
+ title: str, components: List[str], config_values: Dict[str, Any],
215
+ row: int = 1, col: int = 1) -> None:
216
+ """Add a side panel with component breakdown"""
217
+
218
+ # Panel container with dashed border
219
+ fig.add_shape(
220
+ type="rect",
221
+ x0=x, y0=y, x1=x + width, y1=y + height,
222
+ fillcolor=self.universal_colors['callout_bg'],
223
+ line=dict(color='gray', width=1, dash='dash'),
224
+ layer="below",
225
+ row=row, col=col
226
+ )
227
+
228
+ # Panel title
229
+ fig.add_annotation(
230
+ x=x + width / 2, y=y + height - 0.08,
231
+ text=f"<b>{title}</b>",
232
+ showarrow=False,
233
+ font=dict(size=8, color=self.universal_colors['text']),
234
+ row=row, col=col
235
+ )
236
+
237
+ # Component boxes
238
+ component_height = 0.1
239
+ start_y = y + height - 0.2
240
+
241
+ for i, component in enumerate(components):
242
+ comp_y = start_y - i * (component_height + 0.03)
243
+
244
+ if "Linear" in component:
245
+ color = self.universal_colors['output']
246
+ elif "activation" in component.lower() or "SiLU" in component or "ReLU" in component:
247
+ color = self.universal_colors['feedforward_moe'] if config_values['is_moe'] else self.universal_colors[
248
+ 'feedforward_dense']
249
+ else:
250
+ color = self.universal_colors['embedding']
251
+
252
+ self.add_layer_box(
253
+ fig, x + 0.03, comp_y, width - 0.06, component_height,
254
+ component, color, None, row, col, 6
255
+ )
256
+
257
+ def add_connection_arrow(self, fig: go.Figure, start_x: float, start_y: float,
258
+ end_x: float, end_y: float, row: int = 1, col: int = 1) -> None:
259
+ """Add an arrow between layers"""
260
+ fig.add_annotation(
261
+ x=end_x, y=end_y,
262
+ ax=start_x, ay=start_y,
263
+ xref=f'x{col}' if col > 1 else 'x',
264
+ yref=f'y{row}' if row > 1 else 'y',
265
+ axref=f'x{col}' if col > 1 else 'x',
266
+ ayref=f'y{row}' if row > 1 else 'y',
267
+ showarrow=True,
268
+ arrowhead=2,
269
+ arrowsize=1,
270
+ arrowwidth=1.5,
271
+ arrowcolor='black'
272
+ )
273
+
274
+ def create_single_model_diagram(self, fig: go.Figure, model_name: str,
275
+ config_values: Dict[str, Any], row: int = 1, col: int = 1) -> None:
276
+ """Add a single model's architecture to the subplot with improved layout"""
277
+
278
+ # Layout parameters
279
+ base_x, base_y = 0.3, 0.2
280
+ main_width, main_height = 2.2, 2.8
281
+ layer_width, layer_height = 1.8, 0.2
282
+
283
+ # Model title with size
284
+ model_display_name = model_name.split('/')[-1] if '/' in model_name else model_name
285
+ title_text = f"<b>{model_display_name}</b>"
286
+ if config_values['estimated_size'] != "Unknown":
287
+ title_text += f" ({config_values['estimated_size']})"
288
+
289
+ fig.add_annotation(
290
+ x=base_x + main_width / 2, y=base_y + main_height + 0.2,
291
+ text=title_text,
292
+ showarrow=False,
293
+ font=dict(size=10, color=self.universal_colors['accent_blue']),
294
+ row=row, col=col
295
+ )
296
+
297
+ # Outer container (main frame)
298
+ self.add_container(
299
+ fig, base_x - 0.1, base_y - 0.1, main_width + 0.2, main_height + 0.2,
300
+ self.universal_colors['container_outer'], 2, row, col
301
+ )
302
+
303
+ # Inner container (colored by architecture type)
304
+ inner_color = (self.universal_colors['moe_inner'] if config_values['is_moe']
305
+ else self.universal_colors['dense_inner'])
306
+ self.add_container(
307
+ fig, base_x + 0.1, base_y + 0.8, main_width - 0.2, main_height - 1.2,
308
+ inner_color, 1, row, col
309
+ )
310
+
311
+ # Layer definitions with universal colors
312
+ layers = [
313
+ ('Token Embedding', base_y + 0.3, self.universal_colors['embedding'],
314
+ f"Vocab: {config_values['vocab_size']:,}<br>Embedding dim: {config_values['hidden_size']}"),
315
+ ('Layer Norm', base_y + 0.6, self.universal_colors['layer_norm'],
316
+ 'Input normalization'),
317
+ (f'Multi-Head Attention\n({config_values["num_heads"]} heads)',
318
+ base_y + 0.9, self.universal_colors['attention'],
319
+ f"Heads: {config_values['num_heads']}<br>Hidden: {config_values['hidden_size']}<br>KV Heads: {config_values['kv_heads']}"),
320
+ ('Layer Norm', base_y + 1.2, self.universal_colors['layer_norm'],
321
+ 'Post-attention norm'),
322
+ ]
323
+
324
+ # Add MoE or Dense FFN layer
325
+ if config_values['is_moe']:
326
+ layers.append((
327
+ 'MoE Feed Forward',
328
+ base_y + 1.5, self.universal_colors['feedforward_moe'],
329
+ f"Experts: {config_values['moe_params']['num_experts']}<br>Active per token: {config_values['moe_params']['experts_per_token']}<br>MoE intermediate: {config_values['moe_params']['moe_intermediate_size']}"
330
+ ))
331
+ else:
332
+ layers.append((
333
+ 'Feed Forward Network',
334
+ base_y + 1.5, self.universal_colors['feedforward_dense'],
335
+ f"Intermediate size: {config_values['intermediate_size']}<br>Activation: {config_values['activation']}"
336
+ ))
337
+
338
+ layers.extend([
339
+ ('Layer Norm', base_y + 1.8, self.universal_colors['layer_norm'],
340
+ 'Post-FFN normalization'),
341
+ ('Output Projection', base_y + 2.1, self.universal_colors['output'],
342
+ f"Projects to vocab: {config_values['vocab_size']:,}")
343
+ ])
344
+
345
+ # Add all layers
346
+ layer_centers = []
347
+ for layer_name, y_pos, color, hover_info in layers:
348
+ layer_x = base_x + (main_width - layer_width) / 2
349
+
350
+ self.add_layer_box(
351
+ fig, layer_x, y_pos, layer_width, layer_height,
352
+ layer_name, color, hover_info, row, col
353
+ )
354
+
355
+ layer_centers.append((layer_x + layer_width / 2, y_pos + layer_height / 2))
356
+
357
+ # Add arrows between layers
358
+ for i in range(len(layer_centers) - 1):
359
+ start_x, start_y = layer_centers[i]
360
+ end_x, end_y = layer_centers[i + 1]
361
+
362
+ arrow_start_y = start_y + layer_height / 2
363
+ arrow_end_y = end_y - layer_height / 2
364
+
365
+ if arrow_end_y > arrow_start_y:
366
+ self.add_connection_arrow(fig, start_x, arrow_start_y, end_x, arrow_end_y, row, col)
367
+
368
+ # Add layer repetition indicator
369
+ if isinstance(config_values['num_layers'], int) and config_values['num_layers'] > 1:
370
+ fig.add_annotation(
371
+ x=base_x - 0.05, y=base_y + 1.4,
372
+ text=f"Γ—{config_values['num_layers']}<br>layers",
373
+ showarrow=False,
374
+ font=dict(size=7, color=self.universal_colors['text']),
375
+ bgcolor=self.universal_colors['callout_bg'],
376
+ bordercolor="black", borderwidth=1,
377
+ row=row, col=col
378
+ )
379
+
380
+ # Add side panel for component details
381
+ panel_x = base_x + main_width + 0.3
382
+ panel_y = base_y + 1.5 # Moved up to avoid MoE visualization
383
+ panel_width = 0.7
384
+ panel_height = 0.8
385
+
386
+ if config_values['is_moe']:
387
+ # MoE side panel
388
+ components = [
389
+ "Linear layer",
390
+ f"{config_values['activation'].upper()} activation",
391
+ "Linear layer",
392
+ "Router",
393
+ f"{config_values['moe_params']['experts_per_token']} active experts"
394
+ ]
395
+ panel_title = "MoE Module"
396
+ else:
397
+ # Dense FFN side panel
398
+ components = [
399
+ "Linear layer",
400
+ f"{config_values['activation'].upper()} activation",
401
+ "Linear layer"
402
+ ]
403
+ panel_title = "FeedForward Module"
404
+
405
+ self.add_side_panel(fig, panel_x, panel_y, panel_width, panel_height,
406
+ panel_title, components, config_values, row, col)
407
+
408
+ # Add MoE router visualization if applicable
409
+ if config_values['is_moe']:
410
+ # Position router visualization below side panel with better spacing
411
+ router_x = panel_x + 0.05
412
+ router_y = panel_y - 0.5
413
+ self.add_moe_router_visualization(fig, router_x, router_y, config_values, row, col)
414
+
415
+ def add_callout(self, fig: go.Figure, point_x: float, point_y: float,
416
+ text_x: float, text_y: float, text: str, row: int = 1, col: int = 1) -> None:
417
+ """Add a callout with leader line - arrow points FROM point TO text"""
418
+ fig.add_annotation(
419
+ x=text_x, y=text_y, # Text position
420
+ ax=point_x, ay=point_y, # Arrow start position (the component being referenced)
421
+ text=text,
422
+ showarrow=True,
423
+ arrowhead=2, arrowsize=1, arrowwidth=1,
424
+ arrowcolor='gray',
425
+ font=dict(size=7),
426
+ bgcolor=self.universal_colors['callout_bg'],
427
+ bordercolor="gray", borderwidth=1,
428
+ xref=f'x{col}' if col > 1 else 'x',
429
+ yref=f'y{row}' if row > 1 else 'y',
430
+ axref=f'x{col}' if col > 1 else 'x',
431
+ ayref=f'y{row}' if row > 1 else 'y'
432
+ )
433
+
434
+ def create_comparison_diagram(self, models_data: List[Tuple[str, Dict[str, Any]]]) -> go.Figure:
435
+ """Create comparison diagram for multiple models"""
436
+
437
+ num_models = len(models_data)
438
+ if num_models == 0:
439
+ return go.Figure()
440
+
441
+ # Create subplots - always use single row layout
442
+ if num_models == 1:
443
+ fig = make_subplots(rows=1, cols=1, subplot_titles=[models_data[0][0]])
444
+ elif num_models == 2:
445
+ fig = make_subplots(rows=1, cols=2,
446
+ subplot_titles=[model[0] for model in models_data])
447
+ else: # 3 models
448
+ fig = make_subplots(rows=1, cols=3,
449
+ subplot_titles=[model[0] for model in models_data])
450
+
451
+ # Set up layout
452
+ fig.update_layout(
453
+ height=700,
454
+ width=1200,
455
+ showlegend=False,
456
+ title_text="🧠 Model Architecture Comparison",
457
+ title_x=0.5,
458
+ title_font=dict(size=18)
459
+ )
460
+
461
+ # Add each model to its subplot
462
+ for i, (model_name, config_values) in enumerate(models_data):
463
+ row, col = 1, i + 1
464
+ self.create_single_model_diagram(fig, model_name, config_values, row, col)
465
+
466
+ # Update axes to hide ticks and labels - expanded range for callouts
467
+ fig.update_xaxes(showgrid=False, showticklabels=False, zeroline=False, range=[0, 5.0])
468
+ fig.update_yaxes(showgrid=False, showticklabels=False, zeroline=False, range=[-0.5, 3.5])
469
+
470
+ return fig
471
+
472
+ def generate_visualization(self, model_names: List[str]) -> Union[go.Figure, str]:
473
+ """Generate visualization for given models"""
474
+
475
+ # Filter out empty model names
476
+ valid_models = [name.strip() for name in model_names if name and name.strip()]
477
+
478
+ if not valid_models:
479
+ return "Please enter at least one model name."
480
+
481
+ models_data = []
482
+ errors = []
483
+
484
+ for model_name in valid_models:
485
+ try:
486
+ config = self.get_model_config(model_name)
487
+ if config:
488
+ config_values = self.extract_config_values(config)
489
+ models_data.append((model_name, config_values))
490
+ else:
491
+ errors.append(f"Could not load config for {model_name}")
492
+ except Exception as e:
493
+ errors.append(f"Error with {model_name}: {str(e)}")
494
+
495
+ if not models_data:
496
+ return f"❌ Could not load any models. Errors: {'; '.join(errors)}"
497
+
498
+ if errors:
499
+ logger.warning(f"Some models failed to load: {errors}")
500
+
501
+ try:
502
+ fig = self.create_comparison_diagram(models_data)
503
+ return fig
504
+ except Exception as e:
505
+ return f"❌ Error generating diagram: {str(e)}"
506
+
507
+
508
+ def create_gradio_interface():
509
+ """Create and configure the Gradio interface"""
510
+
511
+ visualizer = PlotlyModelArchitectureVisualizer()
512
+
513
+ def process_models(model1: str, model2: str = "", model3: str = "") -> Union[go.Figure, str]:
514
+ """Process the model inputs and generate visualization"""
515
+ models = [model1, model2, model3]
516
+ return visualizer.generate_visualization(models)
517
+
518
+ # Create the interface
519
+ with gr.Blocks(
520
+ title="🧠 Model Architecture Visualizer",
521
+ theme=gr.themes.Soft(),
522
+ css="""
523
+ .gradio-container {
524
+ max-width: 1200px !important;
525
+ }
526
+ .model-input {
527
+ font-family: monospace;
528
+ }
529
+ """
530
+ ) as demo:
531
+ gr.Markdown("""
532
+ # 🧠 Interactive Model Architecture Visualizer
533
+
534
+ Compare up to 3 Hugging Face transformer models side-by-side!
535
+ Enter model IDs below to see their architecture diagrams with interactive features.
536
+
537
+ ### πŸ“‹ How to Use
538
+
539
+ 1. **Enter Model IDs**: Use Hugging Face model identifiers (e.g., `moonshotai/Kimi-K2-Base`, `openai/gpt-oss-120b`, `deepseek-ai/DeepSeek-R1-0528`)
540
+ 2. **Compare Models**: Add up to 3 models to see them side-by-side
541
+ 3. **Explore Interactively**: Hover over components to see detailed specifications
542
+ """)
543
+
544
+ # Model inputs in a single row
545
+ gr.Markdown("### πŸ“ Model Configuration")
546
+ with gr.Row():
547
+ model1 = gr.Textbox(
548
+ label="Model 1 (Required)",
549
+ placeholder="e.g., openai/gpt-oss-120b",
550
+ value="openai/gpt-oss-120b",
551
+ elem_classes=["model-input"]
552
+ )
553
+
554
+ model2 = gr.Textbox(
555
+ label="Model 2 (Optional)",
556
+ placeholder="e.g., moonshotai/Kimi-K2-Base",
557
+ elem_classes=["model-input"]
558
+ )
559
+
560
+ model3 = gr.Textbox(
561
+ label="Model 3 (Optional)",
562
+ placeholder="e.g., deepseek-ai/DeepSeek-R1-0528",
563
+ elem_classes=["model-input"]
564
+ )
565
+
566
+ with gr.Row():
567
+ generate_btn = gr.Button("πŸš€ Generate Visualization", variant="primary", size="lg")
568
+ clear_btn = gr.Button("πŸ—‘οΈ Clear", variant="secondary")
569
+
570
+ # Visualization output - full width
571
+ output_plot = gr.Plot(
572
+ label="🧠 Architecture Visualization",
573
+ show_label=True
574
+ )
575
+
576
+ # Event handlers
577
+ generate_btn.click(
578
+ fn=process_models,
579
+ inputs=[model1, model2, model3],
580
+ outputs=output_plot
581
+ )
582
+
583
+ clear_btn.click(
584
+ fn=lambda: ("", "", "", None),
585
+ outputs=[model1, model2, model3, output_plot]
586
+ )
587
+
588
+ # Auto-generate for default model
589
+ demo.load(
590
+ fn=lambda: process_models("openai/gpt-oss-120b"),
591
+ outputs=output_plot
592
+ )
593
+
594
+ gr.Markdown("""Built with ❀️ using Plotly, Gradio, and Hugging Face Transformers""")
595
+
596
+ return demo
597
+
598
+
599
+ if __name__ == "__main__":
600
+ # Create and launch the app
601
+ demo = create_gradio_interface()
602
+
603
+ # For HuggingFace Spaces deployment
604
+ demo.launch(
605
+ share=False,
606
+ server_name="0.0.0.0",
607
+ server_port=7860,
608
+ show_error=True
609
+ )
src/llm_architect.egg-info/PKG-INFO ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: llm_architect
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.11
src/llm_architect.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ README.md
2
+ pyproject.toml
3
+ src/llm_architect.egg-info/PKG-INFO
4
+ src/llm_architect.egg-info/SOURCES.txt
5
+ src/llm_architect.egg-info/dependency_links.txt
6
+ src/llm_architect.egg-info/top_level.txt
src/llm_architect.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
src/llm_architect.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+