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,539 @@
1
+ # Copyright (c) 2025 HaoLine Contributors
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Reusable report section generators for HaoLine.
6
+
7
+ This module provides format-agnostic functions that generate report sections
8
+ as structured data, which can then be rendered to HTML, Markdown, or Streamlit.
9
+
10
+ Story 41.2: Extract report sections into reusable functions for CLI-Streamlit parity.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from .operational_profiling import BottleneckAnalysis
20
+ from .report import InspectionReport
21
+
22
+
23
+ def format_number(n: int | float) -> str:
24
+ """Format a number with SI suffixes (K, M, B, T)."""
25
+ if n >= 1e12:
26
+ return f"{n / 1e12:.1f}T"
27
+ if n >= 1e9:
28
+ return f"{n / 1e9:.1f}B"
29
+ if n >= 1e6:
30
+ return f"{n / 1e6:.1f}M"
31
+ if n >= 1e3:
32
+ return f"{n / 1e3:.1f}K"
33
+ return str(int(n))
34
+
35
+
36
+ def format_bytes(b: int | float) -> str:
37
+ """Format bytes with appropriate units."""
38
+ if b >= 1024**4:
39
+ return f"{b / 1024**4:.2f} TB"
40
+ if b >= 1024**3:
41
+ return f"{b / 1024**3:.2f} GB"
42
+ if b >= 1024**2:
43
+ return f"{b / 1024**2:.2f} MB"
44
+ if b >= 1024:
45
+ return f"{b / 1024:.2f} KB"
46
+ return f"{int(b)} bytes"
47
+
48
+
49
+ @dataclass
50
+ class MetricsCard:
51
+ """A single metrics card for display."""
52
+
53
+ label: str
54
+ value: str
55
+ raw_value: int | float | None = None
56
+ color: str | None = None # Optional accent color
57
+
58
+
59
+ @dataclass
60
+ class MetricsSummary:
61
+ """Summary metrics for a model."""
62
+
63
+ cards: list[MetricsCard] = field(default_factory=list)
64
+
65
+ @classmethod
66
+ def from_report(cls, report: InspectionReport) -> MetricsSummary:
67
+ """Build metrics summary from an InspectionReport."""
68
+ cards = []
69
+
70
+ # Parameters
71
+ if report.param_counts:
72
+ cards.append(
73
+ MetricsCard(
74
+ label="Parameters",
75
+ value=format_number(report.param_counts.total),
76
+ raw_value=report.param_counts.total,
77
+ )
78
+ )
79
+
80
+ # FLOPs
81
+ if report.flop_counts:
82
+ cards.append(
83
+ MetricsCard(
84
+ label="FLOPs",
85
+ value=format_number(report.flop_counts.total),
86
+ raw_value=report.flop_counts.total,
87
+ )
88
+ )
89
+
90
+ # Model Size
91
+ if report.memory_estimates:
92
+ cards.append(
93
+ MetricsCard(
94
+ label="Model Size",
95
+ value=format_bytes(report.memory_estimates.model_size_bytes),
96
+ raw_value=report.memory_estimates.model_size_bytes,
97
+ )
98
+ )
99
+
100
+ # Peak Memory
101
+ if report.memory_estimates:
102
+ cards.append(
103
+ MetricsCard(
104
+ label="Peak Memory",
105
+ value=format_bytes(report.memory_estimates.peak_activation_bytes),
106
+ raw_value=report.memory_estimates.peak_activation_bytes,
107
+ )
108
+ )
109
+
110
+ # Operators
111
+ if report.graph_summary:
112
+ cards.append(
113
+ MetricsCard(
114
+ label="Operators",
115
+ value=str(report.graph_summary.num_nodes),
116
+ raw_value=report.graph_summary.num_nodes,
117
+ )
118
+ )
119
+
120
+ # Architecture Type
121
+ if report.architecture_type and report.architecture_type != "unknown":
122
+ cards.append(
123
+ MetricsCard(
124
+ label="Architecture",
125
+ value=report.architecture_type.upper(),
126
+ )
127
+ )
128
+
129
+ # Quantization indicator
130
+ if report.param_counts and report.param_counts.is_quantized:
131
+ cards.append(
132
+ MetricsCard(
133
+ label="Quantized",
134
+ value="Yes",
135
+ color="#4CAF50", # Green
136
+ )
137
+ )
138
+
139
+ return cls(cards=cards)
140
+
141
+
142
+ @dataclass
143
+ class KVCacheSection:
144
+ """KV Cache analysis for transformer models."""
145
+
146
+ bytes_per_token: int
147
+ bytes_full_context: int
148
+ num_layers: int | None = None
149
+ hidden_dim: int | None = None
150
+ seq_len: int | None = None
151
+
152
+ @classmethod
153
+ def from_report(cls, report: InspectionReport) -> KVCacheSection | None:
154
+ """Extract KV cache info from report, or None if not a transformer."""
155
+ if not report.memory_estimates:
156
+ return None
157
+ if report.memory_estimates.kv_cache_bytes_per_token <= 0:
158
+ return None
159
+
160
+ config = report.memory_estimates.kv_cache_config or {}
161
+ return cls(
162
+ bytes_per_token=report.memory_estimates.kv_cache_bytes_per_token,
163
+ bytes_full_context=report.memory_estimates.kv_cache_bytes_full_context,
164
+ num_layers=config.get("num_layers"),
165
+ hidden_dim=config.get("hidden_dim"),
166
+ seq_len=config.get("seq_len"),
167
+ )
168
+
169
+ def to_dict(self) -> dict[str, Any]:
170
+ """Convert to dictionary for serialization."""
171
+ return {
172
+ "bytes_per_token": self.bytes_per_token,
173
+ "bytes_per_token_formatted": format_bytes(self.bytes_per_token),
174
+ "bytes_full_context": self.bytes_full_context,
175
+ "bytes_full_context_formatted": format_bytes(self.bytes_full_context),
176
+ "num_layers": self.num_layers,
177
+ "hidden_dim": self.hidden_dim,
178
+ "seq_len": self.seq_len,
179
+ }
180
+
181
+
182
+ @dataclass
183
+ class PrecisionBreakdownRow:
184
+ """A row in the precision breakdown table."""
185
+
186
+ dtype: str
187
+ count: int
188
+ percentage: float
189
+
190
+
191
+ @dataclass
192
+ class PrecisionBreakdown:
193
+ """Precision breakdown for model parameters."""
194
+
195
+ rows: list[PrecisionBreakdownRow] = field(default_factory=list)
196
+ is_quantized: bool = False
197
+ quantized_ops: list[str] = field(default_factory=list)
198
+
199
+ @classmethod
200
+ def from_report(cls, report: InspectionReport) -> PrecisionBreakdown | None:
201
+ """Extract precision breakdown from report."""
202
+ if not report.param_counts:
203
+ return None
204
+ if not report.param_counts.precision_breakdown:
205
+ return None
206
+
207
+ total = sum(report.param_counts.precision_breakdown.values())
208
+ if total == 0:
209
+ return None
210
+
211
+ rows = []
212
+ for dtype, count in sorted(
213
+ report.param_counts.precision_breakdown.items(),
214
+ key=lambda x: -x[1],
215
+ ):
216
+ rows.append(
217
+ PrecisionBreakdownRow(
218
+ dtype=dtype,
219
+ count=count,
220
+ percentage=100.0 * count / total,
221
+ )
222
+ )
223
+
224
+ return cls(
225
+ rows=rows,
226
+ is_quantized=report.param_counts.is_quantized,
227
+ quantized_ops=report.param_counts.quantized_ops or [],
228
+ )
229
+
230
+
231
+ @dataclass
232
+ class MemoryBreakdownRow:
233
+ """A row in the memory breakdown table."""
234
+
235
+ component: str
236
+ size_bytes: int
237
+ percentage: float | None = None
238
+
239
+
240
+ @dataclass
241
+ class MemoryBreakdownSection:
242
+ """Memory breakdown by op type."""
243
+
244
+ weights_by_op: list[MemoryBreakdownRow] = field(default_factory=list)
245
+ activations_by_op: list[MemoryBreakdownRow] = field(default_factory=list)
246
+ total_weights: int = 0
247
+ total_activations: int = 0
248
+
249
+ @classmethod
250
+ def from_report(cls, report: InspectionReport) -> MemoryBreakdownSection | None:
251
+ """Extract memory breakdown from report."""
252
+ if not report.memory_estimates:
253
+ return None
254
+ if not report.memory_estimates.breakdown:
255
+ return None
256
+
257
+ bd = report.memory_estimates.breakdown
258
+ weights_rows = []
259
+ activations_rows = []
260
+
261
+ # Weights by op type
262
+ if bd.weights_by_op_type:
263
+ total_w = sum(bd.weights_by_op_type.values())
264
+ for op_type, size in sorted(bd.weights_by_op_type.items(), key=lambda x: -x[1])[:10]:
265
+ weights_rows.append(
266
+ MemoryBreakdownRow(
267
+ component=op_type,
268
+ size_bytes=size,
269
+ percentage=100.0 * size / total_w if total_w > 0 else 0,
270
+ )
271
+ )
272
+
273
+ # Activations by op type
274
+ if bd.activations_by_op_type:
275
+ total_a = sum(bd.activations_by_op_type.values())
276
+ for op_type, size in sorted(bd.activations_by_op_type.items(), key=lambda x: -x[1])[
277
+ :10
278
+ ]:
279
+ activations_rows.append(
280
+ MemoryBreakdownRow(
281
+ component=op_type,
282
+ size_bytes=size,
283
+ percentage=100.0 * size / total_a if total_a > 0 else 0,
284
+ )
285
+ )
286
+
287
+ if not weights_rows and not activations_rows:
288
+ return None
289
+
290
+ return cls(
291
+ weights_by_op=weights_rows,
292
+ activations_by_op=activations_rows,
293
+ total_weights=sum(bd.weights_by_op_type.values()) if bd.weights_by_op_type else 0,
294
+ total_activations=sum(bd.activations_by_op_type.values())
295
+ if bd.activations_by_op_type
296
+ else 0,
297
+ )
298
+
299
+
300
+ @dataclass
301
+ class HardwareEstimatesSection:
302
+ """Hardware performance estimates."""
303
+
304
+ device: str
305
+ precision: str
306
+ batch_size: int
307
+ vram_required_bytes: int
308
+ fits_in_vram: bool
309
+ theoretical_latency_ms: float
310
+ compute_utilization: float
311
+ gpu_saturation: float
312
+ bottleneck: str
313
+
314
+ @classmethod
315
+ def from_report(cls, report: InspectionReport) -> HardwareEstimatesSection | None:
316
+ """Extract hardware estimates from report."""
317
+ if not report.hardware_estimates:
318
+ return None
319
+
320
+ hw = report.hardware_estimates
321
+ return cls(
322
+ device=hw.device,
323
+ precision=hw.precision,
324
+ batch_size=hw.batch_size,
325
+ vram_required_bytes=hw.vram_required_bytes,
326
+ fits_in_vram=hw.fits_in_vram,
327
+ theoretical_latency_ms=hw.theoretical_latency_ms,
328
+ compute_utilization=hw.compute_utilization_estimate,
329
+ gpu_saturation=hw.gpu_saturation,
330
+ bottleneck=hw.bottleneck,
331
+ )
332
+
333
+ def to_dict(self) -> dict[str, Any]:
334
+ """Convert to dictionary for display."""
335
+ return {
336
+ "device": self.device,
337
+ "precision": self.precision,
338
+ "batch_size": self.batch_size,
339
+ "vram_required": format_bytes(self.vram_required_bytes),
340
+ "vram_required_bytes": self.vram_required_bytes,
341
+ "fits_in_vram": self.fits_in_vram,
342
+ "theoretical_latency_ms": round(self.theoretical_latency_ms, 2),
343
+ "compute_utilization": round(self.compute_utilization * 100, 1),
344
+ "gpu_saturation": round(self.gpu_saturation * 100, 4),
345
+ "bottleneck": self.bottleneck,
346
+ }
347
+
348
+
349
+ @dataclass
350
+ class BottleneckSection:
351
+ """Bottleneck analysis results."""
352
+
353
+ classification: str # "compute_bound", "memory_bound", "balanced"
354
+ compute_time_ms: float
355
+ memory_time_ms: float
356
+ ratio: float
357
+ recommendations: list[str] = field(default_factory=list)
358
+
359
+ @classmethod
360
+ def from_bottleneck_analysis(cls, analysis: BottleneckAnalysis) -> BottleneckSection:
361
+ """Create from BottleneckAnalysis object."""
362
+ return cls(
363
+ classification=analysis.classification,
364
+ compute_time_ms=analysis.compute_time_ms,
365
+ memory_time_ms=analysis.memory_time_ms,
366
+ ratio=analysis.ratio,
367
+ recommendations=analysis.recommendations,
368
+ )
369
+
370
+
371
+ @dataclass
372
+ class OperatorDistribution:
373
+ """Operator type distribution."""
374
+
375
+ op_counts: dict[str, int]
376
+ total_ops: int
377
+
378
+ @classmethod
379
+ def from_report(cls, report: InspectionReport) -> OperatorDistribution | None:
380
+ """Extract operator distribution from report."""
381
+ if not report.graph_summary:
382
+ return None
383
+ if not report.graph_summary.op_type_counts:
384
+ return None
385
+
386
+ return cls(
387
+ op_counts=report.graph_summary.op_type_counts,
388
+ total_ops=report.graph_summary.num_nodes,
389
+ )
390
+
391
+ def top_n(self, n: int = 10) -> list[tuple[str, int, float]]:
392
+ """Get top N operators with counts and percentages."""
393
+ sorted_ops = sorted(self.op_counts.items(), key=lambda x: -x[1])
394
+ return [
395
+ (op, count, 100.0 * count / self.total_ops if self.total_ops > 0 else 0)
396
+ for op, count in sorted_ops[:n]
397
+ ]
398
+
399
+
400
+ @dataclass
401
+ class RiskSignalItem:
402
+ """A single risk signal for display."""
403
+
404
+ id: str
405
+ severity: str # "high", "medium", "low"
406
+ description: str
407
+ nodes: list[str] = field(default_factory=list)
408
+
409
+
410
+ @dataclass
411
+ class RiskSignalsSection:
412
+ """Risk signals summary."""
413
+
414
+ signals: list[RiskSignalItem] = field(default_factory=list)
415
+ high_count: int = 0
416
+ medium_count: int = 0
417
+ low_count: int = 0
418
+
419
+ @classmethod
420
+ def from_report(cls, report: InspectionReport) -> RiskSignalsSection:
421
+ """Extract risk signals from report."""
422
+ signals = []
423
+ high = medium = low = 0
424
+
425
+ for risk in report.risk_signals or []:
426
+ signals.append(
427
+ RiskSignalItem(
428
+ id=risk.id,
429
+ severity=risk.severity,
430
+ description=risk.description,
431
+ nodes=risk.nodes or [],
432
+ )
433
+ )
434
+ if risk.severity == "high":
435
+ high += 1
436
+ elif risk.severity == "medium":
437
+ medium += 1
438
+ else:
439
+ low += 1
440
+
441
+ return cls(
442
+ signals=signals,
443
+ high_count=high,
444
+ medium_count=medium,
445
+ low_count=low,
446
+ )
447
+
448
+
449
+ @dataclass
450
+ class BlockSummaryItem:
451
+ """A single detected block for display."""
452
+
453
+ block_type: str
454
+ name: str
455
+ node_count: int
456
+ nodes: list[str] = field(default_factory=list)
457
+
458
+
459
+ @dataclass
460
+ class DetectedBlocksSection:
461
+ """Detected architecture blocks summary."""
462
+
463
+ blocks: list[BlockSummaryItem] = field(default_factory=list)
464
+ block_type_counts: dict[str, int] = field(default_factory=dict)
465
+
466
+ @classmethod
467
+ def from_report(cls, report: InspectionReport) -> DetectedBlocksSection:
468
+ """Extract detected blocks from report."""
469
+ blocks = []
470
+ type_counts: dict[str, int] = {}
471
+
472
+ for block in report.detected_blocks or []:
473
+ blocks.append(
474
+ BlockSummaryItem(
475
+ block_type=block.block_type,
476
+ name=block.name,
477
+ node_count=len(block.nodes) if block.nodes else 0,
478
+ nodes=block.nodes or [],
479
+ )
480
+ )
481
+ type_counts[block.block_type] = type_counts.get(block.block_type, 0) + 1
482
+
483
+ return cls(blocks=blocks, block_type_counts=type_counts)
484
+
485
+
486
+ @dataclass
487
+ class SharedWeightsSection:
488
+ """Shared weights information."""
489
+
490
+ num_shared: int
491
+ shared_weights: dict[str, list[str]] # weight_name -> list of nodes using it
492
+
493
+ @classmethod
494
+ def from_report(cls, report: InspectionReport) -> SharedWeightsSection | None:
495
+ """Extract shared weights info from report."""
496
+ if not report.param_counts:
497
+ return None
498
+ if report.param_counts.num_shared_weights <= 0:
499
+ return None
500
+
501
+ return cls(
502
+ num_shared=report.param_counts.num_shared_weights,
503
+ shared_weights=report.param_counts.shared_weights or {},
504
+ )
505
+
506
+
507
+ # =============================================================================
508
+ # Full Report Extraction
509
+ # =============================================================================
510
+
511
+
512
+ @dataclass
513
+ class ExtractedReportSections:
514
+ """All extracted sections from an InspectionReport."""
515
+
516
+ metrics: MetricsSummary
517
+ kv_cache: KVCacheSection | None
518
+ precision: PrecisionBreakdown | None
519
+ memory_breakdown: MemoryBreakdownSection | None
520
+ hardware: HardwareEstimatesSection | None
521
+ operators: OperatorDistribution | None
522
+ risks: RiskSignalsSection
523
+ blocks: DetectedBlocksSection
524
+ shared_weights: SharedWeightsSection | None
525
+
526
+ @classmethod
527
+ def from_report(cls, report: InspectionReport) -> ExtractedReportSections:
528
+ """Extract all sections from an InspectionReport."""
529
+ return cls(
530
+ metrics=MetricsSummary.from_report(report),
531
+ kv_cache=KVCacheSection.from_report(report),
532
+ precision=PrecisionBreakdown.from_report(report),
533
+ memory_breakdown=MemoryBreakdownSection.from_report(report),
534
+ hardware=HardwareEstimatesSection.from_report(report),
535
+ operators=OperatorDistribution.from_report(report),
536
+ risks=RiskSignalsSection.from_report(report),
537
+ blocks=DetectedBlocksSection.from_report(report),
538
+ shared_weights=SharedWeightsSection.from_report(report),
539
+ )