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.
- haoline/.streamlit/config.toml +10 -0
- haoline/__init__.py +248 -0
- haoline/analyzer.py +935 -0
- haoline/cli.py +2712 -0
- haoline/compare.py +811 -0
- haoline/compare_visualizations.py +1564 -0
- haoline/edge_analysis.py +525 -0
- haoline/eval/__init__.py +131 -0
- haoline/eval/adapters.py +844 -0
- haoline/eval/cli.py +390 -0
- haoline/eval/comparison.py +542 -0
- haoline/eval/deployment.py +633 -0
- haoline/eval/schemas.py +833 -0
- haoline/examples/__init__.py +15 -0
- haoline/examples/basic_inspection.py +74 -0
- haoline/examples/compare_models.py +117 -0
- haoline/examples/hardware_estimation.py +78 -0
- haoline/format_adapters.py +1001 -0
- haoline/formats/__init__.py +123 -0
- haoline/formats/coreml.py +250 -0
- haoline/formats/gguf.py +483 -0
- haoline/formats/openvino.py +255 -0
- haoline/formats/safetensors.py +273 -0
- haoline/formats/tflite.py +369 -0
- haoline/hardware.py +2307 -0
- haoline/hierarchical_graph.py +462 -0
- haoline/html_export.py +1573 -0
- haoline/layer_summary.py +769 -0
- haoline/llm_summarizer.py +465 -0
- haoline/op_icons.py +618 -0
- haoline/operational_profiling.py +1492 -0
- haoline/patterns.py +1116 -0
- haoline/pdf_generator.py +265 -0
- haoline/privacy.py +250 -0
- haoline/pydantic_models.py +241 -0
- haoline/report.py +1923 -0
- haoline/report_sections.py +539 -0
- haoline/risks.py +521 -0
- haoline/schema.py +523 -0
- haoline/streamlit_app.py +2024 -0
- haoline/tests/__init__.py +4 -0
- haoline/tests/conftest.py +123 -0
- haoline/tests/test_analyzer.py +868 -0
- haoline/tests/test_compare_visualizations.py +293 -0
- haoline/tests/test_edge_analysis.py +243 -0
- haoline/tests/test_eval.py +604 -0
- haoline/tests/test_format_adapters.py +460 -0
- haoline/tests/test_hardware.py +237 -0
- haoline/tests/test_hardware_recommender.py +90 -0
- haoline/tests/test_hierarchical_graph.py +326 -0
- haoline/tests/test_html_export.py +180 -0
- haoline/tests/test_layer_summary.py +428 -0
- haoline/tests/test_llm_patterns.py +540 -0
- haoline/tests/test_llm_summarizer.py +339 -0
- haoline/tests/test_patterns.py +774 -0
- haoline/tests/test_pytorch.py +327 -0
- haoline/tests/test_report.py +383 -0
- haoline/tests/test_risks.py +398 -0
- haoline/tests/test_schema.py +417 -0
- haoline/tests/test_tensorflow.py +380 -0
- haoline/tests/test_visualizations.py +316 -0
- haoline/universal_ir.py +856 -0
- haoline/visualizations.py +1086 -0
- haoline/visualize_yolo.py +44 -0
- haoline/web.py +110 -0
- haoline-0.3.0.dist-info/METADATA +471 -0
- haoline-0.3.0.dist-info/RECORD +70 -0
- haoline-0.3.0.dist-info/WHEEL +4 -0
- haoline-0.3.0.dist-info/entry_points.txt +5 -0
- 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
|
+
)
|