haoline 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. haoline/.streamlit/config.toml +10 -0
  2. haoline/__init__.py +248 -0
  3. haoline/analyzer.py +935 -0
  4. haoline/cli.py +2712 -0
  5. haoline/compare.py +811 -0
  6. haoline/compare_visualizations.py +1564 -0
  7. haoline/edge_analysis.py +525 -0
  8. haoline/eval/__init__.py +131 -0
  9. haoline/eval/adapters.py +844 -0
  10. haoline/eval/cli.py +390 -0
  11. haoline/eval/comparison.py +542 -0
  12. haoline/eval/deployment.py +633 -0
  13. haoline/eval/schemas.py +833 -0
  14. haoline/examples/__init__.py +15 -0
  15. haoline/examples/basic_inspection.py +74 -0
  16. haoline/examples/compare_models.py +117 -0
  17. haoline/examples/hardware_estimation.py +78 -0
  18. haoline/format_adapters.py +1001 -0
  19. haoline/formats/__init__.py +123 -0
  20. haoline/formats/coreml.py +250 -0
  21. haoline/formats/gguf.py +483 -0
  22. haoline/formats/openvino.py +255 -0
  23. haoline/formats/safetensors.py +273 -0
  24. haoline/formats/tflite.py +369 -0
  25. haoline/hardware.py +2307 -0
  26. haoline/hierarchical_graph.py +462 -0
  27. haoline/html_export.py +1573 -0
  28. haoline/layer_summary.py +769 -0
  29. haoline/llm_summarizer.py +465 -0
  30. haoline/op_icons.py +618 -0
  31. haoline/operational_profiling.py +1492 -0
  32. haoline/patterns.py +1116 -0
  33. haoline/pdf_generator.py +265 -0
  34. haoline/privacy.py +250 -0
  35. haoline/pydantic_models.py +241 -0
  36. haoline/report.py +1923 -0
  37. haoline/report_sections.py +539 -0
  38. haoline/risks.py +521 -0
  39. haoline/schema.py +523 -0
  40. haoline/streamlit_app.py +2024 -0
  41. haoline/tests/__init__.py +4 -0
  42. haoline/tests/conftest.py +123 -0
  43. haoline/tests/test_analyzer.py +868 -0
  44. haoline/tests/test_compare_visualizations.py +293 -0
  45. haoline/tests/test_edge_analysis.py +243 -0
  46. haoline/tests/test_eval.py +604 -0
  47. haoline/tests/test_format_adapters.py +460 -0
  48. haoline/tests/test_hardware.py +237 -0
  49. haoline/tests/test_hardware_recommender.py +90 -0
  50. haoline/tests/test_hierarchical_graph.py +326 -0
  51. haoline/tests/test_html_export.py +180 -0
  52. haoline/tests/test_layer_summary.py +428 -0
  53. haoline/tests/test_llm_patterns.py +540 -0
  54. haoline/tests/test_llm_summarizer.py +339 -0
  55. haoline/tests/test_patterns.py +774 -0
  56. haoline/tests/test_pytorch.py +327 -0
  57. haoline/tests/test_report.py +383 -0
  58. haoline/tests/test_risks.py +398 -0
  59. haoline/tests/test_schema.py +417 -0
  60. haoline/tests/test_tensorflow.py +380 -0
  61. haoline/tests/test_visualizations.py +316 -0
  62. haoline/universal_ir.py +856 -0
  63. haoline/visualizations.py +1086 -0
  64. haoline/visualize_yolo.py +44 -0
  65. haoline/web.py +110 -0
  66. haoline-0.3.0.dist-info/METADATA +471 -0
  67. haoline-0.3.0.dist-info/RECORD +70 -0
  68. haoline-0.3.0.dist-info/WHEEL +4 -0
  69. haoline-0.3.0.dist-info/entry_points.txt +5 -0
  70. haoline-0.3.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,462 @@
