From 575d9672fb5c09b7c04bcaf7350e3031dc15e0be Mon Sep 17 00:00:00 2001 From: Reiase Date: Sun, 9 Aug 2026 23:00:59 +0800 Subject: [PATCH 1/6] Add performance evidence lab PoC --- README.md | 7 +- docs/design/performance/providers.en.md | 8 + docs/design/performance/providers.zh.md | 8 + pyproject.toml | 1 + scripts/check_wheel_contract.py | 21 +- src/blueprinting/analysis/__init__.py | 14 + src/blueprinting/analysis/cost/__init__.py | 16 + src/blueprinting/analysis/cost/explorer.py | 286 +++++++++++++++ src/blueprinting/workbench/evidence_lab.py | 369 ++++++++++++++++++++ src/blueprinting/workbench/nicegui_theme.py | 19 +- src/blueprinting/workbench/nicegui_ui.py | 34 +- tests/analysis/test_evidence_explorer.py | 88 +++++ tests/workbench/test_evidence_lab.py | 29 ++ tests/workbench/test_nicegui_workbench.py | 25 ++ 14 files changed, 912 insertions(+), 13 deletions(-) create mode 100644 src/blueprinting/analysis/cost/explorer.py create mode 100644 src/blueprinting/workbench/evidence_lab.py create mode 100644 tests/analysis/test_evidence_explorer.py create mode 100644 tests/workbench/test_evidence_lab.py diff --git a/README.md b/README.md index bbe94c9..1de694c 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,10 @@ uv run blueprinting-workbench blueprinting --help ``` -It provides a shared configuration surface for single-point analysis, canonical IR derivation audit, and bounded -TP/PP/DP strategy exploration. Analysis runs outside the UI event loop, and failed candidates remain visible as -structured diagnostics. +It provides a shared configuration surface for single-point analysis, canonical IR derivation audit, bounded +TP/PP/DP strategy exploration, and a read-only performance-evidence lab. The evidence PoC catalogs the pinned +Vidur Phi-2/A100 records and compares exact GEMM samples with the analytical roofline on identical workload facts. +Analysis runs outside the UI event loop, and failed candidates remain visible as structured diagnostics. The existing Calculon Streamlit tools remain isolated as an optional legacy interface. Floating-point analysis is available in the primary NiceGUI workbench: diff --git a/docs/design/performance/providers.en.md b/docs/design/performance/providers.en.md index 5159c7c..1745bb7 100644 --- a/docs/design/performance/providers.en.md +++ b/docs/design/performance/providers.en.md @@ -172,6 +172,12 @@ The application boundary also accepts `cost_resolver` and `cost_context`, so evi Inference task queries are derived from canonical `PlanTask.workload`; GEMM dimensions and local attention-head dimensions are derived from model and TP facts. Tensor-parallel collectives and pipeline P2P use the same resolver. All tasks must be covered by an installed provider—normally an exact database followed by roofline—so an unknown task never becomes zero. +## Performance-evidence UI PoC + +The NiceGUI workbench's Performance Evidence entry provides a read-only evidence catalog, operation coverage, GEMM characteristic curves, and a query/record inspector. Its first pinned dataset is the Vidur Phi-2/A100 profile, shipped in the base wheel together with its source revision, manifest, and license. + +The PoC compares only four GEMM semantics because their selectors can reconstruct `M/N/K`, FLOPs, and bytes without ambiguity. The measured series contains exact sample points and makes no interpolation claim; the analytical series is a compute/memory roofline over the same workload facts, not an event-level simulation. Attention and other operations currently appear only in the coverage catalog rather than receiving a fabricated generic analytical formula. + ## Implemented boundary and next steps Implemented now: @@ -183,6 +189,7 @@ Implemented now: - generic simulator/profiler table ingestion; - explicit Vidur and four-family AIConfigurator ingestion; - inference task and pipeline integration with regression tests. +- a read-only evidence catalog and GEMM curve-comparison PoC over the pinned Vidur profile. Still missing: @@ -193,5 +200,6 @@ Still missing: - contention, overlap, queueing, and plan-level discrete-event simulation; - energy/power metrics as normalized planner objectives; - observation ingestion and calibration revisions. +- multi-database repository/registry, user import, and explicit evidence-promotion UI. These omissions are important: task latency resolution is an evidence layer, not a completed hardware or serving simulator. diff --git a/docs/design/performance/providers.zh.md b/docs/design/performance/providers.zh.md index 57f1997..e023b54 100644 --- a/docs/design/performance/providers.zh.md +++ b/docs/design/performance/providers.zh.md @@ -172,6 +172,12 @@ Application 入口同样接受 `cost_resolver` 与 `cost_context`,因此导入 Inference task query 直接从 canonical `PlanTask.workload` 推导;GEMM dimension 与 local attention-head dimension 来自 model 和 TP facts。Tensor-parallel collective 与 pipeline P2P 使用同一个 resolver。所有 task 都必须被已安装的 provider 覆盖——通常是 exact database 后接 roofline——unknown task 不会被静默变成零。 +## 性能证据界面 PoC + +NiceGUI workbench 的“性能证据”入口提供只读 evidence catalog、operation coverage、GEMM 特性曲线和 query/record inspector。首个 pinned dataset 是 Vidur Phi-2/A100 profile;它连同 source revision、manifest 与 license 一起进入 base wheel。 + +PoC 只对四类 GEMM semantic 绘制 database-versus-roofline 对比,因为这些 selector 可以无歧义地重建 `M/N/K`、FLOPs 与 bytes。Measured series 只显示 exact sample point,不声明插值;analytical series 是相同 workload facts 下的 compute/memory roofline,不是事件级仿真。Attention 与其他 operation 当前只进入 coverage catalog,尚不伪造通用解析对比公式。 + ## 已实现边界与后续工作 当前已经实现: @@ -183,6 +189,7 @@ Inference task query 直接从 canonical `PlanTask.workload` 推导;GEMM dimen - 通用 simulator/profiler table ingestion; - 显式 Vidur 与四类 AIConfigurator ingestion; - inference task/pipeline 接入与回归测试。 +- pinned Vidur profile 的只读 evidence catalog 与 GEMM curve comparison PoC。 仍未实现: @@ -193,5 +200,6 @@ Inference task query 直接从 canonical `PlanTask.workload` 推导;GEMM dimen - contention、overlap、queueing 与 plan-level discrete-event simulation; - 作为 normalized planner objective 的 energy/power metric; - observation ingestion 与 calibration revision。 +- 多数据库 repository/registry、用户导入与显式 evidence promotion UI。 这些边界非常重要:task latency resolution 是 evidence layer,不是已经完成的 hardware simulator 或 serving simulator。 diff --git a/pyproject.toml b/pyproject.toml index d50399e..48a38e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,6 +123,7 @@ packages = ["src/blueprinting", "src/calculon"] [tool.hatch.build.targets.wheel.force-include] "data/models" = "blueprinting/presets/models" "data/systems" = "blueprinting/presets/systems" +"data/validation/vidur/phi2_a100_tp1" = "blueprinting/presets/evidence/vidur/phi2_a100_tp1" [tool.hatch.envs.default] features = ["dev", "full"] diff --git a/scripts/check_wheel_contract.py b/scripts/check_wheel_contract.py index fbf0dc6..3ca95cb 100644 --- a/scripts/check_wheel_contract.py +++ b/scripts/check_wheel_contract.py @@ -1,4 +1,4 @@ -"""Verify that the base wheel contains presets but excludes optional evidence.""" +"""Verify that the base wheel contains presets and only the pinned PoC evidence slice.""" from __future__ import annotations @@ -23,11 +23,22 @@ def main(argv: list[str]) -> int: for prefix in required_prefixes: if not any(name.startswith(prefix) and name.endswith(".json") for name in names): raise SystemExit(f"wheel is missing JSON presets under {prefix}") - forbidden_prefixes = ( - "blueprinting/systems/", - "data/evidence/", - "blueprinting/presets/evidence/", + evidence_prefix = "blueprinting/presets/evidence/vidur/phi2_a100_tp1/" + required_evidence = { + f"{evidence_prefix}LICENSE.vidur", + f"{evidence_prefix}attention.csv", + f"{evidence_prefix}manifest.json", + f"{evidence_prefix}mlp.csv", + } + missing_evidence = required_evidence - set(names) + if missing_evidence: + raise SystemExit(f"wheel is missing pinned PoC evidence: {sorted(missing_evidence)!r}") + unexpected_evidence = tuple( + name for name in names if name.startswith("blueprinting/presets/evidence/") and name not in required_evidence ) + if unexpected_evidence: + raise SystemExit(f"wheel contains unapproved evidence: {unexpected_evidence[:3]!r}") + forbidden_prefixes = ("blueprinting/systems/", "data/evidence/") leaked = tuple(name for name in names if name.startswith(forbidden_prefixes)) if leaked: raise SystemExit(f"wheel contains optional evidence: {leaked[:3]!r}") diff --git a/src/blueprinting/analysis/__init__.py b/src/blueprinting/analysis/__init__.py index aee014c..1ca6106 100644 --- a/src/blueprinting/analysis/__init__.py +++ b/src/blueprinting/analysis/__init__.py @@ -3,6 +3,8 @@ from .cost import ( AIConfiguratorPerformanceImporter, AIConfiguratorTable, + CostCurvePoint, + CostCurveReport, CostEstimate, CostNotAvailableError, CostProvider, @@ -14,6 +16,8 @@ EstimateMatch, EstimateMethod, EstimateUncertainty, + EvidenceCoverage, + EvidenceDatabaseSummary, EvidenceProvenance, LatencyUnit, PerformanceDatabase, @@ -23,6 +27,9 @@ SimulatorPerformanceImporter, TabularImportSpec, TabularPerformanceImporter, + build_gemm_comparison_curve, + comparable_gemm_semantics, + summarize_performance_database, ) from .cost_model import ( BlockEstimate, @@ -54,6 +61,8 @@ "AIConfiguratorPerformanceImporter", "AIConfiguratorTable", "CostEstimate", + "CostCurvePoint", + "CostCurveReport", "CostNotAvailableError", "CostProvider", "CostQuery", @@ -65,6 +74,8 @@ "EstimateMethod", "EstimateUncertainty", "EvidenceProvenance", + "EvidenceCoverage", + "EvidenceDatabaseSummary", "InferenceBaseline", "InferenceCostProvider", "InferenceEvidenceQuery", @@ -82,6 +93,9 @@ "SimulatorPerformanceImporter", "TabularImportSpec", "TabularPerformanceImporter", + "build_gemm_comparison_curve", + "comparable_gemm_semantics", + "summarize_performance_database", "cost_query_for_inference_task", "estimate_block", "estimate_inference_phase", diff --git a/src/blueprinting/analysis/cost/__init__.py b/src/blueprinting/analysis/cost/__init__.py index deb7e90..da0c08e 100644 --- a/src/blueprinting/analysis/cost/__init__.py +++ b/src/blueprinting/analysis/cost/__init__.py @@ -2,6 +2,15 @@ from .aiconfigurator import AIConfiguratorPerformanceImporter, AIConfiguratorTable from .database import EvidenceProvenance, PerformanceDatabase, PerformanceDatabaseProvider, PerformanceRecord +from .explorer import ( + CostCurvePoint, + CostCurveReport, + EvidenceCoverage, + EvidenceDatabaseSummary, + build_gemm_comparison_curve, + comparable_gemm_semantics, + summarize_performance_database, +) from .importers import ( LatencyUnit, SimulatorPerformanceImporter, @@ -37,6 +46,8 @@ "CostProvider", "CostQuery", "CostQueryContext", + "CostCurvePoint", + "CostCurveReport", "CostResolution", "CostResolver", "CostSubject", @@ -45,6 +56,8 @@ "EstimateMethod", "EstimateUncertainty", "EvidenceProvenance", + "EvidenceCoverage", + "EvidenceDatabaseSummary", "InvalidCostEvidenceError", "LatencyUnit", "PerformanceDatabase", @@ -56,4 +69,7 @@ "SupportStatus", "TabularImportSpec", "TabularPerformanceImporter", + "build_gemm_comparison_curve", + "comparable_gemm_semantics", + "summarize_performance_database", ] diff --git a/src/blueprinting/analysis/cost/explorer.py b/src/blueprinting/analysis/cost/explorer.py new file mode 100644 index 0000000..89a17bb --- /dev/null +++ b/src/blueprinting/analysis/cost/explorer.py @@ -0,0 +1,286 @@ +"""Read-only derived views for inspecting and comparing performance evidence.""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass + +from blueprinting.schema.frozen import FrozenDict +from blueprinting.system import SystemProfile + +from ..cost_model import CalibrationMode +from .database import PerformanceDatabase, PerformanceDatabaseProvider, PerformanceRecord +from .protocol import CostQuery, CostSubject +from .roofline import RooflineCostProvider + + +@dataclass(frozen=True) +class EvidenceCoverage: + subject: str + operation: str + semantic_operation: str + hardware: str + datatype: str + method: str + record_count: int + selector_axes: tuple[str, ...] + + +@dataclass(frozen=True) +class EvidenceDatabaseSummary: + name: str + revision: str + source: str + source_revision: str + importer: str + record_count: int + hardware: tuple[str, ...] + datatypes: tuple[str, ...] + operations: tuple[str, ...] + coverage: tuple[EvidenceCoverage, ...] + + +@dataclass(frozen=True) +class CostCurvePoint: + x: int + query_digest: str + operations: int + read_bytes: int + write_bytes: int + measured_seconds: float + measured_lower_seconds: float | None + measured_upper_seconds: float | None + analytical_seconds: float + measured_teraops_per_second: float + analytical_teraops_per_second: float + analytical_bottleneck: str + relative_error_percent: float | None + sample_count: int + raw_record_ids: tuple[str, ...] + + +@dataclass(frozen=True) +class CostCurveReport: + database_name: str + database_revision: str + analytical_provider: str + analytical_revision: str + operation: str + semantic_operation: str + hardware: str + datatype: str + axis: str + fixed_selectors: FrozenDict + points: tuple[CostCurvePoint, ...] + limitations: tuple[str, ...] + + +def summarize_performance_database(database: PerformanceDatabase) -> EvidenceDatabaseSummary: + """Build a stable catalog/coverage view without changing evidence records.""" + + if not isinstance(database, PerformanceDatabase): + raise TypeError("database must be PerformanceDatabase") + groups: dict[tuple[str, ...], list[PerformanceRecord]] = defaultdict(list) + for record in database.records: + semantic = str(record.selector.get("semantic_operation", record.operation)) + key = ( + record.subject.value, + record.operation, + semantic, + record.hardware, + record.datatype, + record.provenance.method.value, + ) + groups[key].append(record) + coverage = tuple( + EvidenceCoverage( + subject=key[0], + operation=key[1], + semantic_operation=key[2], + hardware=key[3], + datatype=key[4], + method=key[5], + record_count=len(records), + selector_axes=tuple(sorted({name for record in records for name in record.selector})), + ) + for key, records in sorted(groups.items()) + ) + provenances = tuple(record.provenance for record in database.records) + return EvidenceDatabaseSummary( + name=database.name, + revision=database.revision, + source=_single_value(tuple(item.source for item in provenances), "source"), + source_revision=_single_value(tuple(item.source_revision for item in provenances), "source_revision"), + importer=_single_value(tuple(item.importer for item in provenances), "importer"), + record_count=len(database.records), + hardware=tuple(sorted({record.hardware for record in database.records})), + datatypes=tuple(sorted({record.datatype for record in database.records})), + operations=tuple(sorted({record.operation for record in database.records})), + coverage=coverage, + ) + + +def comparable_gemm_semantics(database: PerformanceDatabase) -> tuple[str, ...]: + return tuple( + sorted( + { + str(record.selector["semantic_operation"]) + for record in database.records + if record.operation == "gemm" and "semantic_operation" in record.selector + } + ) + ) + + +def build_gemm_comparison_curve( + database: PerformanceDatabase, + hardware: SystemProfile, + semantic_operation: str, + *, + mode: CalibrationMode = CalibrationMode.SYSTEM_EVIDENCE, +) -> CostCurveReport: + """Compare exact GEMM records with an analytical roofline on identical facts.""" + + if not isinstance(database, PerformanceDatabase): + raise TypeError("database must be PerformanceDatabase") + if not isinstance(hardware, SystemProfile): + raise TypeError("hardware must be SystemProfile") + if not isinstance(mode, CalibrationMode): + raise TypeError("mode must be CalibrationMode") + records = tuple( + record + for record in database.records + if record.subject is CostSubject.OPERATOR + and record.operation == "gemm" + and record.hardware == hardware.name + and record.datatype == hardware.datatype + and record.selector.get("semantic_operation") == semantic_operation + ) + if not records: + raise ValueError(f"database has no comparable GEMM records for {semantic_operation!r}") + if any("num_tokens" not in record.selector for record in records): + raise ValueError("comparable GEMM records require a num_tokens selector") + fixed_selectors = _fixed_selectors(records, axis="num_tokens") + database_provider = PerformanceDatabaseProvider(database) + analytical_provider = RooflineCostProvider(hardware, mode=mode, processing_mode="roofline") + points = [] + seen_tokens: set[int] = set() + for record in sorted(records, key=lambda item: int(item.selector["num_tokens"])): + tokens = int(record.selector["num_tokens"]) + if tokens in seen_tokens: + continue + seen_tokens.add(tokens) + m, n, k = _gemm_shape(record.selector, semantic_operation) + element_bytes = _datatype_bytes(record.datatype) + operations = 2 * m * n * k + read_bytes = (m * n + n * k) * element_bytes + write_bytes = m * k * element_bytes + query = CostQuery( + subject=CostSubject.OPERATOR, + operation="gemm", + hardware=record.hardware, + datatype=record.datatype, + operations=operations, + read_bytes=read_bytes, + write_bytes=write_bytes, + engine="matrix", + hardware_revision=hardware.evidence_revision, + dimensions=record.selector, + ) + measured = database_provider.estimate(query) + analytical = analytical_provider.estimate(query) + measured_rate = operations / measured.seconds / 1e12 if measured.seconds else 0.0 + analytical_rate = operations / analytical.seconds / 1e12 if analytical.seconds else 0.0 + relative_error = None + if measured.seconds: + relative_error = (analytical.seconds - measured.seconds) / measured.seconds * 100 + points.append( + CostCurvePoint( + x=tokens, + query_digest=query.digest, + operations=operations, + read_bytes=read_bytes, + write_bytes=write_bytes, + measured_seconds=measured.seconds, + measured_lower_seconds=measured.uncertainty.lower_bound_seconds, + measured_upper_seconds=measured.uncertainty.upper_bound_seconds, + analytical_seconds=analytical.seconds, + measured_teraops_per_second=measured_rate, + analytical_teraops_per_second=analytical_rate, + analytical_bottleneck=str(analytical.components["bottleneck"]), + relative_error_percent=relative_error, + sample_count=measured.uncertainty.sample_count, + raw_record_ids=measured.raw_record_ids, + ) + ) + return CostCurveReport( + database_name=database.name, + database_revision=database.revision, + analytical_provider=analytical_provider.name, + analytical_revision=analytical_provider.revision, + operation="gemm", + semantic_operation=semantic_operation, + hardware=hardware.name, + datatype=hardware.datatype, + axis="num_tokens", + fixed_selectors=FrozenDict(fixed_selectors), + points=tuple(points), + limitations=( + "Measured points are exact imported selectors; connecting lines do not claim interpolation.", + "The analytical series is a compute/memory roofline bound, not an event-level simulation.", + "This PoC compares GEMM primitives only because their exact M/N/K workload can be reconstructed.", + ), + ) + + +def _single_value(values: tuple[str, ...], name: str) -> str: + unique = tuple(sorted(set(values))) + if len(unique) != 1: + raise ValueError(f"database contains multiple {name} values: {unique}") + return unique[0] + + +def _fixed_selectors(records: tuple[PerformanceRecord, ...], *, axis: str) -> dict[str, object]: + keys = set(records[0].selector) - {axis} + result: dict[str, object] = {} + for key in sorted(keys): + values = {record.selector.get(key) for record in records} + if len(values) != 1: + raise ValueError(f"selector {key!r} varies within one comparison curve") + result[key] = next(iter(values)) + return result + + +def _gemm_shape(selector: FrozenDict, semantic_operation: str) -> tuple[int, int, int]: + tokens = int(selector["num_tokens"]) + hidden = int(selector["hidden_size"]) + feedforward = int(selector["feedforward_size"]) + shapes = { + "attention_pre_projection": (tokens, hidden, 3 * hidden), + "attention_post_projection": (tokens, hidden, hidden), + "mlp_up_projection": (tokens, hidden, feedforward), + "mlp_down_projection": (tokens, feedforward, hidden), + } + try: + return shapes[semantic_operation] + except KeyError as error: + raise ValueError(f"unsupported comparable GEMM semantic: {semantic_operation!r}") from error + + +def _datatype_bytes(datatype: str) -> int: + sizes = {"float8": 1, "float16": 2, "bfloat16": 2, "float32": 4} + try: + return sizes[datatype] + except KeyError as error: + raise ValueError(f"unsupported datatype size: {datatype!r}") from error + + +__all__ = [ + "CostCurvePoint", + "CostCurveReport", + "EvidenceCoverage", + "EvidenceDatabaseSummary", + "build_gemm_comparison_curve", + "comparable_gemm_semantics", + "summarize_performance_database", +] diff --git a/src/blueprinting/workbench/evidence_lab.py b/src/blueprinting/workbench/evidence_lab.py new file mode 100644 index 0000000..b2f244a --- /dev/null +++ b/src/blueprinting/workbench/evidence_lab.py @@ -0,0 +1,369 @@ +"""Read-only performance-evidence catalog and comparison PoC for NiceGUI.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from nicegui import ui + +from blueprinting.analysis import ( + CalibrationMode, + CostCurveReport, + EvidenceDatabaseSummary, + PerformanceDatabase, + VidurProfileImporter, + build_gemm_comparison_curve, + comparable_gemm_semantics, + summarize_performance_database, +) +from blueprinting.system import SystemProfile + +from .catalog import ConfigCatalog + +_SEMANTIC_LABELS = { + "attention_pre_projection": "Attention QKV projection", + "attention_post_projection": "Attention output projection", + "mlp_up_projection": "MLP up projection", + "mlp_down_projection": "MLP down projection", +} + + +@dataclass(frozen=True) +class EvidenceLabData: + database: PerformanceDatabase + hardware: SystemProfile + summary: EvidenceDatabaseSummary + manifest: dict[str, Any] + + +def load_vidur_poc_evidence(catalog: ConfigCatalog, root: Path | None = None) -> EvidenceLabData: + """Load the explicitly pinned repository/package Vidur validation slice.""" + + evidence_root = root or _default_vidur_root() + manifest_path = evidence_root / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + source = manifest["source"] + selection = manifest["selection"] + blueprinting = manifest["blueprinting"] + hardware_name = str(blueprinting["hardware"]["name"]) + datatype = str(selection["datatype"]) + database = VidurProfileImporter.from_csv( + attention_csv=evidence_root / "attention.csv", + compute_csv=evidence_root / "mlp.csv", + model_name=str(selection["model"]), + hardware_name=hardware_name, + source_revision=str(source["revision"]), + datatype=datatype, + database_name="vidur-phi2-a100-poc", + ) + hardware = SystemProfile.from_mapping( + hardware_name, + catalog.load("systems", f"{hardware_name}.json"), + datatype=datatype, + ) + return EvidenceLabData(database, hardware, summarize_performance_database(database), manifest) + + +def coverage_rows(summary: EvidenceDatabaseSummary) -> list[dict[str, Any]]: + return [ + { + "semantic_operation": item.semantic_operation, + "operation": item.operation, + "subject": item.subject, + "records": item.record_count, + "hardware": item.hardware, + "datatype": item.datatype, + "method": item.method, + "axes": ", ".join(item.selector_axes), + } + for item in summary.coverage + ] + + +def coverage_chart_options(summary: EvidenceDatabaseSummary) -> dict[str, Any]: + rows = sorted(coverage_rows(summary), key=lambda item: (item["records"], item["semantic_operation"])) + return { + "backgroundColor": "transparent", + "animationDuration": 250, + "grid": {"left": 172, "right": 30, "top": 18, "bottom": 38}, + "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}}, + "xAxis": {"type": "value", "name": "records", "minInterval": 1}, + "yAxis": { + "type": "category", + "data": [item["semantic_operation"] for item in rows], + "axisLabel": {"width": 158, "overflow": "truncate"}, + }, + "series": [ + { + "name": "Imported records", + "type": "bar", + "barMaxWidth": 16, + "itemStyle": {"color": "#2563eb", "borderRadius": [0, 4, 4, 0]}, + "data": [item["records"] for item in rows], + } + ], + } + + +def latency_curve_options(report: CostCurveReport) -> dict[str, Any]: + return { + "backgroundColor": "transparent", + "animationDuration": 250, + "legend": {"top": 0, "data": ["Vidur measured", "Analytical roofline"]}, + "grid": {"left": 68, "right": 28, "top": 50, "bottom": 48}, + "tooltip": {"trigger": "axis"}, + "xAxis": {"type": "log", "name": "num_tokens", "min": 1}, + "yAxis": {"type": "value", "name": "latency (µs)", "min": 0}, + "series": [ + { + "name": "Vidur measured", + "type": "scatter", + "symbolSize": 10, + "itemStyle": {"color": "#2563eb"}, + "data": [[point.x, point.measured_seconds * 1e6] for point in report.points], + }, + { + "name": "Analytical roofline", + "type": "line", + "showSymbol": True, + "lineStyle": {"color": "#d97706", "width": 2}, + "itemStyle": {"color": "#d97706"}, + "data": [[point.x, point.analytical_seconds * 1e6] for point in report.points], + }, + ], + } + + +def throughput_curve_options(report: CostCurveReport) -> dict[str, Any]: + return { + "backgroundColor": "transparent", + "animationDuration": 250, + "legend": {"top": 0, "data": ["Vidur effective", "Roofline effective"]}, + "grid": {"left": 68, "right": 28, "top": 50, "bottom": 48}, + "tooltip": {"trigger": "axis"}, + "xAxis": {"type": "log", "name": "num_tokens", "min": 1}, + "yAxis": {"type": "value", "name": "effective TOPS", "min": 0}, + "series": [ + { + "name": "Vidur effective", + "type": "scatter", + "symbolSize": 10, + "itemStyle": {"color": "#0891b2"}, + "data": [[point.x, point.measured_teraops_per_second] for point in report.points], + }, + { + "name": "Roofline effective", + "type": "line", + "showSymbol": True, + "lineStyle": {"color": "#7c3aed", "width": 2}, + "itemStyle": {"color": "#7c3aed"}, + "data": [[point.x, point.analytical_teraops_per_second] for point in report.points], + }, + ], + } + + +def curve_rows(report: CostCurveReport) -> list[dict[str, Any]]: + return [ + { + "num_tokens": point.x, + "measured_us": round(point.measured_seconds * 1e6, 4), + "roofline_us": round(point.analytical_seconds * 1e6, 4), + "relative_error_percent": ( + round(point.relative_error_percent, 2) if point.relative_error_percent is not None else None + ), + "measured_tops": round(point.measured_teraops_per_second, 3), + "roofline_tops": round(point.analytical_teraops_per_second, 3), + "bottleneck": point.analytical_bottleneck, + "samples": point.sample_count, + "query_digest": point.query_digest, + "record_ids": ", ".join(point.raw_record_ids), + } + for point in report.points + ] + + +class EvidenceLabPanel: + """Stateful NiceGUI panel around immutable evidence-derived views.""" + + def __init__(self, catalog: ConfigCatalog) -> None: + self.catalog = catalog + self.data: EvidenceLabData | None = None + self.semantic_operation = "attention_pre_projection" + self.content: Any | None = None + self.error: str | None = None + + def build(self) -> None: + try: + self.data = load_vidur_poc_evidence(self.catalog) + semantics = comparable_gemm_semantics(self.data.database) + if self.semantic_operation not in semantics: + self.semantic_operation = semantics[0] + except (FileNotFoundError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error: + self.error = f"{type(error).__name__}: {error}" + with ui.column().classes("w-full gap-4").mark("evidence-lab"): + with ( + ui.element("section").classes("bp-evidence-surface"), + ui.element("div").classes("bp-evidence-section"), + ): + ui.label("EVIDENCE LENS").classes("bp-kicker") + ui.label("性能证据实验室").classes("bp-result-title mt-1") + ui.label( + "浏览带 provenance 的性能记录,并在相同 workload facts 上比较实测数据与解析模型。" + ).classes("bp-card-copy mt-1") + ui.label("只读 PoC · exact selectors · no interpolation").classes("bp-fidelity-tag bp-mono mt-3") + if self.error is not None or self.data is None: + with ( + ui.element("section").classes("bp-evidence-surface"), + ui.element("div").classes("bp-evidence-section"), + ): + ui.label("证据数据不可用").classes("bp-card-title") + ui.label(self.error or "unknown evidence loading error").classes("bp-card-copy bp-mono") + return + semantics = comparable_gemm_semantics(self.data.database) + with ( + ui.element("section").classes("bp-evidence-surface"), + ui.element("div").classes("bp-evidence-section"), + ): + ui.label("对比控制").classes("bp-card-title") + ui.label("首版只比较能够重建精确 M/N/K 的 GEMM primitive;其他 operation 仍出现在覆盖目录。 ").classes( + "bp-card-copy" + ) + ui.select( + {item: _SEMANTIC_LABELS.get(item, item) for item in semantics}, + value=self.semantic_operation, + label="GEMM semantic operation", + on_change=self._semantic_changed, + ).props("outlined dense").classes("w-80 max-w-full mt-3").mark("evidence-semantic-operation") + self.content = ui.column().classes("w-full gap-4") + self._render_content() + + def _semantic_changed(self, event: Any) -> None: + self.semantic_operation = str(event.value) + self._render_content() + + def _render_content(self) -> None: + if self.content is None or self.data is None: + return + self.content.clear() + report = build_gemm_comparison_curve( + self.data.database, + self.data.hardware, + self.semantic_operation, + mode=CalibrationMode.SYSTEM_EVIDENCE, + ) + summary = self.data.summary + source = self.data.manifest["source"] + with self.content: + with ( + ui.element("section").classes("bp-evidence-surface"), + ui.element("div").classes("bp-evidence-section"), + ): + ui.label("Evidence catalog").classes("bp-card-title") + with ui.element("div").classes("bp-chain-stats mt-3"): + for label, value in ( + ("Database", summary.name), + ("Records", str(summary.record_count)), + ("Hardware", ", ".join(summary.hardware)), + ("Datatype", ", ".join(summary.datatypes)), + ("Method", summary.coverage[0].method), + ("Source revision", summary.source_revision[:12]), + ): + with ui.column().classes("gap-0"): + ui.label(label).classes("bp-summary-label") + ui.label(value).classes("bp-card-copy bp-mono") + ui.label( + f"Source: {source['repository']} · License: {source['license']} · DB revision: {summary.revision}" + ).classes("bp-card-copy bp-mono mt-3") + with ui.element("section").classes("bp-evidence-surface"): + with ui.element("div").classes("bp-evidence-section"): + ui.label("Operation coverage").classes("bp-card-title") + ui.label("这里展示已导入的 exact records,不代表未覆盖点可插值。 ").classes("bp-card-copy") + ui.echart(coverage_chart_options(summary), renderer="svg").classes("w-full h-80") + with ui.element("div").classes("bp-evidence-section"): + ui.aggrid( + { + "columnDefs": [ + {"headerName": "Semantic operation", "field": "semantic_operation", "pinned": "left"}, + {"headerName": "Family", "field": "operation"}, + {"headerName": "Records", "field": "records", "type": "numericColumn"}, + {"headerName": "Hardware", "field": "hardware"}, + {"headerName": "Datatype", "field": "datatype"}, + {"headerName": "Method", "field": "method"}, + {"headerName": "Controlled selectors", "field": "axes", "flex": 1}, + ], + "rowData": coverage_rows(summary), + "defaultColDef": {"sortable": True, "filter": True, "resizable": True}, + }, + theme="quartz", + ).classes("w-full").style("height: 360px") + with ui.element("section").classes("bp-evidence-surface"): + with ui.element("div").classes("bp-evidence-section"): + ui.label(f"Characteristic curve · {_SEMANTIC_LABELS.get(self.semantic_operation)}").classes( + "bp-card-title" + ) + ui.label("蓝色散点是 Vidur exact records;橙色线是同一 workload facts 的解析 Roofline。 ").classes( + "bp-card-copy" + ) + with ui.element("div").classes("bp-evidence-section bp-analysis-split"): + with ui.element("section").classes("bp-evidence-block"): + ui.label("Latency").classes("bp-section-title") + ui.echart(latency_curve_options(report), renderer="svg").classes("w-full h-80") + with ui.element("section").classes("bp-evidence-block"): + ui.label("Effective throughput").classes("bp-section-title") + ui.echart(throughput_curve_options(report), renderer="svg").classes("w-full h-80") + with ui.element("div").classes("bp-evidence-section"): + ui.aggrid( + { + "columnDefs": [ + {"headerName": "Tokens", "field": "num_tokens", "pinned": "left"}, + {"headerName": "Measured µs", "field": "measured_us", "type": "numericColumn"}, + {"headerName": "Roofline µs", "field": "roofline_us", "type": "numericColumn"}, + {"headerName": "Model error %", "field": "relative_error_percent", "type": "numericColumn"}, + {"headerName": "Measured TOPS", "field": "measured_tops", "type": "numericColumn"}, + {"headerName": "Roofline TOPS", "field": "roofline_tops", "type": "numericColumn"}, + {"headerName": "Bottleneck", "field": "bottleneck"}, + {"headerName": "Samples", "field": "samples", "type": "numericColumn"}, + {"headerName": "Query digest", "field": "query_digest", "flex": 1}, + {"headerName": "Raw record IDs", "field": "record_ids", "flex": 1}, + ], + "rowData": curve_rows(report), + "defaultColDef": {"sortable": True, "filter": True, "resizable": True}, + }, + theme="quartz", + ).classes("w-full").style("height: 300px").mark("evidence-query-inspector") + with ( + ui.element("section").classes("bp-evidence-surface"), + ui.element("div").classes("bp-evidence-section"), + ): + ui.label("适用边界").classes("bp-card-title") + for limitation in report.limitations: + with ui.row().classes("items-start gap-2 no-wrap"): + ui.icon("info", size="18px", color="secondary") + ui.label(limitation).classes("bp-card-copy") + + +def _default_vidur_root() -> Path: + package_root = Path(__file__).resolve().parents[1] / "presets" / "evidence" / "vidur" / "phi2_a100_tp1" + if package_root.is_dir(): + return package_root + repository_root = Path(__file__).resolve().parents[3] + source_root = repository_root / "data" / "validation" / "vidur" / "phi2_a100_tp1" + if source_root.is_dir(): + return source_root + raise FileNotFoundError("the pinned Vidur Phi-2/A100 evidence slice is not installed") + + +__all__ = [ + "EvidenceLabData", + "EvidenceLabPanel", + "coverage_chart_options", + "coverage_rows", + "curve_rows", + "latency_curve_options", + "load_vidur_poc_evidence", + "throughput_curve_options", +] diff --git a/src/blueprinting/workbench/nicegui_theme.py b/src/blueprinting/workbench/nicegui_theme.py index a0c8cda..b98d99f 100644 --- a/src/blueprinting/workbench/nicegui_theme.py +++ b/src/blueprinting/workbench/nicegui_theme.py @@ -124,7 +124,7 @@ } .bp-mode-switch { - min-height: 42px; + min-height: 79px; padding: 3px; border: 1px solid var(--bp-sidebar-line); border-radius: 8px; @@ -132,19 +132,32 @@ } .bp-mode-switch .q-tabs__content { + display: grid !important; + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px; + overflow: visible !important; + transform: none !important; } .bp-mode-switch .q-tab { + width: 100%; + min-width: 0; min-height: 34px; - flex: 1 1 0; - padding: 0 7px; + padding: 0 5px; color: var(--bp-sidebar-muted); border: 1px solid transparent; border-radius: 6px; font-size: 11px; } +.bp-mode-switch .q-tabs__arrow { + display: none !important; +} + +.bp-mode-switch .q-tab__indicator { + display: none !important; +} + .bp-mode-switch .q-tab__content { flex-direction: row; justify-content: center; diff --git a/src/blueprinting/workbench/nicegui_ui.py b/src/blueprinting/workbench/nicegui_ui.py index d5170dd..1e39723 100644 --- a/src/blueprinting/workbench/nicegui_ui.py +++ b/src/blueprinting/workbench/nicegui_ui.py @@ -28,6 +28,7 @@ from .catalog import ConfigCatalog, default_catalog from .chrome_trace import perfetto_open_javascript, portable_projection_trace_json +from .evidence_lab import EvidenceLabPanel from .float_analysis import FloatAnalysisPanel from .nicegui_theme import METRIC_COLORS, WORKBENCH_CSS from .presentation import ( @@ -76,6 +77,7 @@ class WorkbenchMode(Enum): ANALYSIS = "analysis" SWEEP = "sweep" + EVIDENCE = "evidence" FLOAT = "float" @@ -608,6 +610,7 @@ def __init__( self.batch_pp_filter: Any | None = None self.batch_dp_filter: Any | None = None self.batch_grid: Any | None = None + self.evidence_panel: EvidenceLabPanel | None = None self.float_panel: FloatAnalysisPanel | None = None def build(self) -> None: @@ -644,6 +647,11 @@ def build(self) -> None: "批量探索", icon="scatter_plot", ).mark("mode-sweep") + self.evidence_mode_tab = ui.tab( + WorkbenchMode.EVIDENCE.value, + "性能证据", + icon="monitoring", + ).mark("mode-evidence") self.float_mode_tab = ui.tab( WorkbenchMode.FLOAT.value, "浮点分析", @@ -715,7 +723,7 @@ def _change_mode(self, event: Any) -> None: self.mode = WorkbenchMode(str(event.value)) self.local_error = None self.config_dialog.close() - if self.form is not None and self.mode is not WorkbenchMode.FLOAT: + if self.form is not None and self.mode in {WorkbenchMode.ANALYSIS, WorkbenchMode.SWEEP}: self.form.set_mode(self.mode) self._render_sidebar_controls() self._render_workspace() @@ -747,6 +755,14 @@ def _render_sidebar_controls(self) -> None: self.quick_pp = None self.quick_dp = None self.quick_calibration = None + if self.mode is WorkbenchMode.EVIDENCE: + with self.sidebar_controls: + ui.label("EVIDENCE CONTROLS").classes("bp-sidebar-kicker") + with ui.element("section").classes("bp-sidebar-controls-card"): + ui.label("只读证据目录").classes("bp-sidebar-title") + ui.label("Vidur Phi-2 · A100 · exact selectors").classes("bp-sidebar-meta") + ui.label("GEMM primitive 在主视图切换。 ").classes("bp-sidebar-meta") + return if self.mode is WorkbenchMode.FLOAT: with self.sidebar_controls: ui.label("NUMERIC CONTROLS").classes("bp-sidebar-kicker") @@ -940,6 +956,9 @@ def _render_workspace(self) -> None: with self.workspace: if self.busy: self._render_loading() + elif self.mode is WorkbenchMode.EVIDENCE: + self.evidence_panel = EvidenceLabPanel(self.catalog) + self.evidence_panel.build() elif self.mode is WorkbenchMode.FLOAT: self.float_panel = FloatAnalysisPanel() self.float_panel.build() @@ -956,6 +975,13 @@ def _render_sidebar_summary(self) -> None: if self.sidebar_summary is None: return self.sidebar_summary.clear() + if self.mode is WorkbenchMode.EVIDENCE: + with self.sidebar_summary: + ui.label("EVIDENCE STATUS").classes("bp-sidebar-kicker") + ui.label("Pinned profile").classes("bp-sidebar-title") + ui.label("20 typed records · measured + analytical").classes("bp-sidebar-meta") + ui.label("只读 PoC").classes("bp-sidebar-state bp-sidebar-state--ready") + return if self.mode is WorkbenchMode.FLOAT: with self.sidebar_summary: ui.label("NUMERIC STATUS").classes("bp-sidebar-kicker") @@ -1007,7 +1033,7 @@ def _render_sidebar_action(self) -> None: if self.sidebar_action_host is None: return self.sidebar_action_host.clear() - if self.mode is WorkbenchMode.FLOAT: + if self.mode in {WorkbenchMode.EVIDENCE, WorkbenchMode.FLOAT}: return if self.form is None: return @@ -1220,6 +1246,7 @@ async def run_analysis(self) -> None: self._set_quick_controls_busy(True) self.analysis_mode_tab.disable() self.sweep_mode_tab.disable() + self.evidence_mode_tab.disable() self.float_mode_tab.disable() self.config_dialog.close() self._render_workspace() @@ -1239,6 +1266,7 @@ async def run_analysis(self) -> None: self._set_quick_controls_busy(False) self.analysis_mode_tab.enable() self.sweep_mode_tab.enable() + self.evidence_mode_tab.enable() self.float_mode_tab.enable() self._render_workspace() @@ -1261,6 +1289,7 @@ async def run_sweep(self) -> None: self._set_quick_controls_busy(True) self.analysis_mode_tab.disable() self.sweep_mode_tab.disable() + self.evidence_mode_tab.disable() self.float_mode_tab.disable() self.config_dialog.close() self.progress_timer.activate() @@ -1290,6 +1319,7 @@ def on_progress(completed: int, total: int) -> None: self._set_quick_controls_busy(False) self.analysis_mode_tab.enable() self.sweep_mode_tab.enable() + self.evidence_mode_tab.enable() self.float_mode_tab.enable() self._render_workspace() diff --git a/tests/analysis/test_evidence_explorer.py b/tests/analysis/test_evidence_explorer.py new file mode 100644 index 0000000..245902e --- /dev/null +++ b/tests/analysis/test_evidence_explorer.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from blueprinting.analysis import ( + PerformanceDatabase, + VidurProfileImporter, + build_gemm_comparison_curve, + comparable_gemm_semantics, + summarize_performance_database, +) +from blueprinting.system import SystemProfile + +ROOT = Path(__file__).resolve().parents[2] +PROFILE = ROOT / "data" / "validation" / "vidur" / "phi2_a100_tp1" +SOURCE_REVISION = "8383d2935bc62723a212090baa9f98ada206fc14" + + +def _database() -> PerformanceDatabase: + return VidurProfileImporter.from_csv( + attention_csv=PROFILE / "attention.csv", + compute_csv=PROFILE / "mlp.csv", + model_name="microsoft/phi-2", + hardware_name="a100_80g", + source_revision=SOURCE_REVISION, + database_name="vidur-phi2-a100-poc", + ) + + +def _hardware() -> SystemProfile: + payload = json.loads((ROOT / "data" / "systems" / "a100_80g.json").read_text(encoding="utf-8")) + return SystemProfile.from_mapping("a100_80g", payload, datatype="float16") + + +def test_evidence_catalog_reports_typed_coverage_and_provenance() -> None: + summary = summarize_performance_database(_database()) + + assert summary.record_count == 20 + assert summary.hardware == ("a100_80g",) + assert summary.datatypes == ("float16",) + assert summary.source == "vidur-profile" + assert summary.source_revision == SOURCE_REVISION + assert {item.semantic_operation for item in summary.coverage} >= { + "attention_core", + "attention_pre_projection", + "mlp_up_projection", + } + + +def test_gemm_curve_compares_identical_facts_without_mutating_database() -> None: + database = _database() + before = database.to_json() + + report = build_gemm_comparison_curve(database, _hardware(), "mlp_up_projection") + + assert [point.x for point in report.points] == [1, 128] + assert all(point.operations > 0 and point.read_bytes > 0 and point.write_bytes > 0 for point in report.points) + assert all(point.measured_seconds > 0 and point.analytical_seconds > 0 for point in report.points) + assert all(point.raw_record_ids and point.query_digest for point in report.points) + assert len({point.query_digest for point in report.points}) == 2 + assert database.to_json() == before + + +def test_evidence_curve_is_stable_after_database_round_trip() -> None: + database = _database() + restored = PerformanceDatabase.from_json(database.to_json()) + + original = build_gemm_comparison_curve(database, _hardware(), "attention_post_projection") + round_trip = build_gemm_comparison_curve(restored, _hardware(), "attention_post_projection") + + assert restored.revision == database.revision + assert round_trip == original + + +def test_evidence_curve_rejects_an_uncovered_semantic() -> None: + database = _database() + + assert comparable_gemm_semantics(database) == ( + "attention_post_projection", + "attention_pre_projection", + "mlp_down_projection", + "mlp_up_projection", + ) + with pytest.raises(ValueError, match="no comparable GEMM records"): + build_gemm_comparison_curve(database, _hardware(), "attention_core") diff --git a/tests/workbench/test_evidence_lab.py b/tests/workbench/test_evidence_lab.py new file mode 100644 index 0000000..a445766 --- /dev/null +++ b/tests/workbench/test_evidence_lab.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from blueprinting.analysis import build_gemm_comparison_curve +from blueprinting.workbench.catalog import default_catalog +from blueprinting.workbench.evidence_lab import ( + coverage_chart_options, + coverage_rows, + curve_rows, + latency_curve_options, + load_vidur_poc_evidence, + throughput_curve_options, +) + + +def test_evidence_lab_builds_catalog_and_comparison_presentations() -> None: + data = load_vidur_poc_evidence(default_catalog()) + report = build_gemm_comparison_curve(data.database, data.hardware, "mlp_up_projection") + + coverage = coverage_rows(data.summary) + latency = latency_curve_options(report) + throughput = throughput_curve_options(report) + rows = curve_rows(report) + + assert len(coverage) == 9 + assert {series["name"] for series in latency["series"]} == {"Vidur measured", "Analytical roofline"} + assert {series["name"] for series in throughput["series"]} == {"Vidur effective", "Roofline effective"} + assert [row["num_tokens"] for row in rows] == [1, 128] + assert all(row["query_digest"] and row["record_ids"] for row in rows) + assert coverage_chart_options(data.summary)["series"][0]["name"] == "Imported records" diff --git a/tests/workbench/test_nicegui_workbench.py b/tests/workbench/test_nicegui_workbench.py index 751a2f1..69954ac 100644 --- a/tests/workbench/test_nicegui_workbench.py +++ b/tests/workbench/test_nicegui_workbench.py @@ -10,6 +10,7 @@ from nicegui.testing import User, user_simulation from blueprinting.workbench.nicegui_app import build_parser, run_workbench +from blueprinting.workbench.nicegui_theme import WORKBENCH_CSS from blueprinting.workbench.nicegui_ui import create_workbench_root @@ -51,6 +52,7 @@ async def test_nicegui_workbench_loads_without_eager_analysis( await user.should_see(marker="run-analysis") await user.should_see(marker="sidebar-run-analysis") await user.should_see("Calculon / Streamlit Legacy") + await user.should_see(marker="mode-evidence") await user.should_see(marker="mode-float") @@ -149,6 +151,22 @@ async def test_nicegui_float_analysis_is_available_without_running_workload_anal await user.should_see("FP7(E4M2) 位级计算器") +async def test_nicegui_evidence_lab_compares_measured_and_analytical_curves( + simulated_user: Callable[[Callable[[], None]], AbstractAsyncContextManager[User]], +) -> None: + async with simulated_user(create_workbench_root()) as user: + await user.open("/") + user.find(marker="mode-evidence").click() + + await user.should_see("性能证据实验室") + await user.should_see("Evidence catalog") + await user.should_see("Operation coverage") + await user.should_see("Characteristic curve") + await user.should_see("Vidur exact records") + await user.should_see(marker="evidence-semantic-operation") + await user.should_see(marker="evidence-query-inspector") + + def test_workbench_cli_defaults_to_local_only() -> None: args = build_parser().parse_args([]) @@ -158,6 +176,13 @@ def test_workbench_cli_defaults_to_local_only() -> None: assert not args.reload +def test_sidebar_mode_switch_uses_a_non_scrolling_two_by_two_grid() -> None: + assert "grid-template-columns: repeat(2, minmax(0, 1fr))" in WORKBENCH_CSS + assert ".bp-mode-switch .q-tabs__arrow" in WORKBENCH_CSS + assert ".bp-mode-switch .q-tab__indicator" in WORKBENCH_CSS + assert "display: none !important" in WORKBENCH_CSS + + def test_workbench_cli_accepts_server_overrides() -> None: args: Any = build_parser().parse_args(["--host", "0.0.0.0", "--port", "9000", "--no-open", "--reload"]) From c3bb8e82f54c2e638cdfecae2c1f60b465670fac Mon Sep 17 00:00:00 2001 From: Reiase Date: Sun, 9 Aug 2026 23:14:26 +0800 Subject: [PATCH 2/6] Fix workbench mode switch layout --- src/blueprinting/workbench/nicegui_theme.py | 44 +++++----- src/blueprinting/workbench/nicegui_ui.py | 93 ++++++++++++++------- tests/workbench/test_nicegui_workbench.py | 7 +- 3 files changed, 84 insertions(+), 60 deletions(-) diff --git a/src/blueprinting/workbench/nicegui_theme.py b/src/blueprinting/workbench/nicegui_theme.py index b98d99f..1061a31 100644 --- a/src/blueprinting/workbench/nicegui_theme.py +++ b/src/blueprinting/workbench/nicegui_theme.py @@ -124,53 +124,47 @@ } .bp-mode-switch { - min-height: 79px; - padding: 3px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px; + padding: 4px; border: 1px solid var(--bp-sidebar-line); border-radius: 8px; background: rgba(15, 23, 42, .78); } -.bp-mode-switch .q-tabs__content { - display: grid !important; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 3px; - overflow: visible !important; - transform: none !important; -} - -.bp-mode-switch .q-tab { +.bp-mode-switch .bp-mode-button { width: 100%; min-width: 0; - min-height: 34px; - padding: 0 5px; + min-height: 38px; + margin: 0; + padding: 0 6px; color: var(--bp-sidebar-muted); border: 1px solid transparent; border-radius: 6px; + background: transparent; + box-shadow: none; font-size: 11px; } -.bp-mode-switch .q-tabs__arrow { - display: none !important; -} - -.bp-mode-switch .q-tab__indicator { - display: none !important; -} - -.bp-mode-switch .q-tab__content { - flex-direction: row; +.bp-mode-switch .bp-mode-button .q-btn__content { justify-content: center; gap: 6px; + flex-wrap: nowrap; + white-space: nowrap; +} + +.bp-mode-switch .bp-mode-button .q-icon { + font-size: 18px; } -.bp-mode-switch .q-tab--active { +.bp-mode-switch .bp-mode-button--active { color: #dbeafe; border-color: rgba(96, 165, 250, .18); background: rgba(37, 99, 235, .18); } -.bp-mode-switch .q-tab--active .q-icon { +.bp-mode-switch .bp-mode-button--active .q-icon { color: #60a5fa; } diff --git a/src/blueprinting/workbench/nicegui_ui.py b/src/blueprinting/workbench/nicegui_ui.py index 1e39723..2f613aa 100644 --- a/src/blueprinting/workbench/nicegui_ui.py +++ b/src/blueprinting/workbench/nicegui_ui.py @@ -629,34 +629,56 @@ def build(self) -> None: ui.label("Blueprinting").classes("bp-brand-title") ui.label("硬件架构探索工作台").classes("bp-brand-subtitle") ui.separator().classes("bp-sidebar-rule") - ui.label("ANALYSIS LENS").classes("bp-sidebar-kicker") + ui.label("WORKBENCH LENS").classes("bp-sidebar-kicker") ui.label("观察尺度").classes("bp-sidebar-title") - with ( - ui.tabs(value=self.mode.value, on_change=self._change_mode) - .props("dense no-caps indicator-color=transparent") - .classes("bp-mode-switch w-full mt-2") - .mark("mode-switch") as self.mode_switch - ): - self.analysis_mode_tab = ui.tab( - WorkbenchMode.ANALYSIS.value, - "单点剖析", - icon="query_stats", - ).mark("mode-analysis") - self.sweep_mode_tab = ui.tab( - WorkbenchMode.SWEEP.value, - "批量探索", - icon="scatter_plot", - ).mark("mode-sweep") - self.evidence_mode_tab = ui.tab( - WorkbenchMode.EVIDENCE.value, - "性能证据", - icon="monitoring", - ).mark("mode-evidence") - self.float_mode_tab = ui.tab( - WorkbenchMode.FLOAT.value, - "浮点分析", - icon="calculate", - ).mark("mode-float") + with ui.element("div").classes("bp-mode-switch w-full mt-2").mark("mode-switch") as self.mode_switch: + self.analysis_mode_tab = ( + ui.button( + "单点剖析", + icon="query_stats", + on_click=partial(self._select_mode, WorkbenchMode.ANALYSIS), + ) + .props("flat no-caps") + .classes("bp-mode-button") + .mark("mode-analysis") + ) + self.sweep_mode_tab = ( + ui.button( + "批量探索", + icon="scatter_plot", + on_click=partial(self._select_mode, WorkbenchMode.SWEEP), + ) + .props("flat no-caps") + .classes("bp-mode-button") + .mark("mode-sweep") + ) + self.evidence_mode_tab = ( + ui.button( + "性能证据", + icon="monitoring", + on_click=partial(self._select_mode, WorkbenchMode.EVIDENCE), + ) + .props("flat no-caps") + .classes("bp-mode-button") + .mark("mode-evidence") + ) + self.float_mode_tab = ( + ui.button( + "浮点分析", + icon="calculate", + on_click=partial(self._select_mode, WorkbenchMode.FLOAT), + ) + .props("flat no-caps") + .classes("bp-mode-button") + .mark("mode-float") + ) + self._mode_buttons = { + WorkbenchMode.ANALYSIS: self.analysis_mode_tab, + WorkbenchMode.SWEEP: self.sweep_mode_tab, + WorkbenchMode.EVIDENCE: self.evidence_mode_tab, + WorkbenchMode.FLOAT: self.float_mode_tab, + } + self._sync_mode_buttons() ui.separator().classes("bp-sidebar-rule") self.sidebar_controls = ui.column().classes("w-full gap-2") self.sidebar_summary = ui.column().classes("bp-sidebar-summary w-full gap-2") @@ -716,11 +738,11 @@ def _build_legacy_dialog(self) -> None: ui.link("打开 localhost:8501", "http://127.0.0.1:8501", new_tab=True).classes("text-secondary") ui.button("关闭", on_click=self.legacy_dialog.close).props("flat no-caps") - def _change_mode(self, event: Any) -> None: - if self.busy: - self.mode_switch.set_value(self.mode.value) + def _select_mode(self, mode: WorkbenchMode) -> None: + if self.busy or mode is self.mode: return - self.mode = WorkbenchMode(str(event.value)) + self.mode = mode + self._sync_mode_buttons() self.local_error = None self.config_dialog.close() if self.form is not None and self.mode in {WorkbenchMode.ANALYSIS, WorkbenchMode.SWEEP}: @@ -728,6 +750,13 @@ def _change_mode(self, event: Any) -> None: self._render_sidebar_controls() self._render_workspace() + def _sync_mode_buttons(self) -> None: + for mode, button in self._mode_buttons.items(): + if mode is self.mode: + button.classes(add="bp-mode-button--active") + else: + button.classes(remove="bp-mode-button--active") + def _ensure_form(self, host: Any) -> None: if self.form is None: with host: @@ -1980,7 +2009,7 @@ async def _open_selected_batch_case(self) -> None: return self.form.load_point_parallelism(int(row["tp"]), int(row["pp"]), int(row["dp"])) self.mode = WorkbenchMode.ANALYSIS - self.mode_switch.set_value(self.mode.value) + self._sync_mode_buttons() self.form.set_mode(self.mode) self.local_error = None self._render_sidebar_controls() diff --git a/tests/workbench/test_nicegui_workbench.py b/tests/workbench/test_nicegui_workbench.py index 69954ac..65828d9 100644 --- a/tests/workbench/test_nicegui_workbench.py +++ b/tests/workbench/test_nicegui_workbench.py @@ -178,9 +178,10 @@ def test_workbench_cli_defaults_to_local_only() -> None: def test_sidebar_mode_switch_uses_a_non_scrolling_two_by_two_grid() -> None: assert "grid-template-columns: repeat(2, minmax(0, 1fr))" in WORKBENCH_CSS - assert ".bp-mode-switch .q-tabs__arrow" in WORKBENCH_CSS - assert ".bp-mode-switch .q-tab__indicator" in WORKBENCH_CSS - assert "display: none !important" in WORKBENCH_CSS + assert ".bp-mode-switch .bp-mode-button" in WORKBENCH_CSS + assert ".bp-mode-switch .bp-mode-button--active" in WORKBENCH_CSS + assert "q-tabs__arrow" not in WORKBENCH_CSS + assert "q-tab__indicator" not in WORKBENCH_CSS def test_workbench_cli_accepts_server_overrides() -> None: From 7afbf10e30f977f33b1e4608c3a0b28d6451398a Mon Sep 17 00:00:00 2001 From: Reiase Date: Sun, 9 Aug 2026 23:17:39 +0800 Subject: [PATCH 3/6] Keep workbench mode icons inside controls --- src/blueprinting/workbench/nicegui_theme.py | 29 +++++-- src/blueprinting/workbench/nicegui_ui.py | 96 ++++++++++----------- tests/workbench/test_nicegui_workbench.py | 2 + 3 files changed, 69 insertions(+), 58 deletions(-) diff --git a/src/blueprinting/workbench/nicegui_theme.py b/src/blueprinting/workbench/nicegui_theme.py index 1061a31..4fcf8f2 100644 --- a/src/blueprinting/workbench/nicegui_theme.py +++ b/src/blueprinting/workbench/nicegui_theme.py @@ -134,28 +134,40 @@ } .bp-mode-switch .bp-mode-button { + display: flex; + align-items: center; + justify-content: center; + gap: 7px; width: 100%; min-width: 0; min-height: 38px; margin: 0; padding: 0 6px; + appearance: none; color: var(--bp-sidebar-muted); border: 1px solid transparent; border-radius: 6px; background: transparent; box-shadow: none; + cursor: pointer; + font-family: inherit; font-size: 11px; + line-height: 1; } -.bp-mode-switch .bp-mode-button .q-btn__content { - justify-content: center; - gap: 6px; - flex-wrap: nowrap; +.bp-mode-switch .bp-mode-label { + min-width: 0; white-space: nowrap; } -.bp-mode-switch .bp-mode-button .q-icon { +.bp-mode-switch .bp-mode-icon { + position: static; + flex: 0 0 auto; + width: 18px; + height: 18px; + margin: 0; font-size: 18px; + line-height: 18px; } .bp-mode-switch .bp-mode-button--active { @@ -164,10 +176,15 @@ background: rgba(37, 99, 235, .18); } -.bp-mode-switch .bp-mode-button--active .q-icon { +.bp-mode-switch .bp-mode-button--active .bp-mode-icon { color: #60a5fa; } +.bp-mode-switch .bp-mode-button:disabled { + cursor: default; + opacity: .55; +} + .bp-sidebar-rule { margin: 11px 0; background: var(--bp-sidebar-line) !important; diff --git a/src/blueprinting/workbench/nicegui_ui.py b/src/blueprinting/workbench/nicegui_ui.py index 2f613aa..cce34f2 100644 --- a/src/blueprinting/workbench/nicegui_ui.py +++ b/src/blueprinting/workbench/nicegui_ui.py @@ -632,45 +632,29 @@ def build(self) -> None: ui.label("WORKBENCH LENS").classes("bp-sidebar-kicker") ui.label("观察尺度").classes("bp-sidebar-title") with ui.element("div").classes("bp-mode-switch w-full mt-2").mark("mode-switch") as self.mode_switch: - self.analysis_mode_tab = ( - ui.button( - "单点剖析", - icon="query_stats", - on_click=partial(self._select_mode, WorkbenchMode.ANALYSIS), - ) - .props("flat no-caps") - .classes("bp-mode-button") - .mark("mode-analysis") + self.analysis_mode_tab = self._build_mode_button( + WorkbenchMode.ANALYSIS, + "单点剖析", + "query_stats", + "mode-analysis", ) - self.sweep_mode_tab = ( - ui.button( - "批量探索", - icon="scatter_plot", - on_click=partial(self._select_mode, WorkbenchMode.SWEEP), - ) - .props("flat no-caps") - .classes("bp-mode-button") - .mark("mode-sweep") + self.sweep_mode_tab = self._build_mode_button( + WorkbenchMode.SWEEP, + "批量探索", + "scatter_plot", + "mode-sweep", ) - self.evidence_mode_tab = ( - ui.button( - "性能证据", - icon="monitoring", - on_click=partial(self._select_mode, WorkbenchMode.EVIDENCE), - ) - .props("flat no-caps") - .classes("bp-mode-button") - .mark("mode-evidence") + self.evidence_mode_tab = self._build_mode_button( + WorkbenchMode.EVIDENCE, + "性能证据", + "monitoring", + "mode-evidence", ) - self.float_mode_tab = ( - ui.button( - "浮点分析", - icon="calculate", - on_click=partial(self._select_mode, WorkbenchMode.FLOAT), - ) - .props("flat no-caps") - .classes("bp-mode-button") - .mark("mode-float") + self.float_mode_tab = self._build_mode_button( + WorkbenchMode.FLOAT, + "浮点分析", + "calculate", + "mode-float", ) self._mode_buttons = { WorkbenchMode.ANALYSIS: self.analysis_mode_tab, @@ -750,6 +734,19 @@ def _select_mode(self, mode: WorkbenchMode) -> None: self._render_sidebar_controls() self._render_workspace() + def _build_mode_button(self, mode: WorkbenchMode, label: str, icon: str, marker: str) -> Any: + button = ( + ui.element("button") + .props("type=button") + .classes("bp-mode-button") + .mark(marker) + .on("click", lambda _: self._select_mode(mode)) + ) + with button: + ui.icon(icon).classes("bp-mode-icon") + ui.label(label).classes("bp-mode-label") + return button + def _sync_mode_buttons(self) -> None: for mode, button in self._mode_buttons.items(): if mode is self.mode: @@ -757,6 +754,13 @@ def _sync_mode_buttons(self) -> None: else: button.classes(remove="bp-mode-button--active") + def _set_mode_buttons_busy(self, busy: bool) -> None: + for button in self._mode_buttons.values(): + if busy: + button.props(add="disabled") + else: + button.props(remove="disabled") + def _ensure_form(self, host: Any) -> None: if self.form is None: with host: @@ -1273,10 +1277,7 @@ async def run_analysis(self) -> None: self.busy = True self.form.set_busy(True) self._set_quick_controls_busy(True) - self.analysis_mode_tab.disable() - self.sweep_mode_tab.disable() - self.evidence_mode_tab.disable() - self.float_mode_tab.disable() + self._set_mode_buttons_busy(True) self.config_dialog.close() self._render_workspace() try: @@ -1293,10 +1294,7 @@ async def run_analysis(self) -> None: self.busy = False self.form.set_busy(False) self._set_quick_controls_busy(False) - self.analysis_mode_tab.enable() - self.sweep_mode_tab.enable() - self.evidence_mode_tab.enable() - self.float_mode_tab.enable() + self._set_mode_buttons_busy(False) self._render_workspace() async def run_sweep(self) -> None: @@ -1316,10 +1314,7 @@ async def run_sweep(self) -> None: self.busy = True self.form.set_busy(True) self._set_quick_controls_busy(True) - self.analysis_mode_tab.disable() - self.sweep_mode_tab.disable() - self.evidence_mode_tab.disable() - self.float_mode_tab.disable() + self._set_mode_buttons_busy(True) self.config_dialog.close() self.progress_timer.activate() self._render_workspace() @@ -1346,10 +1341,7 @@ def on_progress(completed: int, total: int) -> None: self.busy = False self.form.set_busy(False) self._set_quick_controls_busy(False) - self.analysis_mode_tab.enable() - self.sweep_mode_tab.enable() - self.evidence_mode_tab.enable() - self.float_mode_tab.enable() + self._set_mode_buttons_busy(False) self._render_workspace() def _render_loading(self) -> None: diff --git a/tests/workbench/test_nicegui_workbench.py b/tests/workbench/test_nicegui_workbench.py index 65828d9..c68329c 100644 --- a/tests/workbench/test_nicegui_workbench.py +++ b/tests/workbench/test_nicegui_workbench.py @@ -180,6 +180,8 @@ def test_sidebar_mode_switch_uses_a_non_scrolling_two_by_two_grid() -> None: assert "grid-template-columns: repeat(2, minmax(0, 1fr))" in WORKBENCH_CSS assert ".bp-mode-switch .bp-mode-button" in WORKBENCH_CSS assert ".bp-mode-switch .bp-mode-button--active" in WORKBENCH_CSS + assert ".bp-mode-switch .bp-mode-icon" in WORKBENCH_CSS + assert "position: static" in WORKBENCH_CSS assert "q-tabs__arrow" not in WORKBENCH_CSS assert "q-tab__indicator" not in WORKBENCH_CSS From 2604fc499641212c22aafcedbee5f405c6669e70 Mon Sep 17 00:00:00 2001 From: Reiase Date: Sun, 9 Aug 2026 23:19:16 +0800 Subject: [PATCH 4/6] Use stable icon for evidence mode --- src/blueprinting/workbench/nicegui_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueprinting/workbench/nicegui_ui.py b/src/blueprinting/workbench/nicegui_ui.py index cce34f2..8155131 100644 --- a/src/blueprinting/workbench/nicegui_ui.py +++ b/src/blueprinting/workbench/nicegui_ui.py @@ -647,7 +647,7 @@ def build(self) -> None: self.evidence_mode_tab = self._build_mode_button( WorkbenchMode.EVIDENCE, "性能证据", - "monitoring", + "speed", "mode-evidence", ) self.float_mode_tab = self._build_mode_button( From c350ab42675896f76f8e40f4c055c92d42293eed Mon Sep 17 00:00:00 2001 From: Reiase Date: Sun, 9 Aug 2026 23:26:37 +0800 Subject: [PATCH 5/6] Redesign performance evidence dashboard --- src/blueprinting/workbench/evidence_lab.py | 293 +++++++++++++------- src/blueprinting/workbench/nicegui_theme.py | 186 +++++++++++++ tests/workbench/test_evidence_lab.py | 11 + tests/workbench/test_nicegui_workbench.py | 4 +- 4 files changed, 398 insertions(+), 96 deletions(-) diff --git a/src/blueprinting/workbench/evidence_lab.py b/src/blueprinting/workbench/evidence_lab.py index b2f244a..9104602 100644 --- a/src/blueprinting/workbench/evidence_lab.py +++ b/src/blueprinting/workbench/evidence_lab.py @@ -39,6 +39,20 @@ class EvidenceLabData: manifest: dict[str, Any] +@dataclass(frozen=True) +class ComparisonSummary: + point_count: int + total_samples: int + token_min: int + token_max: int + peak_measured_tops: float + mean_absolute_error_percent: float + worst_error_percent: float + worst_error_tokens: int + bias: str + bottlenecks: tuple[str, ...] + + def load_vidur_poc_evidence(catalog: ConfigCatalog, root: Path | None = None) -> EvidenceLabData: """Load the explicitly pinned repository/package Vidur validation slice.""" @@ -166,6 +180,62 @@ def throughput_curve_options(report: CostCurveReport) -> dict[str, Any]: } +def relative_error_chart_options(report: CostCurveReport) -> dict[str, Any]: + return { + "backgroundColor": "transparent", + "animationDuration": 250, + "grid": {"left": 58, "right": 18, "top": 24, "bottom": 44}, + "tooltip": {"trigger": "axis"}, + "xAxis": {"type": "category", "name": "tokens", "data": [point.x for point in report.points]}, + "yAxis": {"type": "value", "name": "error %"}, + "series": [ + { + "name": "Roofline error", + "type": "bar", + "barMaxWidth": 34, + "data": [ + { + "value": point.relative_error_percent, + "itemStyle": {"color": "#d97706" if (point.relative_error_percent or 0) < 0 else "#0891b2"}, + } + for point in report.points + ], + "markLine": { + "silent": True, + "symbol": "none", + "lineStyle": {"color": "#94a3b8", "type": "dashed"}, + "data": [{"yAxis": 0}], + }, + } + ], + } + + +def summarize_comparison(report: CostCurveReport) -> ComparisonSummary: + if not report.points: + raise ValueError("comparison report must contain at least one point") + errors = tuple(point.relative_error_percent or 0.0 for point in report.points) + worst = max(report.points, key=lambda point: abs(point.relative_error_percent or 0.0)) + if all(error < 0 for error in errors): + bias = "Roofline 全部偏乐观" + elif all(error > 0 for error in errors): + bias = "Roofline 全部偏保守" + else: + bias = "Roofline 偏差方向不一致" + return ComparisonSummary( + point_count=len(report.points), + total_samples=sum(point.sample_count for point in report.points), + token_min=min(point.x for point in report.points), + token_max=max(point.x for point in report.points), + peak_measured_tops=max(point.measured_teraops_per_second for point in report.points), + mean_absolute_error_percent=sum(abs(error) for error in errors) / len(errors), + worst_error_percent=worst.relative_error_percent or 0.0, + worst_error_tokens=worst.x, + bias=bias, + bottlenecks=tuple(sorted({point.analytical_bottleneck for point in report.points})), + ) + + def curve_rows(report: CostCurveReport) -> list[dict[str, Any]]: return [ { @@ -204,17 +274,24 @@ def build(self) -> None: self.semantic_operation = semantics[0] except (FileNotFoundError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error: self.error = f"{type(error).__name__}: {error}" - with ui.column().classes("w-full gap-4").mark("evidence-lab"): + with ui.column().classes("w-full gap-3").mark("evidence-lab"): with ( ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), + ui.element("div").classes("bp-evidence-toolbar"), ): - ui.label("EVIDENCE LENS").classes("bp-kicker") - ui.label("性能证据实验室").classes("bp-result-title mt-1") - ui.label( - "浏览带 provenance 的性能记录,并在相同 workload facts 上比较实测数据与解析模型。" - ).classes("bp-card-copy mt-1") - ui.label("只读 PoC · exact selectors · no interpolation").classes("bp-fidelity-tag bp-mono mt-3") + self._render_heading() + if self.error is None and self.data is not None: + semantics = comparable_gemm_semantics(self.data.database) + with ui.element("div").classes("bp-evidence-toolbar-controls"): + ui.select( + {item: _SEMANTIC_LABELS.get(item, item) for item in semantics}, + value=self.semantic_operation, + label="对比 Primitive", + on_change=self._semantic_changed, + ).props("outlined dense").classes("bp-evidence-selector").mark( + "evidence-semantic-operation" + ) + ui.label("EXACT · NO INTERPOLATION").classes("bp-fidelity-tag bp-mono") if self.error is not None or self.data is None: with ( ui.element("section").classes("bp-evidence-surface"), @@ -223,24 +300,16 @@ def build(self) -> None: ui.label("证据数据不可用").classes("bp-card-title") ui.label(self.error or "unknown evidence loading error").classes("bp-card-copy bp-mono") return - semantics = comparable_gemm_semantics(self.data.database) - with ( - ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), - ): - ui.label("对比控制").classes("bp-card-title") - ui.label("首版只比较能够重建精确 M/N/K 的 GEMM primitive;其他 operation 仍出现在覆盖目录。 ").classes( - "bp-card-copy" - ) - ui.select( - {item: _SEMANTIC_LABELS.get(item, item) for item in semantics}, - value=self.semantic_operation, - label="GEMM semantic operation", - on_change=self._semantic_changed, - ).props("outlined dense").classes("w-80 max-w-full mt-3").mark("evidence-semantic-operation") - self.content = ui.column().classes("w-full gap-4") + self.content = ui.column().classes("w-full gap-3") self._render_content() + @staticmethod + def _render_heading() -> None: + with ui.column().classes("bp-evidence-heading gap-1"): + ui.label("EVIDENCE LENS").classes("bp-kicker") + ui.label("性能证据实验室").classes("bp-result-title") + ui.label("同一 workload facts 下比较实测证据与解析模型。 ").classes("bp-card-copy") + def _semantic_changed(self, event: Any) -> None: self.semantic_operation = str(event.value) self._render_content() @@ -257,93 +326,124 @@ def _render_content(self) -> None: ) summary = self.data.summary source = self.data.manifest["source"] + comparison = summarize_comparison(report) + fixed = report.fixed_selectors with self.content: - with ( - ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), - ): - ui.label("Evidence catalog").classes("bp-card-title") - with ui.element("div").classes("bp-chain-stats mt-3"): - for label, value in ( - ("Database", summary.name), - ("Records", str(summary.record_count)), - ("Hardware", ", ".join(summary.hardware)), - ("Datatype", ", ".join(summary.datatypes)), - ("Method", summary.coverage[0].method), - ("Source revision", summary.source_revision[:12]), + with ui.element("section").classes("bp-evidence-surface"): + with ui.element("div").classes("bp-evidence-section bp-evidence-summary-head"): + with ui.row().classes("items-center gap-2 no-wrap"): + ui.label(_SEMANTIC_LABELS.get(self.semantic_operation, self.semantic_operation)).classes( + "bp-card-title" + ) + ui.label(f"{comparison.point_count} exact points").classes("bp-fidelity-tag bp-mono") + ui.label( + f"Phi-2 · {report.hardware} · {report.datatype} · " + f"M={comparison.token_min}–{comparison.token_max} tokens · " + f"H={fixed['hidden_size']} · FFN={fixed['feedforward_size']}" + ).classes("bp-card-copy bp-mono") + with ui.element("div").classes("bp-evidence-metrics"): + for label, value, detail in ( + ("EXACT POINTS", str(comparison.point_count), f"{comparison.total_samples} total samples"), + ("TOKEN DOMAIN", f"{comparison.token_min}–{comparison.token_max}", "discrete selectors"), + ("PEAK MEASURED", f"{comparison.peak_measured_tops:.1f} TOPS", "within imported points"), + ("MEAN |ERROR|", f"{comparison.mean_absolute_error_percent:.1f}%", "roofline vs measured"), + ("BOUND", " / ".join(comparison.bottlenecks), "analytical bottleneck"), ): - with ui.column().classes("gap-0"): + with ui.element("div").classes("bp-evidence-metric"): ui.label(label).classes("bp-summary-label") - ui.label(value).classes("bp-card-copy bp-mono") - ui.label( - f"Source: {source['repository']} · License: {source['license']} · DB revision: {summary.revision}" - ).classes("bp-card-copy bp-mono mt-3") + ui.label(value).classes("bp-evidence-metric-value") + ui.label(detail).classes("bp-metric-detail") + with ui.element("div").classes("bp-evidence-insight"): + ui.icon("warning_amber", size="19px") + ui.label( + f"{comparison.bias};最大偏差 {comparison.worst_error_percent:+.1f}% " + f"出现在 {comparison.worst_error_tokens} tokens。当前点数不足以支持插值。" + ).classes("bp-card-copy") with ui.element("section").classes("bp-evidence-surface"): - with ui.element("div").classes("bp-evidence-section"): - ui.label("Operation coverage").classes("bp-card-title") - ui.label("这里展示已导入的 exact records,不代表未覆盖点可插值。 ").classes("bp-card-copy") - ui.echart(coverage_chart_options(summary), renderer="svg").classes("w-full h-80") - with ui.element("div").classes("bp-evidence-section"): + with ui.element("div").classes("bp-evidence-section bp-evidence-compact-head"): + ui.label("Measured vs analytical").classes("bp-card-title") + ui.label("散点是 Vidur exact records;连线只帮助阅读,不表示中间点已有证据。 ").classes("bp-card-copy") + with ui.element("div").classes("bp-evidence-chart-grid"): + with ui.element("section").classes("bp-evidence-chart"): + ui.label("Latency · µs").classes("bp-section-title") + ui.echart(latency_curve_options(report), renderer="svg").classes("w-full bp-evidence-chart-canvas") + with ui.element("section").classes("bp-evidence-chart"): + ui.label("Effective throughput · TOPS").classes("bp-section-title") + ui.echart(throughput_curve_options(report), renderer="svg").classes( + "w-full bp-evidence-chart-canvas" + ) + with ui.element("section").classes("bp-evidence-chart"): + ui.label("Roofline relative error").classes("bp-section-title") + ui.echart(relative_error_chart_options(report), renderer="svg").classes( + "w-full bp-evidence-chart-canvas" + ) + with ui.element("div").classes("bp-evidence-section bp-evidence-table-section"): ui.aggrid( { "columnDefs": [ - {"headerName": "Semantic operation", "field": "semantic_operation", "pinned": "left"}, - {"headerName": "Family", "field": "operation"}, - {"headerName": "Records", "field": "records", "type": "numericColumn"}, - {"headerName": "Hardware", "field": "hardware"}, - {"headerName": "Datatype", "field": "datatype"}, - {"headerName": "Method", "field": "method"}, - {"headerName": "Controlled selectors", "field": "axes", "flex": 1}, - ], - "rowData": coverage_rows(summary), - "defaultColDef": {"sortable": True, "filter": True, "resizable": True}, - }, - theme="quartz", - ).classes("w-full").style("height: 360px") - with ui.element("section").classes("bp-evidence-surface"): - with ui.element("div").classes("bp-evidence-section"): - ui.label(f"Characteristic curve · {_SEMANTIC_LABELS.get(self.semantic_operation)}").classes( - "bp-card-title" - ) - ui.label("蓝色散点是 Vidur exact records;橙色线是同一 workload facts 的解析 Roofline。 ").classes( - "bp-card-copy" - ) - with ui.element("div").classes("bp-evidence-section bp-analysis-split"): - with ui.element("section").classes("bp-evidence-block"): - ui.label("Latency").classes("bp-section-title") - ui.echart(latency_curve_options(report), renderer="svg").classes("w-full h-80") - with ui.element("section").classes("bp-evidence-block"): - ui.label("Effective throughput").classes("bp-section-title") - ui.echart(throughput_curve_options(report), renderer="svg").classes("w-full h-80") - with ui.element("div").classes("bp-evidence-section"): - ui.aggrid( - { - "columnDefs": [ - {"headerName": "Tokens", "field": "num_tokens", "pinned": "left"}, + {"headerName": "Tokens", "field": "num_tokens", "pinned": "left", "width": 90}, {"headerName": "Measured µs", "field": "measured_us", "type": "numericColumn"}, {"headerName": "Roofline µs", "field": "roofline_us", "type": "numericColumn"}, - {"headerName": "Model error %", "field": "relative_error_percent", "type": "numericColumn"}, + {"headerName": "Error %", "field": "relative_error_percent", "type": "numericColumn"}, {"headerName": "Measured TOPS", "field": "measured_tops", "type": "numericColumn"}, - {"headerName": "Roofline TOPS", "field": "roofline_tops", "type": "numericColumn"}, - {"headerName": "Bottleneck", "field": "bottleneck"}, + {"headerName": "Bound", "field": "bottleneck"}, {"headerName": "Samples", "field": "samples", "type": "numericColumn"}, - {"headerName": "Query digest", "field": "query_digest", "flex": 1}, - {"headerName": "Raw record IDs", "field": "record_ids", "flex": 1}, + {"headerName": "Record IDs", "field": "record_ids", "flex": 1}, + {"headerName": "Query digest", "field": "query_digest", "hide": True}, ], "rowData": curve_rows(report), "defaultColDef": {"sortable": True, "filter": True, "resizable": True}, }, theme="quartz", - ).classes("w-full").style("height: 300px").mark("evidence-query-inspector") - with ( - ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), - ): - ui.label("适用边界").classes("bp-card-title") - for limitation in report.limitations: - with ui.row().classes("items-start gap-2 no-wrap"): - ui.icon("info", size="18px", color="secondary") - ui.label(limitation).classes("bp-card-copy") + ).classes("w-full").style("height: 186px").mark("evidence-query-inspector") + with ui.element("section").classes("bp-evidence-surface"): + with ui.element("div").classes("bp-evidence-section bp-evidence-compact-head"): + ui.label("Operation coverage").classes("bp-card-title") + ui.label("导入记录的离散覆盖;不暗示未测 selector 可插值。 ").classes("bp-card-copy") + with ui.element("div").classes("bp-evidence-coverage-grid"): + with ui.element("section").classes("bp-evidence-chart"): + ui.echart(coverage_chart_options(summary), renderer="svg").classes( + "w-full bp-evidence-coverage-canvas" + ) + with ui.element("section").classes("bp-evidence-coverage-table"): + ui.aggrid( + { + "columnDefs": [ + {"headerName": "Operation", "field": "semantic_operation", "pinned": "left"}, + {"headerName": "Family", "field": "operation"}, + {"headerName": "Records", "field": "records", "type": "numericColumn"}, + {"headerName": "Selectors", "field": "axes", "flex": 1}, + ], + "rowData": coverage_rows(summary), + "defaultColDef": {"sortable": True, "filter": True, "resizable": True}, + }, + theme="quartz", + ).classes("w-full").style("height: 276px") + with ui.element("section").classes("bp-evidence-surface bp-evidence-footer-grid"): + with ui.element("div").classes("bp-evidence-section"): + ui.label("Evidence catalog").classes("bp-card-title") + with ui.element("div").classes("bp-evidence-catalog-grid"): + for label, value in ( + ("Database", summary.name), + ("Records", str(summary.record_count)), + ("Hardware", ", ".join(summary.hardware)), + ("Datatype", ", ".join(summary.datatypes)), + ("Method", summary.coverage[0].method), + ("Source revision", summary.source_revision[:12]), + ): + with ui.column().classes("gap-0 min-w-0"): + ui.label(label).classes("bp-summary-label") + ui.label(value).classes("bp-card-copy bp-mono") + ui.link("Vidur source", source["repository"], new_tab=True).classes("bp-card-copy") + ui.label(f"License {source['license']} · DB {summary.revision[:16]}").classes( + "bp-card-copy bp-mono" + ) + with ui.element("div").classes("bp-evidence-section"): + ui.label("适用边界").classes("bp-card-title") + for limitation in report.limitations: + with ui.row().classes("items-start gap-2 no-wrap"): + ui.icon("info", size="17px", color="secondary") + ui.label(limitation).classes("bp-card-copy") def _default_vidur_root() -> Path: @@ -360,10 +460,13 @@ def _default_vidur_root() -> Path: __all__ = [ "EvidenceLabData", "EvidenceLabPanel", + "ComparisonSummary", "coverage_chart_options", "coverage_rows", "curve_rows", "latency_curve_options", "load_vidur_poc_evidence", + "relative_error_chart_options", + "summarize_comparison", "throughput_curve_options", ] diff --git a/src/blueprinting/workbench/nicegui_theme.py b/src/blueprinting/workbench/nicegui_theme.py index 4fcf8f2..5c982ef 100644 --- a/src/blueprinting/workbench/nicegui_theme.py +++ b/src/blueprinting/workbench/nicegui_theme.py @@ -801,6 +801,141 @@ border-top: 1px solid var(--bp-line); } +.bp-evidence-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 14px 16px; +} + +.bp-evidence-heading { + min-width: 260px; +} + +.bp-evidence-toolbar-controls { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + min-width: 0; +} + +.bp-evidence-selector { + width: 320px; + max-width: 42vw; +} + +.bp-evidence-selector .q-field__control { + min-height: 42px; +} + +.bp-evidence-summary-head, +.bp-evidence-compact-head { + gap: 6px; + padding-top: 13px; + padding-bottom: 11px; +} + +.bp-evidence-metrics { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + border-top: 1px solid var(--bp-line); + border-bottom: 1px solid var(--bp-line); +} + +.bp-evidence-metric { + min-width: 0; + padding: 13px 16px; + border-left: 1px solid var(--bp-line); +} + +.bp-evidence-metric:first-child { + border-left: 0; +} + +.bp-evidence-metric-value { + margin-top: 5px; + overflow: hidden; + color: var(--bp-text); + font-size: 19px; + font-weight: 680; + font-variant-numeric: tabular-nums; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bp-evidence-insight { + display: flex; + align-items: center; + gap: 9px; + margin: 10px 16px 13px; + padding: 9px 11px; + color: #92400e; + border-left: 3px solid #d97706; + background: #fffbeb; +} + +.bp-evidence-chart-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border-top: 1px solid var(--bp-line); +} + +.bp-evidence-chart { + min-width: 0; + padding: 12px 14px 4px; + border-left: 1px solid var(--bp-line); +} + +.bp-evidence-chart:first-child { + border-left: 0; +} + +.bp-evidence-chart-canvas { + height: 270px; +} + +.bp-evidence-table-section { + padding-top: 10px; + padding-bottom: 12px; +} + +.bp-evidence-coverage-grid { + display: grid; + grid-template-columns: minmax(360px, .8fr) minmax(0, 1.2fr); + border-top: 1px solid var(--bp-line); +} + +.bp-evidence-coverage-canvas { + height: 276px; +} + +.bp-evidence-coverage-table { + min-width: 0; + padding: 10px 12px; + border-left: 1px solid var(--bp-line); +} + +.bp-evidence-footer-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +.bp-evidence-footer-grid .bp-evidence-section + .bp-evidence-section { + border-top: 0; + border-left: 1px solid var(--bp-line); +} + +.bp-evidence-catalog-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px 16px; + padding-top: 10px; + border-top: 1px solid var(--bp-line); +} + .bp-metric-grid { width: 100%; display: grid; @@ -1240,6 +1375,30 @@ .bp-chain-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } + + .bp-evidence-chart-grid { + grid-template-columns: 1fr; + } + + .bp-evidence-chart { + border-top: 1px solid var(--bp-line); + border-left: 0; + } + + .bp-evidence-chart:first-child { + border-top: 0; + } + + .bp-evidence-coverage-grid, + .bp-evidence-footer-grid { + grid-template-columns: 1fr; + } + + .bp-evidence-coverage-table, + .bp-evidence-footer-grid .bp-evidence-section + .bp-evidence-section { + border-top: 1px solid var(--bp-line); + border-left: 0; + } } @media (max-width: 980px) { @@ -1399,6 +1558,33 @@ padding: 13px; } + .bp-evidence-toolbar, + .bp-evidence-toolbar-controls { + align-items: stretch; + flex-direction: column; + } + + .bp-evidence-selector { + width: 100%; + max-width: none; + } + + .bp-evidence-metrics { + grid-template-columns: 1fr 1fr; + } + + .bp-evidence-metric:nth-child(odd) { + border-left: 0; + } + + .bp-evidence-metric:nth-child(n + 3) { + border-top: 1px solid var(--bp-line); + } + + .bp-evidence-catalog-grid { + grid-template-columns: 1fr 1fr; + } + .bp-derivation-details .bp-card { padding: 0; } diff --git a/tests/workbench/test_evidence_lab.py b/tests/workbench/test_evidence_lab.py index a445766..e5d5afb 100644 --- a/tests/workbench/test_evidence_lab.py +++ b/tests/workbench/test_evidence_lab.py @@ -8,6 +8,8 @@ curve_rows, latency_curve_options, load_vidur_poc_evidence, + relative_error_chart_options, + summarize_comparison, throughput_curve_options, ) @@ -19,11 +21,20 @@ def test_evidence_lab_builds_catalog_and_comparison_presentations() -> None: coverage = coverage_rows(data.summary) latency = latency_curve_options(report) throughput = throughput_curve_options(report) + relative_error = relative_error_chart_options(report) + comparison = summarize_comparison(report) rows = curve_rows(report) assert len(coverage) == 9 assert {series["name"] for series in latency["series"]} == {"Vidur measured", "Analytical roofline"} assert {series["name"] for series in throughput["series"]} == {"Vidur effective", "Roofline effective"} + assert relative_error["series"][0]["name"] == "Roofline error" + assert comparison.point_count == 2 + assert comparison.total_samples == 2 + assert comparison.token_min == 1 + assert comparison.token_max == 128 + assert comparison.bias == "Roofline 全部偏乐观" + assert comparison.worst_error_tokens == 128 assert [row["num_tokens"] for row in rows] == [1, 128] assert all(row["query_digest"] and row["record_ids"] for row in rows) assert coverage_chart_options(data.summary)["series"][0]["name"] == "Imported records" diff --git a/tests/workbench/test_nicegui_workbench.py b/tests/workbench/test_nicegui_workbench.py index c68329c..393ccee 100644 --- a/tests/workbench/test_nicegui_workbench.py +++ b/tests/workbench/test_nicegui_workbench.py @@ -161,7 +161,9 @@ async def test_nicegui_evidence_lab_compares_measured_and_analytical_curves( await user.should_see("性能证据实验室") await user.should_see("Evidence catalog") await user.should_see("Operation coverage") - await user.should_see("Characteristic curve") + await user.should_see("Measured vs analytical") + await user.should_see("Roofline relative error") + await user.should_see("Roofline 全部偏乐观") await user.should_see("Vidur exact records") await user.should_see(marker="evidence-semantic-operation") await user.should_see(marker="evidence-query-inspector") From 771ae7c4d961f73233320aefc99a42c0b889c33d Mon Sep 17 00:00:00 2001 From: Reiase Date: Sun, 9 Aug 2026 23:31:06 +0800 Subject: [PATCH 6/6] Redesign floating point analysis layout --- src/blueprinting/workbench/float_analysis.py | 245 ++++++++++++------- src/blueprinting/workbench/nicegui_theme.py | 222 +++++++++++++++++ 2 files changed, 376 insertions(+), 91 deletions(-) diff --git a/src/blueprinting/workbench/float_analysis.py b/src/blueprinting/workbench/float_analysis.py index 75283f5..0a270d4 100644 --- a/src/blueprinting/workbench/float_analysis.py +++ b/src/blueprinting/workbench/float_analysis.py @@ -230,55 +230,64 @@ def __init__(self) -> None: self.content: Any | None = None def build(self) -> None: - with ui.column().classes("w-full gap-4").mark("float-analysis"): + with ui.column().classes("w-full gap-3").mark("float-analysis"): with ( ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), + ui.element("div").classes("bp-numeric-toolbar"), ): - ui.label("NUMERIC LENS").classes("bp-kicker") - ui.label("浮点数分析").classes("bp-result-title mt-1") - ui.label("探索格式位宽、数值编码、动态范围、可表示值分布和运算边界。 ").classes("bp-card-copy mt-1") - with ui.row().classes("w-full items-end gap-3 mt-3"): - self.exponent_control = ( - ui.number( - "指数位", - value=self.spec.exponent_bits, - min=2, - max=6, - step=1, - precision=0, - on_change=self._format_changed, + with ui.column().classes("bp-numeric-heading gap-1"): + ui.label("NUMERIC LENS").classes("bp-kicker") + ui.label("浮点数分析").classes("bp-result-title") + ui.label("格式、编码、动态范围与运算边界。 ").classes("bp-card-copy") + with ui.element("div").classes("bp-numeric-controls"): + with ui.column().classes("bp-numeric-control gap-1"): + ui.label("指数位 E").classes("bp-summary-label") + self.exponent_control = ( + ui.number( + value=self.spec.exponent_bits, + min=2, + max=6, + step=1, + precision=0, + on_change=self._format_changed, + ) + .props("outlined dense") + .classes("w-full") + .mark("float-exponent-bits") ) - .props("outlined dense") - .mark("float-exponent-bits") - ) - self.mantissa_control = ( - ui.number( - "尾数位", - value=self.spec.mantissa_bits, - min=0, - max=6, - step=1, - precision=0, - on_change=self._format_changed, + with ui.column().classes("bp-numeric-control gap-1"): + ui.label("尾数位 M").classes("bp-summary-label") + self.mantissa_control = ( + ui.number( + value=self.spec.mantissa_bits, + min=0, + max=6, + step=1, + precision=0, + on_change=self._format_changed, + ) + .props("outlined dense") + .classes("w-full") + .mark("float-mantissa-bits") ) - .props("outlined dense") - .mark("float-mantissa-bits") - ) - self.sign_control = ui.switch( - "符号位", value=self.spec.sign_bit, on_change=self._format_changed - ).mark("float-sign-bit") - self.range_control = ( - ui.select( - {0.001: "±0.001", 0.01: "±0.01", 0.1: "±0.1", 1.0: "±1", 10.0: "±10"}, - value=self.limit, - label="观察范围", - on_change=self._range_changed, + with ui.column().classes("bp-numeric-control bp-numeric-control--sign gap-1"): + ui.label("符号位 S").classes("bp-summary-label") + self.sign_control = ui.switch( + "启用", value=self.spec.sign_bit, on_change=self._format_changed + ).props("dense").mark("float-sign-bit") + with ui.column().classes("bp-numeric-control bp-numeric-control--range gap-1"): + ui.label("观察范围").classes("bp-summary-label") + self.range_control = ( + ui.select( + {0.001: "±0.001", 0.01: "±0.01", 0.1: "±0.1", 1.0: "±1", 10.0: "±10"}, + value=self.limit, + on_change=self._range_changed, + ) + .props("outlined dense") + .classes("w-full") + .mark("float-range") ) - .props("outlined dense") - .mark("float-range") - ) - self.content = ui.column().classes("w-full gap-4") + self.content = ui.column().classes("w-full gap-3") self._render_content() def _format_changed(self, _: Any) -> None: @@ -296,19 +305,19 @@ def _range_changed(self, event: Any) -> None: self.limit = float(event.value) self._render_content() - def _set_sign(self, event: Any) -> None: - self.negative = bool(event.value) + def _toggle_sign(self) -> None: + self.negative = not self.negative self._render_content() - def _set_exponent_bit(self, index: int, event: Any) -> None: + def _toggle_exponent_bit(self, index: int) -> None: bits = list(self.exponent) - bits[index] = bool(event.value) + bits[index] = not bits[index] self.exponent = tuple(bits) self._render_content() - def _set_mantissa_bit(self, index: int, event: Any) -> None: + def _toggle_mantissa_bit(self, index: int) -> None: bits = list(self.mantissa) - bits[index] = bool(event.value) + bits[index] = not bits[index] self.mantissa = tuple(bits) self._render_content() @@ -324,39 +333,56 @@ def _render_content(self) -> None: mantissa=self.mantissa, ) with self.content: - with ( - ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), - ): - ui.label("格式位宽对比").classes("bp-card-title") - ui.echart(format_layout_chart_options(self.spec), renderer="svg").classes("w-full h-80") - with ( - ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), - ): - ui.label(f"{self.spec.name} 位级计算器").classes("bp-card-title") - ui.label("指数全 0/全 1 分别按 subnormal/zero 与 Inf/NaN 处理。 ").classes("bp-card-copy") - with ui.row().classes("w-full items-start gap-5 mt-3"): - ui.switch("S", value=self.negative, on_change=self._set_sign).mark("float-value-sign") - with ui.column().classes("gap-1"): - ui.label("Exponent").classes("bp-summary-label") - with ui.row().classes("gap-1"): - for index, value in enumerate(self.exponent): - ui.checkbox( - f"E{index}", - value=value, - on_change=lambda event, i=index: self._set_exponent_bit(i, event), - ).props("dense") - with ui.column().classes("gap-1"): - ui.label("Mantissa").classes("bp-summary-label") - with ui.row().classes("gap-1"): - for index, value in enumerate(self.mantissa): - ui.checkbox( - f"M{index}", - value=value, - on_change=lambda event, i=index: self._set_mantissa_bit(i, event), - ).props("dense") - with ui.element("div").classes("bp-chain-stats mt-3"): + with ui.element("section").classes("bp-evidence-surface bp-numeric-overview"): + with ui.element("div").classes("bp-evidence-section bp-numeric-chart-panel"): + ui.label("格式位宽对比").classes("bp-card-title") + ui.echart(format_layout_chart_options(self.spec), renderer="svg").classes( + "w-full bp-format-chart" + ) + with ui.element("aside").classes("bp-numeric-spec-panel"): + ui.label(self.spec.name).classes("bp-card-title") + ui.label("当前格式摘要").classes("bp-card-copy") + for label, value in ( + ("Total bits", str(self.spec.total_bits)), + ("Exponent bias", str(self.spec.bias)), + ("Min normal", f"{self.spec.min_normal:.3e}"), + ("Max finite", f"{self.spec.max_finite:.3e}"), + ("Finite values", f"{len(values):,}"), + ): + with ui.row().classes("bp-numeric-fact items-center no-wrap"): + ui.label(label).classes("bp-summary-label") + ui.space() + ui.label(value).classes("bp-card-copy bp-mono") + with ui.element("section").classes("bp-evidence-surface"): + with ui.element("div").classes("bp-evidence-section bp-evidence-compact-head"): + ui.label(f"{self.spec.name} 位级计算器").classes("bp-card-title") + ui.label("MSB → LSB;指数全 0/全 1 分别按 zero/subnormal 与 Inf/NaN 处理。 ").classes( + "bp-card-copy" + ) + with ui.element("div").classes("bp-bit-editor"): + self._render_bit_group( + "Sign", + "S", + (self.negative,), + "sign", + lambda _: self._toggle_sign(), + marker="float-value-sign", + ) + self._render_bit_group( + "Exponent", + "E", + self.exponent, + "exponent", + lambda index: self._toggle_exponent_bit(index), + ) + self._render_bit_group( + "Mantissa", + "M", + self.mantissa, + "mantissa", + lambda index: self._toggle_mantissa_bit(index), + ) + with ui.element("div").classes("bp-numeric-decode-grid"): for label, value in ( ("Category", decoded.category), ("Raw exponent", str(decoded.raw_exponent)), @@ -365,13 +391,14 @@ def _render_content(self) -> None: ): with ui.column().classes("gap-0"): ui.label(label).classes("bp-summary-label") - ui.label(value).classes("bp-result-title bp-mono") - with ( - ui.element("section").classes("bp-evidence-surface"), - ui.element("div").classes("bp-evidence-section"), - ): - ui.label("动态范围与可表示值").classes("bp-card-title") - with ui.element("div").classes("bp-chain-stats mt-2"): + ui.label(value).classes("bp-numeric-decode-value bp-mono") + with ui.element("section").classes("bp-evidence-surface"): + with ui.element("div").classes("bp-evidence-section bp-evidence-compact-head"): + ui.label("动态范围与可表示值").classes("bp-card-title") + ui.label(f"当前观察窗口 ±{self.limit:g};蓝色为 normal,红色为 subnormal。 ").classes( + "bp-card-copy" + ) + with ui.element("div").classes("bp-numeric-range-metrics"): for label, value in ( ("Bias", str(self.spec.bias)), ("Min subnormal", f"{self.spec.min_subnormal:.4e}"), @@ -381,8 +408,17 @@ def _render_content(self) -> None: with ui.column().classes("gap-0"): ui.label(label).classes("bp-summary-label") ui.label(value).classes("bp-card-copy bp-mono") - ui.echart(distribution_chart_options(values, self.spec, self.limit)).classes("w-full h-64") - ui.echart(quantization_chart_options(values, self.limit)).classes("w-full h-64") + with ui.element("div").classes("bp-numeric-chart-grid"): + with ui.element("div").classes("bp-numeric-chart"): + ui.label("Representable values").classes("bp-section-title") + ui.echart(distribution_chart_options(values, self.spec, self.limit)).classes( + "w-full bp-numeric-chart-canvas" + ) + with ui.element("div").classes("bp-numeric-chart"): + ui.label("Nearest-value quantization error").classes("bp-section-title") + ui.echart(quantization_chart_options(values, self.limit)).classes( + "w-full bp-numeric-chart-canvas" + ) with ( ui.element("section").classes("bp-evidence-surface"), ui.element("div").classes("bp-evidence-section"), @@ -406,6 +442,33 @@ def _render_content(self) -> None: theme="quartz", ).classes("w-full").style("height: 250px") + @staticmethod + def _render_bit_group( + title: str, + prefix: str, + bits: tuple[bool, ...], + tone: str, + on_toggle: Any, + *, + marker: str | None = None, + ) -> None: + with ui.element("div").classes(f"bp-bit-group bp-bit-group--{tone}"): + ui.label(title).classes("bp-summary-label") + with ui.element("div").classes("bp-bit-row"): + for index, value in enumerate(bits): + classes = "bp-bit-cell bp-bit-cell--on" if value else "bp-bit-cell" + button = ( + ui.element("button") + .props(f"type=button aria-label={title}-{index}") + .classes(classes) + .on("click", lambda _, i=index: on_toggle(i)) + ) + if marker is not None: + button.mark(marker) + with button: + ui.label(f"{prefix}{index if len(bits) > 1 else ''}").classes("bp-bit-index") + ui.label("1" if value else "0").classes("bp-bit-value") + def _format_series(name: str, color: str, data: list[int]) -> dict[str, Any]: return {"name": name, "type": "bar", "stack": "bits", "data": data, "itemStyle": {"color": color}} diff --git a/src/blueprinting/workbench/nicegui_theme.py b/src/blueprinting/workbench/nicegui_theme.py index 5c982ef..bdfc1a4 100644 --- a/src/blueprinting/workbench/nicegui_theme.py +++ b/src/blueprinting/workbench/nicegui_theme.py @@ -936,6 +936,191 @@ border-top: 1px solid var(--bp-line); } +.bp-numeric-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 13px 16px; +} + +.bp-numeric-heading { + min-width: 220px; +} + +.bp-numeric-controls { + display: grid; + grid-template-columns: 92px 92px 100px 116px; + gap: 10px; + align-items: end; +} + +.bp-numeric-control { + min-width: 0; +} + +.bp-numeric-control .q-field__control { + min-height: 38px; +} + +.bp-numeric-control--sign { + justify-content: end; + min-height: 58px; +} + +.bp-numeric-control--sign .q-toggle { + min-height: 38px; +} + +.bp-numeric-overview { + display: grid; + grid-template-columns: minmax(0, 1fr) 260px; +} + +.bp-numeric-chart-panel { + min-width: 0; +} + +.bp-format-chart { + height: 248px; +} + +.bp-numeric-spec-panel { + padding: 16px; + border-left: 1px solid var(--bp-line); + background: #f8fafc; +} + +.bp-numeric-fact { + min-height: 34px; + border-bottom: 1px solid var(--bp-line); +} + +.bp-numeric-fact:first-of-type { + margin-top: 10px; + border-top: 1px solid var(--bp-line); +} + +.bp-bit-editor { + display: flex; + align-items: flex-end; + gap: 24px; + padding: 14px 16px; + border-top: 1px solid var(--bp-line); + background: #f8fafc; +} + +.bp-bit-group { + min-width: 0; +} + +.bp-bit-row { + display: flex; + gap: 5px; + margin-top: 6px; +} + +.bp-bit-cell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 44px; + height: 48px; + margin: 0; + padding: 3px; + appearance: none; + color: #475569; + border: 1px solid #cbd5e1; + border-radius: 6px; + background: #ffffff; + cursor: pointer; + font-family: inherit; + line-height: 1; +} + +.bp-bit-cell:hover { + border-color: #94a3b8; + background: #f8fafc; +} + +.bp-bit-index { + color: #64748b; + font: 650 9px ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.bp-bit-value { + margin-top: 3px; + font: 700 17px ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.bp-bit-group--sign .bp-bit-cell--on { + color: #991b1b; + border-color: #fca5a5; + background: #fef2f2; +} + +.bp-bit-group--exponent .bp-bit-cell--on { + color: #166534; + border-color: #86efac; + background: #f0fdf4; +} + +.bp-bit-group--mantissa .bp-bit-cell--on { + color: #1d4ed8; + border-color: #93c5fd; + background: #eff6ff; +} + +.bp-numeric-decode-grid, +.bp-numeric-range-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border-top: 1px solid var(--bp-line); +} + +.bp-numeric-decode-grid > *, +.bp-numeric-range-metrics > * { + min-width: 0; + padding: 11px 16px; + border-left: 1px solid var(--bp-line); +} + +.bp-numeric-decode-grid > *:first-child, +.bp-numeric-range-metrics > *:first-child { + border-left: 0; +} + +.bp-numeric-decode-value { + margin-top: 4px; + overflow: hidden; + color: var(--bp-text); + font-size: 17px; + font-weight: 680; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bp-numeric-chart-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + border-top: 1px solid var(--bp-line); +} + +.bp-numeric-chart { + min-width: 0; + padding: 12px 14px 4px; + border-left: 1px solid var(--bp-line); +} + +.bp-numeric-chart:first-child { + border-left: 0; +} + +.bp-numeric-chart-canvas { + height: 238px; +} + .bp-metric-grid { width: 100%; display: grid; @@ -1399,6 +1584,24 @@ border-top: 1px solid var(--bp-line); border-left: 0; } + + .bp-numeric-toolbar { + align-items: stretch; + flex-direction: column; + } + + .bp-numeric-controls { + grid-template-columns: repeat(4, minmax(90px, 1fr)); + } + + .bp-numeric-overview { + grid-template-columns: 1fr; + } + + .bp-numeric-spec-panel { + border-top: 1px solid var(--bp-line); + border-left: 0; + } } @media (max-width: 980px) { @@ -1585,6 +1788,25 @@ grid-template-columns: 1fr 1fr; } + .bp-numeric-controls, + .bp-numeric-decode-grid, + .bp-numeric-range-metrics, + .bp-numeric-chart-grid { + grid-template-columns: 1fr 1fr; + } + + .bp-bit-editor { + align-items: flex-start; + flex-direction: column; + overflow-x: auto; + } + + .bp-numeric-chart:nth-child(odd), + .bp-numeric-decode-grid > *:nth-child(odd), + .bp-numeric-range-metrics > *:nth-child(odd) { + border-left: 0; + } + .bp-derivation-details .bp-card { padding: 0; }