gdsdiff 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gdsdiff/__init__.py +148 -0
- gdsdiff/__main__.py +7 -0
- gdsdiff/_environment.py +12 -0
- gdsdiff/_version.py +34 -0
- gdsdiff/acceptance_evidence.py +530 -0
- gdsdiff/api.py +2955 -0
- gdsdiff/backends/__init__.py +26 -0
- gdsdiff/backends/base.py +77 -0
- gdsdiff/backends/boundary_loop_ctypes.py +191 -0
- gdsdiff/backends/cpu.py +20 -0
- gdsdiff/backends/cpu_ctypes.py +255 -0
- gdsdiff/backends/cuda.py +40 -0
- gdsdiff/backends/cuda_ctypes.py +1297 -0
- gdsdiff/backends/exact.py +424 -0
- gdsdiff/backends/geometry_ctypes.py +252 -0
- gdsdiff/backends/identity.py +53 -0
- gdsdiff/backends/metal.py +198 -0
- gdsdiff/backends/metal_ctypes.py +866 -0
- gdsdiff/backends/polygon_extract_ctypes.py +233 -0
- gdsdiff/backends/structural_ctypes.py +130 -0
- gdsdiff/boundary_loops.py +616 -0
- gdsdiff/cache.py +544 -0
- gdsdiff/canonicalize.py +176 -0
- gdsdiff/cli.py +352 -0
- gdsdiff/config.py +257 -0
- gdsdiff/cuda_setup.py +174 -0
- gdsdiff/design_history.py +65 -0
- gdsdiff/diagnostics.py +42 -0
- gdsdiff/diff_gds.py +66 -0
- gdsdiff/exact_diff_markdown.py +979 -0
- gdsdiff/exact_oracle.py +649 -0
- gdsdiff/extract.py +376 -0
- gdsdiff/gds_metadata.py +43 -0
- gdsdiff/geometry.py +112 -0
- gdsdiff/grid.py +143 -0
- gdsdiff/hierarchy.py +215 -0
- gdsdiff/hierarchy_extract.py +263 -0
- gdsdiff/inventory.py +153 -0
- gdsdiff/multiprocessing_support.py +39 -0
- gdsdiff/native.py +135 -0
- gdsdiff/native_cache.py +265 -0
- gdsdiff/native_src/cpu_prefilter.cpp +349 -0
- gdsdiff/native_src/gds_boundary_loops.cpp +505 -0
- gdsdiff/native_src/gds_geometry_scan.cpp +601 -0
- gdsdiff/native_src/gds_polygon_extract.cpp +594 -0
- gdsdiff/native_src/gds_structural_scan.cpp +335 -0
- gdsdiff/native_src/gdsdiff/CMakeLists.txt +74 -0
- gdsdiff/native_src/gdsdiff/core.cpp +191 -0
- gdsdiff/native_src/gdsdiff/core.hpp +96 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cu +304 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cuh +33 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cu +133 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cuh +17 -0
- gdsdiff/native_src/gdsdiff/cuda_ctypes.cu +4528 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cu +194 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cuh +34 -0
- gdsdiff/native_src/gdsdiff/metal_ctypes.mm +2791 -0
- gdsdiff/native_src/gdsdiff/python_bindings.cpp +21 -0
- gdsdiff/native_src/gdsdiff/test_core.cpp +93 -0
- gdsdiff/native_src/gdsdiff/test_cuda_assignment.cu +109 -0
- gdsdiff/native_src/gdsdiff/test_cuda_candidates.cu +94 -0
- gdsdiff/native_src/gdsdiff/test_cuda_memory.cu +97 -0
- gdsdiff/policy.py +305 -0
- gdsdiff/polygon_components.py +860 -0
- gdsdiff/profiles.py +152 -0
- gdsdiff/py.typed +1 -0
- gdsdiff/rect_coverage.py +384 -0
- gdsdiff/report.py +354 -0
- gdsdiff/schemas/gdsdiff_report-v1.schema.json +92 -0
- gdsdiff/semantics.py +75 -0
- gdsdiff/source_edge_diff.py +1120 -0
- gdsdiff/spatial.py +71 -0
- gdsdiff/structural_guard.py +161 -0
- gdsdiff/surplus_coverage.py +255 -0
- gdsdiff/synthetic_suite.py +1643 -0
- gdsdiff/templates.py +709 -0
- gdsdiff/tiling.py +153 -0
- gdsdiff/windowed_exact.py +270 -0
- gdsdiff/windows.py +184 -0
- gdsdiff-0.1.0.dist-info/METADATA +135 -0
- gdsdiff-0.1.0.dist-info/RECORD +85 -0
- gdsdiff-0.1.0.dist-info/WHEEL +5 -0
- gdsdiff-0.1.0.dist-info/entry_points.txt +4 -0
- gdsdiff-0.1.0.dist-info/licenses/LICENSE +674 -0
- gdsdiff-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from time import perf_counter
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .api import CompareRunResult, compare_gds_files
|
|
16
|
+
from .cache import load_or_build_cached_geometry
|
|
17
|
+
from .config import CompareConfig
|
|
18
|
+
from .native import discover_native_capabilities
|
|
19
|
+
|
|
20
|
+
SCHEMA_VERSION = "gdsdiff-acceptance-evidence-v1"
|
|
21
|
+
TOP_NAME_OVERRIDES = {
|
|
22
|
+
"medium": "medium_compare_top",
|
|
23
|
+
"equal_count": "equal_count_top",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AcceptanceCorpusPair:
|
|
29
|
+
name: str
|
|
30
|
+
before: Path
|
|
31
|
+
after: Path
|
|
32
|
+
top: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_acceptance_corpus(repo_root: str | Path) -> tuple[AcceptanceCorpusPair, ...]:
|
|
36
|
+
repo_root = Path(repo_root)
|
|
37
|
+
test_root = repo_root / "references" / "testGDS"
|
|
38
|
+
if not test_root.exists():
|
|
39
|
+
return ()
|
|
40
|
+
pairs: list[AcceptanceCorpusPair] = []
|
|
41
|
+
for before in sorted(test_root.rglob("*_before.gds")):
|
|
42
|
+
stem = before.name[: -len("_before.gds")]
|
|
43
|
+
after = before.with_name(f"{stem}_after.gds")
|
|
44
|
+
if not after.exists():
|
|
45
|
+
continue
|
|
46
|
+
relative_parent = before.parent.relative_to(test_root)
|
|
47
|
+
name = stem if stem else before.parent.name
|
|
48
|
+
if name == before.parent.name or name.startswith(f"{before.parent.name}"):
|
|
49
|
+
pair_name = name
|
|
50
|
+
else:
|
|
51
|
+
pair_name = "_".join((*relative_parent.parts, stem))
|
|
52
|
+
top = TOP_NAME_OVERRIDES.get(pair_name) or TOP_NAME_OVERRIDES.get(stem) or TOP_NAME_OVERRIDES.get(before.parent.name)
|
|
53
|
+
pairs.append(AcceptanceCorpusPair(pair_name, before, after, top=top))
|
|
54
|
+
return tuple(pairs)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_acceptance_evidence(
|
|
58
|
+
pairs: tuple[AcceptanceCorpusPair, ...],
|
|
59
|
+
output_dir: str | Path,
|
|
60
|
+
*,
|
|
61
|
+
min_speedup: float = 100.0,
|
|
62
|
+
tile_size: int = 256,
|
|
63
|
+
fast_backend: str = "cuda",
|
|
64
|
+
monitor_cpu: bool = False,
|
|
65
|
+
monitor_interval_s: float = 0.1,
|
|
66
|
+
force_geometry_prefilter: bool = False,
|
|
67
|
+
geometry_cache_dir: str | Path | None = None,
|
|
68
|
+
warm_geometry_cache: bool = False,
|
|
69
|
+
structural_guard: bool = False,
|
|
70
|
+
native_geometry_guard: bool = False,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
output_dir = Path(output_dir)
|
|
73
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
started = datetime.now(timezone.utc).isoformat()
|
|
75
|
+
rows = []
|
|
76
|
+
for pair in pairs:
|
|
77
|
+
row = _run_pair(
|
|
78
|
+
pair,
|
|
79
|
+
output_dir,
|
|
80
|
+
min_speedup=min_speedup,
|
|
81
|
+
tile_size=tile_size,
|
|
82
|
+
fast_backend=fast_backend,
|
|
83
|
+
monitor_cpu=monitor_cpu,
|
|
84
|
+
monitor_interval_s=monitor_interval_s,
|
|
85
|
+
force_geometry_prefilter=force_geometry_prefilter,
|
|
86
|
+
geometry_cache_dir=geometry_cache_dir,
|
|
87
|
+
warm_geometry_cache=warm_geometry_cache,
|
|
88
|
+
structural_guard=structural_guard,
|
|
89
|
+
native_geometry_guard=native_geometry_guard,
|
|
90
|
+
)
|
|
91
|
+
rows.append(row)
|
|
92
|
+
result = {
|
|
93
|
+
"schema_version": SCHEMA_VERSION,
|
|
94
|
+
"started": started,
|
|
95
|
+
"min_speedup": min_speedup,
|
|
96
|
+
"tile_size": tile_size,
|
|
97
|
+
"fast_backend": fast_backend,
|
|
98
|
+
"monitor_cpu": monitor_cpu,
|
|
99
|
+
"monitor_interval_s": monitor_interval_s,
|
|
100
|
+
"force_geometry_prefilter": force_geometry_prefilter,
|
|
101
|
+
"geometry_cache_dir": str(geometry_cache_dir) if geometry_cache_dir is not None else None,
|
|
102
|
+
"warm_geometry_cache": warm_geometry_cache,
|
|
103
|
+
"structural_guard": structural_guard,
|
|
104
|
+
"native_geometry_guard_requested": native_geometry_guard,
|
|
105
|
+
"capabilities": discover_native_capabilities().to_json(),
|
|
106
|
+
"results": rows,
|
|
107
|
+
"all_pairs_pass_speed_status_gate": all(row["passes_speed_status_gate"] for row in rows),
|
|
108
|
+
"all_pairs_have_exact_polygon_evidence": all(row["exact_polygon_evidence_complete"] for row in rows),
|
|
109
|
+
"all_monitored_cpu_runs_populated_all_cores_ge_50pct": all(
|
|
110
|
+
row["cpu_windowed_all_cores_ge_50pct_seen"] for row in rows
|
|
111
|
+
)
|
|
112
|
+
if monitor_cpu
|
|
113
|
+
else None,
|
|
114
|
+
"notes": [
|
|
115
|
+
"fast_backend timing is status evidence, not polygon-level evidence, unless exact_polygon_evidence_source says otherwise.",
|
|
116
|
+
"exact_diff_markdown is generated by the full exact gdstk oracle and is the vertex-level evidence artifact.",
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
validate_acceptance_evidence(result)
|
|
120
|
+
output_path = output_dir / "acceptance_evidence.json"
|
|
121
|
+
output_path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
122
|
+
return result | {"output_path": str(output_path)}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_acceptance_evidence(result: dict[str, Any]) -> None:
|
|
126
|
+
required = {
|
|
127
|
+
"schema_version",
|
|
128
|
+
"started",
|
|
129
|
+
"min_speedup",
|
|
130
|
+
"tile_size",
|
|
131
|
+
"fast_backend",
|
|
132
|
+
"capabilities",
|
|
133
|
+
"results",
|
|
134
|
+
"all_pairs_pass_speed_status_gate",
|
|
135
|
+
"all_pairs_have_exact_polygon_evidence",
|
|
136
|
+
}
|
|
137
|
+
missing = sorted(required - set(result))
|
|
138
|
+
if missing:
|
|
139
|
+
raise ValueError(f"acceptance evidence missing fields: {', '.join(missing)}")
|
|
140
|
+
if result["schema_version"] != SCHEMA_VERSION:
|
|
141
|
+
raise ValueError("unsupported acceptance evidence schema")
|
|
142
|
+
if not isinstance(result["results"], list):
|
|
143
|
+
raise ValueError("acceptance evidence results must be a list")
|
|
144
|
+
for row in result["results"]:
|
|
145
|
+
_validate_row(row)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_row(row: dict[str, Any]) -> None:
|
|
149
|
+
required = {
|
|
150
|
+
"pair",
|
|
151
|
+
"before",
|
|
152
|
+
"after",
|
|
153
|
+
"cpu_windowed_wall_s",
|
|
154
|
+
"fast_wall_s",
|
|
155
|
+
"exact_wall_s",
|
|
156
|
+
"speedup_vs_cpu_windowed",
|
|
157
|
+
"cpu_windowed_status",
|
|
158
|
+
"fast_status",
|
|
159
|
+
"exact_status",
|
|
160
|
+
"fast_status_matches_exact",
|
|
161
|
+
"exact_polygon_evidence_complete",
|
|
162
|
+
"passes_speed_status_gate",
|
|
163
|
+
"exact_diff_markdown",
|
|
164
|
+
"exact_policy_sha256",
|
|
165
|
+
}
|
|
166
|
+
missing = sorted(required - set(row))
|
|
167
|
+
if missing:
|
|
168
|
+
raise ValueError(f"acceptance evidence row missing fields: {', '.join(missing)}")
|
|
169
|
+
for field in ("cpu_windowed_wall_s", "fast_wall_s", "exact_wall_s", "speedup_vs_cpu_windowed"):
|
|
170
|
+
if row[field] < 0:
|
|
171
|
+
raise ValueError(f"{field} must be nonnegative")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _run_pair(
|
|
175
|
+
pair: AcceptanceCorpusPair,
|
|
176
|
+
output_dir: Path,
|
|
177
|
+
*,
|
|
178
|
+
min_speedup: float,
|
|
179
|
+
tile_size: int,
|
|
180
|
+
fast_backend: str,
|
|
181
|
+
monitor_cpu: bool,
|
|
182
|
+
monitor_interval_s: float,
|
|
183
|
+
force_geometry_prefilter: bool,
|
|
184
|
+
geometry_cache_dir: str | Path | None,
|
|
185
|
+
warm_geometry_cache: bool,
|
|
186
|
+
structural_guard: bool,
|
|
187
|
+
native_geometry_guard: bool,
|
|
188
|
+
) -> dict[str, Any]:
|
|
189
|
+
exact_markdown_path = output_dir / f"{pair.name}_exact_diff.md"
|
|
190
|
+
if monitor_cpu:
|
|
191
|
+
with _forced_multicore_thresholds(), _CpuMonitor(interval_s=monitor_interval_s) as monitor:
|
|
192
|
+
cpu_windowed, cpu_windowed_wall = _timed_compare(
|
|
193
|
+
CompareConfig(pair.before, pair.after, top=pair.top, backend="cpu", tile_size=tile_size),
|
|
194
|
+
exact=False,
|
|
195
|
+
command=("acceptance-evidence", pair.name, "cpu-windowed"),
|
|
196
|
+
)
|
|
197
|
+
cpu_monitor = monitor.summary()
|
|
198
|
+
else:
|
|
199
|
+
cpu_windowed, cpu_windowed_wall = _timed_compare(
|
|
200
|
+
CompareConfig(pair.before, pair.after, top=pair.top, backend="cpu", tile_size=tile_size),
|
|
201
|
+
exact=False,
|
|
202
|
+
command=("acceptance-evidence", pair.name, "cpu-windowed"),
|
|
203
|
+
)
|
|
204
|
+
cpu_monitor = {"available": False, "reason": "monitoring disabled"}
|
|
205
|
+
if fast_backend == "cpu":
|
|
206
|
+
verified, verified_wall = _timed_compare(
|
|
207
|
+
CompareConfig(pair.before, pair.after, top=pair.top, backend="cpu", tile_size=tile_size),
|
|
208
|
+
exact=True,
|
|
209
|
+
exact_diff_markdown_path=exact_markdown_path,
|
|
210
|
+
command=("acceptance-evidence", pair.name, "cpu-exact-smoke"),
|
|
211
|
+
)
|
|
212
|
+
fast_status = verified.status.value
|
|
213
|
+
fast_wall = verified_wall
|
|
214
|
+
exact_wall = verified_wall
|
|
215
|
+
else:
|
|
216
|
+
cache_warm_s = 0.0
|
|
217
|
+
if warm_geometry_cache and geometry_cache_dir is not None:
|
|
218
|
+
cache_warm_s = _warm_geometry_cache(pair, geometry_cache_dir, tile_size=tile_size, backend=fast_backend)
|
|
219
|
+
verified, verified_wall = _timed_compare(
|
|
220
|
+
CompareConfig(
|
|
221
|
+
pair.before,
|
|
222
|
+
pair.after,
|
|
223
|
+
top=pair.top,
|
|
224
|
+
backend=fast_backend,
|
|
225
|
+
tile_size=tile_size,
|
|
226
|
+
geometry_cache_dir=geometry_cache_dir,
|
|
227
|
+
),
|
|
228
|
+
exact=False,
|
|
229
|
+
exact_diff_markdown_path=exact_markdown_path,
|
|
230
|
+
verify_exact_after_fast=True,
|
|
231
|
+
use_fast_shortcuts=not force_geometry_prefilter,
|
|
232
|
+
use_structural_guard=structural_guard,
|
|
233
|
+
use_native_geometry_guard=native_geometry_guard,
|
|
234
|
+
command=("acceptance-evidence", pair.name, f"{fast_backend}-fast-plus-exact"),
|
|
235
|
+
)
|
|
236
|
+
fast_status = str(verified.report.exact_fallback.get("fast_status", verified.status.value))
|
|
237
|
+
fast_wall = float(
|
|
238
|
+
verified.timings.get(
|
|
239
|
+
"fast_status_s",
|
|
240
|
+
verified.timings.get("geometry_status_s", verified.timings.get("total_s", verified_wall)),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
exact_wall = float(verified.timings.get("exact_verification_s", verified_wall))
|
|
244
|
+
speedup = cpu_windowed_wall / fast_wall if fast_wall > 0 else float("inf")
|
|
245
|
+
exact_policy_sha = _policy_sha256(verified)
|
|
246
|
+
all_cores_ge_50_seen = _all_cores_ge_50_seen(cpu_monitor)
|
|
247
|
+
return {
|
|
248
|
+
"pair": pair.name,
|
|
249
|
+
"before": str(pair.before),
|
|
250
|
+
"after": str(pair.after),
|
|
251
|
+
"top": pair.top,
|
|
252
|
+
"cpu_windowed_wall_s": cpu_windowed_wall,
|
|
253
|
+
"fast_wall_s": fast_wall,
|
|
254
|
+
"exact_wall_s": exact_wall,
|
|
255
|
+
"verified_total_wall_s": verified_wall,
|
|
256
|
+
"speedup_vs_cpu_windowed": speedup,
|
|
257
|
+
"cpu_windowed_status": cpu_windowed.status.value,
|
|
258
|
+
"cpu_windowed_timings": dict(cpu_windowed.timings),
|
|
259
|
+
"cpu_windowed_cpu_monitor": cpu_monitor,
|
|
260
|
+
"cpu_windowed_all_cores_ge_50pct_seen": all_cores_ge_50_seen,
|
|
261
|
+
"fast_status": fast_status,
|
|
262
|
+
"exact_status": verified.status.value,
|
|
263
|
+
"cpu_windowed_exact_verification_complete": cpu_windowed.report.exact_verification_complete,
|
|
264
|
+
"fast_exact_verification_complete": verified.report.exact_verification_complete,
|
|
265
|
+
"exact_verification_complete": verified.report.exact_verification_complete,
|
|
266
|
+
"fast_status_matches_exact": fast_status == verified.status.value,
|
|
267
|
+
"cpu_windowed_status_matches_exact": cpu_windowed.status == verified.status,
|
|
268
|
+
"exact_polygon_evidence_complete": verified.report.exact_verification_complete,
|
|
269
|
+
"exact_polygon_evidence_source": "fast-comparator-run-with-full-gdstk-oracle",
|
|
270
|
+
"passes_speed_status_gate": speedup >= min_speedup and fast_status == verified.status.value and verified.report.exact_verification_complete,
|
|
271
|
+
"exact_diff_markdown": str(exact_markdown_path),
|
|
272
|
+
"exact_policy_sha256": exact_policy_sha,
|
|
273
|
+
"exact_diff_markdown_sha256": _file_sha256(exact_markdown_path),
|
|
274
|
+
"artifacts": {
|
|
275
|
+
"exact_diff_markdown": str(exact_markdown_path),
|
|
276
|
+
},
|
|
277
|
+
"verified_report_backend": verified.report.backend,
|
|
278
|
+
"verified_report_exact_fallback": verified.report.exact_fallback,
|
|
279
|
+
"verified_timings": dict(verified.timings),
|
|
280
|
+
"force_geometry_prefilter": force_geometry_prefilter,
|
|
281
|
+
"geometry_cache_dir": str(geometry_cache_dir) if geometry_cache_dir is not None else None,
|
|
282
|
+
"warm_geometry_cache": warm_geometry_cache,
|
|
283
|
+
"geometry_cache_warm_s": cache_warm_s if fast_backend != "cpu" else 0.0,
|
|
284
|
+
"structural_guard": structural_guard,
|
|
285
|
+
"native_geometry_guard_requested": native_geometry_guard,
|
|
286
|
+
"native_geometry_guard_used": bool(verified.report.backend.get("native_geometry_guard")),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _timed_compare(config: CompareConfig, **kwargs) -> tuple[CompareRunResult, float]:
|
|
291
|
+
start = perf_counter()
|
|
292
|
+
result = compare_gds_files(config, **kwargs)
|
|
293
|
+
return result, perf_counter() - start
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _warm_geometry_cache(pair: AcceptanceCorpusPair, geometry_cache_dir: str | Path, *, tile_size: int, backend: str) -> float:
|
|
297
|
+
start = perf_counter()
|
|
298
|
+
for path in (pair.before, pair.after):
|
|
299
|
+
load_or_build_cached_geometry(
|
|
300
|
+
cache_dir=geometry_cache_dir,
|
|
301
|
+
path=path,
|
|
302
|
+
top_name=pair.top,
|
|
303
|
+
layers="all",
|
|
304
|
+
meters_per_dbu=None,
|
|
305
|
+
path_policy="gdstk",
|
|
306
|
+
)
|
|
307
|
+
compare_gds_files(
|
|
308
|
+
CompareConfig(
|
|
309
|
+
pair.before,
|
|
310
|
+
pair.after,
|
|
311
|
+
top=pair.top,
|
|
312
|
+
backend=backend,
|
|
313
|
+
tile_size=tile_size,
|
|
314
|
+
geometry_cache_dir=geometry_cache_dir,
|
|
315
|
+
),
|
|
316
|
+
exact=False,
|
|
317
|
+
verify_exact_after_fast=False,
|
|
318
|
+
use_fast_shortcuts=False,
|
|
319
|
+
command=("acceptance-evidence", pair.name, "warm-geometry-cache"),
|
|
320
|
+
)
|
|
321
|
+
return perf_counter() - start
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class _CpuMonitor:
|
|
325
|
+
def __init__(self, *, interval_s: float = 0.1) -> None:
|
|
326
|
+
self._interval_s = max(interval_s, 0.01)
|
|
327
|
+
self._stop = threading.Event()
|
|
328
|
+
self._thread: threading.Thread | None = None
|
|
329
|
+
self._samples: list[tuple[float, ...]] = []
|
|
330
|
+
self._available = True
|
|
331
|
+
self._reason = ""
|
|
332
|
+
|
|
333
|
+
def __enter__(self) -> "_CpuMonitor":
|
|
334
|
+
try:
|
|
335
|
+
initial = _read_cpu_counters()
|
|
336
|
+
except OSError as exc:
|
|
337
|
+
self._available = False
|
|
338
|
+
self._reason = str(exc)
|
|
339
|
+
return self
|
|
340
|
+
if not initial:
|
|
341
|
+
self._available = False
|
|
342
|
+
self._reason = "no per-core CPU counters found"
|
|
343
|
+
return self
|
|
344
|
+
self._thread = threading.Thread(target=self._run, args=(initial,), daemon=True)
|
|
345
|
+
self._thread.start()
|
|
346
|
+
return self
|
|
347
|
+
|
|
348
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
349
|
+
self._stop.set()
|
|
350
|
+
if self._thread is not None:
|
|
351
|
+
self._thread.join(timeout=max(self._interval_s * 4, 0.25))
|
|
352
|
+
|
|
353
|
+
def summary(self) -> dict[str, Any]:
|
|
354
|
+
if not self._available:
|
|
355
|
+
return {"available": False, "reason": self._reason}
|
|
356
|
+
if not self._samples:
|
|
357
|
+
return {"available": True, "sample_count": 0, "reason": "no completed CPU samples"}
|
|
358
|
+
logical_cpu_count = len(self._samples[0])
|
|
359
|
+
return {
|
|
360
|
+
"available": True,
|
|
361
|
+
"sample_count": len(self._samples),
|
|
362
|
+
"logical_cpu_count": logical_cpu_count,
|
|
363
|
+
"max_cores_ge_25pct": max(_count_at_least(sample, 25.0) for sample in self._samples),
|
|
364
|
+
"max_cores_ge_50pct": max(_count_at_least(sample, 50.0) for sample in self._samples),
|
|
365
|
+
"max_cores_ge_75pct": max(_count_at_least(sample, 75.0) for sample in self._samples),
|
|
366
|
+
"mean_cores_ge_50pct": sum(_count_at_least(sample, 50.0) for sample in self._samples) / len(self._samples),
|
|
367
|
+
"max_mean_busy_pct": max(sum(sample) / len(sample) for sample in self._samples),
|
|
368
|
+
"mean_busy_pct": sum(sum(sample) / len(sample) for sample in self._samples) / len(self._samples),
|
|
369
|
+
"max_core_busy_pct": max(max(sample) for sample in self._samples),
|
|
370
|
+
"interval_s": self._interval_s,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
def _run(self, previous: list[tuple[int, int]]) -> None:
|
|
374
|
+
while not self._stop.wait(self._interval_s):
|
|
375
|
+
try:
|
|
376
|
+
current = _read_cpu_counters()
|
|
377
|
+
except OSError as exc:
|
|
378
|
+
self._available = False
|
|
379
|
+
self._reason = str(exc)
|
|
380
|
+
return
|
|
381
|
+
if len(current) != len(previous):
|
|
382
|
+
self._available = False
|
|
383
|
+
self._reason = "CPU counter count changed during monitoring"
|
|
384
|
+
return
|
|
385
|
+
sample = tuple(_busy_percent(old, new) for old, new in zip(previous, current))
|
|
386
|
+
self._samples.append(sample)
|
|
387
|
+
previous = current
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _read_cpu_counters() -> list[tuple[int, int]]:
|
|
391
|
+
path = Path("/proc/stat")
|
|
392
|
+
rows: list[tuple[int, int]] = []
|
|
393
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
394
|
+
for line in handle:
|
|
395
|
+
parts = line.split()
|
|
396
|
+
if not parts:
|
|
397
|
+
continue
|
|
398
|
+
name = parts[0]
|
|
399
|
+
if not name.startswith("cpu") or name == "cpu" or not name[3:].isdigit():
|
|
400
|
+
continue
|
|
401
|
+
values = [int(value) for value in parts[1:]]
|
|
402
|
+
idle = values[3] + (values[4] if len(values) > 4 else 0)
|
|
403
|
+
total = sum(values)
|
|
404
|
+
rows.append((total, idle))
|
|
405
|
+
return rows
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _busy_percent(old: tuple[int, int], new: tuple[int, int]) -> float:
|
|
409
|
+
old_total, old_idle = old
|
|
410
|
+
new_total, new_idle = new
|
|
411
|
+
total_delta = new_total - old_total
|
|
412
|
+
if total_delta <= 0:
|
|
413
|
+
return 0.0
|
|
414
|
+
idle_delta = new_idle - old_idle
|
|
415
|
+
return max(0.0, min(100.0, 100.0 * (total_delta - idle_delta) / total_delta))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _count_at_least(sample: tuple[float, ...], threshold: float) -> int:
|
|
419
|
+
return sum(1 for value in sample if value >= threshold)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _all_cores_ge_50_seen(summary: dict[str, Any]) -> bool:
|
|
423
|
+
if not summary.get("available"):
|
|
424
|
+
return False
|
|
425
|
+
logical_cpu_count = summary.get("logical_cpu_count")
|
|
426
|
+
max_cores = summary.get("max_cores_ge_50pct")
|
|
427
|
+
return isinstance(logical_cpu_count, int) and isinstance(max_cores, int) and max_cores >= logical_cpu_count
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@contextmanager
|
|
431
|
+
def _forced_multicore_thresholds():
|
|
432
|
+
names = (
|
|
433
|
+
"GDSDIFF_PARALLEL_MIN_POLYGONS",
|
|
434
|
+
"GDSDIFF_PARALLEL_EXTRACT_PAIR_MIN_BYTES",
|
|
435
|
+
"GDS_GPU_COMPARE_PARALLEL_MIN_POLYGONS",
|
|
436
|
+
"GDS_GPU_COMPARE_PARALLEL_EXTRACT_PAIR_MIN_BYTES",
|
|
437
|
+
)
|
|
438
|
+
previous = {name: os.environ.get(name) for name in names}
|
|
439
|
+
try:
|
|
440
|
+
for name in names:
|
|
441
|
+
os.environ[name] = "1"
|
|
442
|
+
yield
|
|
443
|
+
finally:
|
|
444
|
+
for name, value in previous.items():
|
|
445
|
+
if value is None:
|
|
446
|
+
os.environ.pop(name, None)
|
|
447
|
+
else:
|
|
448
|
+
os.environ[name] = value
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _policy_sha256(result: CompareRunResult) -> str:
|
|
452
|
+
payload = json.dumps(result.policy_result.to_json(), sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
453
|
+
return hashlib.sha256(payload).hexdigest()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _file_sha256(path: Path) -> str:
|
|
457
|
+
digest = hashlib.sha256()
|
|
458
|
+
with path.open("rb") as handle:
|
|
459
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
460
|
+
digest.update(chunk)
|
|
461
|
+
return digest.hexdigest()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
465
|
+
parser = argparse.ArgumentParser(description="Run GDS comparator acceptance evidence over the reference corpus.")
|
|
466
|
+
parser.add_argument("--repo-root", type=Path, default=Path.cwd())
|
|
467
|
+
parser.add_argument("--output-dir", type=Path, required=True)
|
|
468
|
+
parser.add_argument("--pair", action="append", metavar="NAME:BEFORE:AFTER[:TOP]")
|
|
469
|
+
parser.add_argument("--min-speedup", type=float, default=100.0)
|
|
470
|
+
parser.add_argument("--tile-size", type=int, default=256)
|
|
471
|
+
parser.add_argument("--fast-backend", default="cuda")
|
|
472
|
+
parser.add_argument("--monitor-cpu", action="store_true")
|
|
473
|
+
parser.add_argument("--monitor-interval-s", type=float, default=0.1)
|
|
474
|
+
parser.add_argument("--force-geometry-prefilter", action="store_true")
|
|
475
|
+
parser.add_argument("--geometry-cache-dir", type=Path)
|
|
476
|
+
parser.add_argument("--warm-geometry-cache", action="store_true")
|
|
477
|
+
parser.add_argument("--structural-guard", action="store_true")
|
|
478
|
+
parser.add_argument("--native-geometry-guard", action="store_true")
|
|
479
|
+
return parser
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def main(argv: list[str] | None = None) -> int:
|
|
483
|
+
parser = build_parser()
|
|
484
|
+
args = parser.parse_args(argv)
|
|
485
|
+
if args.min_speedup < 0:
|
|
486
|
+
parser.error("--min-speedup must be nonnegative")
|
|
487
|
+
if args.tile_size <= 0:
|
|
488
|
+
parser.error("--tile-size must be positive")
|
|
489
|
+
if args.monitor_interval_s <= 0:
|
|
490
|
+
parser.error("--monitor-interval-s must be positive")
|
|
491
|
+
pairs = _parse_pairs(args.pair) if args.pair else default_acceptance_corpus(args.repo_root)
|
|
492
|
+
if not pairs:
|
|
493
|
+
parser.error("no acceptance corpus pairs found")
|
|
494
|
+
result = run_acceptance_evidence(
|
|
495
|
+
pairs,
|
|
496
|
+
args.output_dir,
|
|
497
|
+
min_speedup=args.min_speedup,
|
|
498
|
+
tile_size=args.tile_size,
|
|
499
|
+
fast_backend=args.fast_backend,
|
|
500
|
+
monitor_cpu=args.monitor_cpu,
|
|
501
|
+
monitor_interval_s=args.monitor_interval_s,
|
|
502
|
+
force_geometry_prefilter=args.force_geometry_prefilter,
|
|
503
|
+
geometry_cache_dir=args.geometry_cache_dir,
|
|
504
|
+
warm_geometry_cache=args.warm_geometry_cache,
|
|
505
|
+
structural_guard=args.structural_guard,
|
|
506
|
+
native_geometry_guard=args.native_geometry_guard,
|
|
507
|
+
)
|
|
508
|
+
print(result["output_path"])
|
|
509
|
+
return 0 if result["all_pairs_pass_speed_status_gate"] else 1
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _parse_pairs(values: list[str]) -> tuple[AcceptanceCorpusPair, ...]:
|
|
513
|
+
pairs: list[AcceptanceCorpusPair] = []
|
|
514
|
+
for value in values:
|
|
515
|
+
parts = value.split(":")
|
|
516
|
+
if len(parts) not in {3, 4}:
|
|
517
|
+
raise ValueError("--pair must have form NAME:BEFORE:AFTER[:TOP]")
|
|
518
|
+
pairs.append(
|
|
519
|
+
AcceptanceCorpusPair(
|
|
520
|
+
name=parts[0],
|
|
521
|
+
before=Path(parts[1]),
|
|
522
|
+
after=Path(parts[2]),
|
|
523
|
+
top=parts[3] if len(parts) == 4 and parts[3] else None,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
return tuple(pairs)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
if __name__ == "__main__":
|
|
530
|
+
raise SystemExit(main())
|