1
+ # Copyright (c) 2025 HaoLine Contributors
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Hierarchical Graph View for visualization.
6
+
7
+ Task 5.7: Creates collapsible, multi-level graph representations
8
+ for LLM-scale models where flat visualization would be unreadable.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from .analyzer import GraphInfo
20
+ from .patterns import Block
21
+
22
+
23
+ @dataclass
24
+ class HierarchicalNode:
25
+ """
26
+ A node in the hierarchical graph that can contain children.
27
+
28
+ Task 5.7.1: Create HierarchicalNode with children.
29
+ """
30
+
31
+ id: str
32
+ name: str
33
+ node_type: str # "op", "block", "layer", "model"
34
+ op_type: str | None = None # ONNX op type for leaf nodes
35
+
36
+ # Hierarchy
37
+ children: list[HierarchicalNode] = field(default_factory=list)
38
+ parent_id: str | None = None
39
+ depth: int = 0
40
+
41
+ # State
42
+ is_collapsed: bool = True # Task 5.7.3
43
+ is_repeated: bool = False
44
+ repeat_count: int = 1 # Task 5.7.5: xN notation
45
+
46
+ # Aggregated stats (Task 5.7.4)
47
+ total_flops: int = 0
48
+ total_params: int = 0
49
+ total_memory_bytes: int = 0
50
+ node_count: int = 1
51
+
52
+ # Inputs/outputs for edges
53
+ inputs: list[str] = field(default_factory=list)
54
+ outputs: list[str] = field(default_factory=list)
55
+
56
+ # Metadata
57
+ attributes: dict = field(default_factory=dict)
58
+
59
+ def is_leaf(self) -> bool:
60
+ """Check if this is a leaf node (no children)."""
61
+ return len(self.children) == 0
62
+
63
+ def get_display_name(self) -> str:
64
+ """Get display name with repeat notation."""
65
+ if self.repeat_count > 1:
66
+ return f"{self.name} x{self.repeat_count}"
67
+ return self.name
68
+
69
+ def collapse(self) -> None:
70
+ """Collapse this node."""
71
+ self.is_collapsed = True
72
+
73
+ def expand(self) -> None:
74
+ """Expand this node."""
75
+ self.is_collapsed = False
76
+
77
+ def toggle(self) -> None:
78
+ """Toggle collapsed state."""
79
+ self.is_collapsed = not self.is_collapsed
80
+
81
+ def aggregate_stats(self) -> None:
82
+ """
83
+ Aggregate statistics from children.
84
+
85
+ Task 5.7.4: Aggregate stats for collapsed blocks.
86
+ """
87
+ if self.is_leaf():
88
+ return
89
+
90
+ self.total_flops = 0
91
+ self.total_params = 0
92
+ self.total_memory_bytes = 0
93
+ self.node_count = 0
94
+
95
+ for child in self.children:
96
+ child.aggregate_stats()
97
+ self.total_flops += child.total_flops
98
+ self.total_params += child.total_params
99
+ self.total_memory_bytes += child.total_memory_bytes
100
+ self.node_count += child.node_count
101
+
102
+ # Apply repeat multiplier
103
+ if self.repeat_count > 1:
104
+ self.total_flops *= self.repeat_count
105
+ self.total_memory_bytes *= self.repeat_count
106
+ self.node_count *= self.repeat_count
107
+ # Params are shared, don't multiply
108
+
109
+ def to_dict(self, include_children: bool = True) -> dict[str, Any]:
110
+ """
111
+ Convert to dictionary for JSON export.
112
+
113
+ Task 5.7.6: JSON export for visualization.
114
+ """
115
+ result: dict[str, Any] = {
116
+ "id": self.id,
117
+ "name": self.name,
118
+ "display_name": self.get_display_name(),
119
+ "node_type": self.node_type,
120
+ "op_type": self.op_type,
121
+ "depth": self.depth,
122
+ "is_collapsed": self.is_collapsed,
123
+ "is_repeated": self.is_repeated,
124
+ "repeat_count": self.repeat_count,
125
+ "total_flops": self.total_flops,
126
+ "total_params": self.total_params,
127
+ "total_memory_bytes": self.total_memory_bytes,
128
+ "node_count": self.node_count,
129
+ "inputs": self.inputs,
130
+ "outputs": self.outputs,
131
+ "attributes": self.attributes,
132
+ }
133
+
134
+ if include_children and self.children:
135
+ result["children"] = [c.to_dict(include_children) for c in self.children]
136
+
137
+ return result
138
+
139
+
140
+ @dataclass
141
+ class HierarchicalGraph:
142
+ """Complete hierarchical graph representation."""
143
+
144
+ root: HierarchicalNode
145
+ nodes_by_id: dict[str, HierarchicalNode] = field(default_factory=dict)
146
+ total_nodes: int = 0
147
+ depth: int = 0
148
+
149
+ def get_node(self, node_id: str) -> HierarchicalNode | None:
150
+ """Get node by ID."""
151
+ return self.nodes_by_id.get(node_id)
152
+
153
+ def collapse_all(self) -> None:
154
+ """Collapse all non-leaf nodes."""
155
+ for node in self.nodes_by_id.values():
156
+ if not node.is_leaf():
157
+ node.collapse()
158
+
159
+ def expand_all(self) -> None:
160
+ """Expand all nodes."""
161
+ for node in self.nodes_by_id.values():
162
+ node.expand()
163
+
164
+ def expand_to_depth(self, max_depth: int) -> None:
165
+ """Expand nodes up to a certain depth."""
166
+ for node in self.nodes_by_id.values():
167
+ if node.depth <= max_depth:
168
+ node.expand()
169
+ else:
170
+ node.collapse()
171
+
172
+ def get_visible_nodes(self) -> list[HierarchicalNode]:
173
+ """Get nodes visible given current collapse state."""
174
+ visible: list[HierarchicalNode] = []
175
+ self._collect_visible(self.root, visible)
176
+ return visible
177
+
178
+ def _collect_visible(self, node: HierarchicalNode, visible: list[HierarchicalNode]) -> None:
179
+ """Recursively collect visible nodes."""
180
+ visible.append(node)
181
+ if not node.is_collapsed:
182
+ for child in node.children:
183
+ self._collect_visible(child, visible)
184
+
185
+ def to_dict(self) -> dict:
186
+ """Export to dictionary."""
187
+ return {
188
+ "root": self.root.to_dict(),
189
+ "total_nodes": self.total_nodes,
190
+ "depth": self.depth,
191
+ }
192
+
193
+ def to_json(self, indent: int | None = 2) -> str:
194
+ """Export to JSON string."""
195
+ return json.dumps(self.to_dict(), indent=indent)
196
+
197
+
198
+ class HierarchicalGraphBuilder:
199
+ """
200
+ Build hierarchical graph from flat ONNX graph and detected blocks.
201
+
202
+ Task 5.7.2: Convert Blocks to HierarchicalNodes.
203
+ """
204
+
205
+ def __init__(self, logger: logging.Logger | None = None):
206
+ self.logger = logger or logging.getLogger("haoline.hierarchy")
207
+
208
+ def build(
209
+ self,
210
+ graph_info: GraphInfo,
211
+ blocks: list[Block],
212
+ model_name: str = "Model",
213
+ ) -> HierarchicalGraph:
214
+ """
215
+ Build hierarchical graph from ONNX graph and detected blocks.
216
+
217
+ Args:
218
+ graph_info: Parsed ONNX graph.
219
+ blocks: Detected architectural blocks.
220
+ model_name: Name for the root node.
221
+
222
+ Returns:
223
+ HierarchicalGraph with multi-level structure.
224
+ """
225
+ nodes_by_id: dict[str, HierarchicalNode] = {}
226
+
227
+ # Create root node
228
+ root = HierarchicalNode(
229
+ id="root",
230
+ name=model_name,
231
+ node_type="model",
232
+ depth=0,
233
+ is_collapsed=False,
234
+ )
235
+ nodes_by_id["root"] = root
236
+
237
+ # Map op nodes to block memberships
238
+ node_to_block: dict[str, str] = {}
239
+ for block in blocks:
240
+ for node_name in block.nodes:
241
+ if node_name not in node_to_block:
242
+ node_to_block[node_name] = block.name
243
+
244
+ # Create block nodes
245
+ block_nodes: dict[str, HierarchicalNode] = {}
246
+ for block in blocks:
247
+ block_node = HierarchicalNode(
248
+ id=f"block_{block.name}",
249
+ name=block.name,
250
+ node_type="block",
251
+ depth=1,
252
+ parent_id="root",
253
+ attributes=block.attributes.copy() if block.attributes else {},
254
+ )
255
+ block_node.attributes["block_type"] = block.block_type
256
+ block_nodes[block.name] = block_node
257
+ nodes_by_id[block_node.id] = block_node
258
+
259
+ # Process repeated blocks (Task 5.7.5)
260
+ repeated_blocks = [b for b in blocks if b.block_type == "RepeatedBlock"]
261
+ for rb in repeated_blocks:
262
+ rb.attributes.get("repeated_type", "")
263
+ repeat_count = rb.attributes.get("num_repetitions", 1)
264
+ block_names = rb.attributes.get("block_names", [])
265
+
266
+ # Mark the first block as repeated, hide others
267
+ if block_names and repeat_count > 1:
268
+ first_block_name = block_names[0]
269
+ if first_block_name in block_nodes:
270
+ block_nodes[first_block_name].is_repeated = True
271
+ block_nodes[first_block_name].repeat_count = repeat_count
272
+
273
+ # Remove subsequent blocks from display (they're represented by xN)
274
+ for name in block_names[1:]:
275
+ if name in block_nodes:
276
+ # Mark as hidden by adding to a "collapsed repeated" group
277
+ block_nodes[name].attributes["hidden_by_repeat"] = True
278
+
279
+ # Create op nodes and assign to blocks or root
280
+ for node in graph_info.nodes:
281
+ op_node = HierarchicalNode(
282
+ id=f"op_{node.name}",
283
+ name=node.name,
284
+ node_type="op",
285
+ op_type=node.op_type,
286
+ inputs=list(node.inputs),
287
+ outputs=list(node.outputs),
288
+ )
289
+
290
+ # Get stats from graph_info if available
291
+ if hasattr(graph_info, "node_flops"):
292
+ op_node.total_flops = graph_info.node_flops.get(node.name, 0)
293
+
294
+ if node.name in node_to_block:
295
+ # Node belongs to a block
296
+ block_name = node_to_block[node.name]
297
+ if block_name in block_nodes:
298
+ parent = block_nodes[block_name]
299
+ op_node.parent_id = parent.id
300
+ op_node.depth = parent.depth + 1
301
+ parent.children.append(op_node)
302
+ else:
303
+ # Standalone node at root level
304
+ op_node.parent_id = "root"
305
+ op_node.depth = 1
306
+ root.children.append(op_node)
307
+
308
+ nodes_by_id[op_node.id] = op_node
309
+
310
+ # Add non-hidden blocks to root
311
+ for block_node in block_nodes.values():
312
+ if not block_node.attributes.get("hidden_by_repeat", False):
313
+ root.children.append(block_node)
314
+
315
+ # Aggregate stats up the tree
316
+ root.aggregate_stats()
317
+
318
+ # Calculate depth
319
+ max_depth = max(n.depth for n in nodes_by_id.values())
320
+
321
+ return HierarchicalGraph(
322
+ root=root,
323
+ nodes_by_id=nodes_by_id,
324
+ total_nodes=len(nodes_by_id),
325
+ depth=max_depth,
326
+ )
327
+
328
+ def build_layer_hierarchy(
329
+ self,
330
+ graph_info: GraphInfo,
331
+ blocks: list[Block],
332
+ model_name: str = "Model",
333
+ ) -> HierarchicalGraph:
334
+ """
335
+ Build a 3-level hierarchy: Model -> Layers -> Blocks -> Ops.
336
+
337
+ For LLMs, groups attention+MLP into "TransformerLayer" containers.
338
+ """
339
+ basic_graph = self.build(graph_info, blocks, model_name)
340
+
341
+ # Group consecutive attention + MLP blocks into layers
342
+ layers: list[HierarchicalNode] = []
343
+ current_layer_blocks: list[HierarchicalNode] = []
344
+ layer_idx = 0
345
+
346
+ for child in basic_graph.root.children:
347
+ if child.node_type == "block":
348
+ block_type = child.attributes.get("block_type", "")
349
+
350
+ # Check if this starts a new layer
351
+ if block_type in ("AttentionHead", "Attention") and current_layer_blocks:
352
+ # Previous blocks become a layer
353
+ if len(current_layer_blocks) > 1:
354
+ layer = self._create_layer_node(current_layer_blocks, layer_idx)
355
+ layers.append(layer)
356
+ layer_idx += 1
357
+ else:
358
+ layers.extend(current_layer_blocks)
359
+ current_layer_blocks = []
360
+
361
+ current_layer_blocks.append(child)
362
+ else:
363
+ # Non-block node, flush any pending layer
364
+ if current_layer_blocks:
365
+ if len(current_layer_blocks) > 1:
366
+ layer = self._create_layer_node(current_layer_blocks, layer_idx)
367
+ layers.append(layer)
368
+ layer_idx += 1
369
+ else:
370
+ layers.extend(current_layer_blocks)
371
+ current_layer_blocks = []
372
+ layers.append(child)
373
+
374
+ # Handle remaining blocks
375
+ if current_layer_blocks:
376
+ if len(current_layer_blocks) > 1:
377
+ layer = self._create_layer_node(current_layer_blocks, layer_idx)
378
+ layers.append(layer)
379
+ else:
380
+ layers.extend(current_layer_blocks)
381
+
382
+ # Update root children
383
+ basic_graph.root.children = layers
384
+
385
+ # Update nodes_by_id
386
+ for layer in layers:
387
+ if layer.node_type == "layer":
388
+ basic_graph.nodes_by_id[layer.id] = layer
389
+
390
+ # Recalculate depths
391
+ self._recalculate_depths(basic_graph.root, 0)
392
+ basic_graph.depth = max(n.depth for n in basic_graph.nodes_by_id.values())
393
+
394
+ # Re-aggregate stats
395
+ basic_graph.root.aggregate_stats()
396
+
397
+ return basic_graph
398
+
399
+ def _create_layer_node(
400
+ self, blocks: list[HierarchicalNode], layer_idx: int
401
+ ) -> HierarchicalNode:
402
+ """Create a layer node containing multiple blocks."""
403
+ layer = HierarchicalNode(
404
+ id=f"layer_{layer_idx}",
405
+ name=f"Layer {layer_idx}",
406
+ node_type="layer",
407
+ depth=1,
408
+ parent_id="root",
409
+ children=blocks,
410
+ )
411
+
412
+ # Update children's parent
413
+ for block in blocks:
414
+ block.parent_id = layer.id
415
+ block.depth = 2
416
+ # Update children of blocks
417
+ for child in block.children:
418
+ child.depth = 3
419
+
420
+ return layer
421
+
422
+ def _recalculate_depths(self, node: HierarchicalNode, depth: int) -> None:
423
+ """Recursively recalculate depths."""
424
+ node.depth = depth
425
+ for child in node.children:
426
+ self._recalculate_depths(child, depth + 1)
427
+
428
+
429
+ def generate_summary(graph: HierarchicalGraph) -> str:
430
+ """
431
+ Generate multi-level text summary.
432
+
433
+ Task 5.7.5: Generate multi-level summary (xN notation).
434
+ """
435
+ lines = []
436
+ lines.append(f"# {graph.root.name}")
437
+ lines.append(f"Total Nodes: {graph.total_nodes}")
438
+ lines.append(f"Max Depth: {graph.depth}")
439
+ lines.append("")
440
+
441
+ def format_node(node: HierarchicalNode, indent: int = 0) -> None:
442
+ prefix = " " * indent
443
+ display = node.get_display_name()
444
+
445
+ if node.node_type == "op":
446
+ lines.append(f"{prefix}- {display} ({node.op_type})")
447
+ elif node.node_type == "block":
448
+ block_type = node.attributes.get("block_type", "Block")
449
+ lines.append(f"{prefix}[{block_type}] {display}")
450
+ elif node.node_type == "layer":
451
+ lines.append(f"{prefix}== {display} ==")
452
+ else:
453
+ lines.append(f"{prefix}{display}")
454
+
455
+ if not node.is_collapsed:
456
+ for child in node.children:
457
+ format_node(child, indent + 1)
458
+
459
+ for child in graph.root.children:
460
+ format_node(child, 0)
461
+
462
+ return "\n".join(lines)