evaluators 2.1.2__tar.gz → 2.1.4__tar.gz
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.
- {evaluators-2.1.2 → evaluators-2.1.4}/PKG-INFO +25 -1
- {evaluators-2.1.2 → evaluators-2.1.4}/README.md +24 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/pyproject.toml +83 -3
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/__init__.py +2 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/_engine.py +10 -3
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/_streams.py +11 -1
- evaluators-2.1.4/sources/evaluators/benchmarks/coordinator.py +274 -0
- evaluators-2.1.4/sources/evaluators/benchmarks/scenarios.py +110 -0
- evaluators-2.1.4/sources/evaluators/benchmarks/sys_info.py +224 -0
- evaluators-2.1.4/sources/evaluators/benchmarks/worker.py +152 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/cli/__main__.py +1 -5
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/cli/compute.py +50 -10
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/cli/memory.py +91 -80
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/cli/third_party.py +19 -24
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/integrations/lightning.py +111 -41
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/__init__.py +36 -2
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/__init__.py +2 -1
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/_registry.py +53 -10
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/filesystem/_reader.py +4 -7
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/filesystem/_writer.py +4 -2
- evaluators-2.1.4/sources/evaluators/memory/backends/gds/__init__.py +3 -0
- evaluators-2.1.4/sources/evaluators/memory/backends/gds/reader.py +87 -0
- evaluators-2.1.4/sources/evaluators/memory/backends/gds/utils.py +17 -0
- evaluators-2.1.4/sources/evaluators/memory/backends/gds/writer.py +168 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/lmdb/_reader.py +44 -28
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/lmdb/_writer.py +18 -2
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/memmap/_buffers.py +28 -9
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/memmap/_reader.py +32 -11
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/memmap/_writer.py +57 -7
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/config.py +6 -3
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/interfaces.py +11 -22
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/schema.py +1 -3
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/metrics/domain/depth.py +3 -9
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/metrics/domain/dvps.py +40 -64
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/metrics/domain/segmentation/_utils.py +10 -17
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/metrics/domain/segmentation/panoptic.py +8 -16
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/metrics/domain/segmentation/semantic.py +4 -12
- evaluators-2.1.4/sources/evaluators/metrics/domain/step.py +794 -0
- evaluators-2.1.4/sources/evaluators/metrics/domain/tracking.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/metrics/stats.py +2 -6
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/presets/cityscapes.py +60 -7
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/presets/semkitti.py +60 -1
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/scheduling/_catalog.py +3 -6
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/third_party/__init__.py +0 -1
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/third_party/adapters/__init__.py +0 -1
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/third_party/adapters/coco.py +30 -9
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/third_party/adapters/dvpq.py +5 -21
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/types.py +52 -2
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/utils/comms.py +1 -3
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/utils/configs.py +2 -4
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/utils/tensor_ops.py +3 -4
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/visuals/__init__.py +1 -1
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/visuals/metric.py +7 -14
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/visuals/tools/__init__.py +2 -0
- evaluators-2.1.4/sources/evaluators/visuals/tools/_visualizer.py +1411 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/visuals/tools/transform.py +9 -23
- evaluators-2.1.2/sources/evaluators/metrics/domain/step.py +0 -393
- evaluators-2.1.2/sources/evaluators/visuals/tools/_visualizer.py +0 -779
- {evaluators-2.1.2/sources/evaluators/metrics → evaluators-2.1.4/sources/evaluators/benchmarks}/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/constants.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/filesystem/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/lmdb/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/backends/memmap/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/codecs/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/codecs/_blosc.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/codecs/_png.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/codecs/_raw.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/memory/codecs/_tiff.py +0 -0
- /evaluators-2.1.2/sources/evaluators/metrics/adapters/coco.py → /evaluators-2.1.4/sources/evaluators/metrics/__init__.py +0 -0
- /evaluators-2.1.2/sources/evaluators/metrics/adapters/trackeval.py → /evaluators-2.1.4/sources/evaluators/metrics/adapters/coco.py +0 -0
- /evaluators-2.1.2/sources/evaluators/metrics/domain/tracking.py → /evaluators-2.1.4/sources/evaluators/metrics/adapters/trackeval.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/metrics/domain/segmentation/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/presets/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/presets/kitti.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/scheduling/__init__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/scheduling/_partitioner.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/third_party/__main__.py +0 -0
- {evaluators-2.1.2 → evaluators-2.1.4}/sources/evaluators/third_party/adapters/pq.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: evaluators
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: Various scene understanding and perception evaluation metrics.
|
|
5
5
|
Author: Kurt Stolle
|
|
6
6
|
Author-email: Kurt Stolle <kurt@computer.org>
|
|
@@ -232,6 +232,30 @@ See [OFFLINE_EVALUATION.md](OFFLINE_EVALUATION.md) for detailed usage and design
|
|
|
232
232
|
|
|
233
233
|
---
|
|
234
234
|
|
|
235
|
+
## Performance
|
|
236
|
+
|
|
237
|
+
`evaluators` is built for high-throughput I/O. The following benchmarks were conducted using the **Comprehensive Memory Evaluation Suite (CMES)** on a mobile workstation (i7-12700H, NVMe SSD).
|
|
238
|
+
|
|
239
|
+
### Throughput (FPS)
|
|
240
|
+
|
|
241
|
+
| Backend | Codec | Resolution | Write FPS | Read FPS | Compression |
|
|
242
|
+
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
243
|
+
| **Memmap** | Raw | 512x1024 | 250.8 | 501.6 | 1.0x |
|
|
244
|
+
| **Memmap** | Blosc | 512x1024 | 59.7 | 119.3 | 0.85x |
|
|
245
|
+
| **LMDB** | Raw | 512x1024 | 13.3 | 26.7 | 1.0x |
|
|
246
|
+
| **LMDB** | PNG | 512x1024 | 27.2 | 13.6 | 0.62x |
|
|
247
|
+
| **Filesystem**| TIFF | 512x1024 | 124.9 | 249.7 | 0.89x |
|
|
248
|
+
|
|
249
|
+
### Insights
|
|
250
|
+
|
|
251
|
+
- **Memmap is king for raw throughput:** The `MemmapTemporalWriter` achieves >500 FPS for read operations on mid-resolution video frames, making it ideal for fast metric computation.
|
|
252
|
+
- **Blosc provides the best balance:** Using `BloscCodec` with `Memmap` offers significant storage savings with minimal CPU overhead compared to PNG/TIFF.
|
|
253
|
+
- **LMDB for stability:** While slower for sequential video access, LMDB provides robust ACID compliance and is preferred for random-access metadata or small feature vectors.
|
|
254
|
+
|
|
255
|
+
> Full benchmark reports, including memory usage (Peak RSS) and CPU overhead plots, are available in `docs/benchmarks/`.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
235
259
|
## Development
|
|
236
260
|
|
|
237
261
|
This project uses modern Python tooling for dependency management and quality assurance.
|
|
@@ -191,6 +191,30 @@ See [OFFLINE_EVALUATION.md](OFFLINE_EVALUATION.md) for detailed usage and design
|
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
194
|
+
## Performance
|
|
195
|
+
|
|
196
|
+
`evaluators` is built for high-throughput I/O. The following benchmarks were conducted using the **Comprehensive Memory Evaluation Suite (CMES)** on a mobile workstation (i7-12700H, NVMe SSD).
|
|
197
|
+
|
|
198
|
+
### Throughput (FPS)
|
|
199
|
+
|
|
200
|
+
| Backend | Codec | Resolution | Write FPS | Read FPS | Compression |
|
|
201
|
+
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
202
|
+
| **Memmap** | Raw | 512x1024 | 250.8 | 501.6 | 1.0x |
|
|
203
|
+
| **Memmap** | Blosc | 512x1024 | 59.7 | 119.3 | 0.85x |
|
|
204
|
+
| **LMDB** | Raw | 512x1024 | 13.3 | 26.7 | 1.0x |
|
|
205
|
+
| **LMDB** | PNG | 512x1024 | 27.2 | 13.6 | 0.62x |
|
|
206
|
+
| **Filesystem**| TIFF | 512x1024 | 124.9 | 249.7 | 0.89x |
|
|
207
|
+
|
|
208
|
+
### Insights
|
|
209
|
+
|
|
210
|
+
- **Memmap is king for raw throughput:** The `MemmapTemporalWriter` achieves >500 FPS for read operations on mid-resolution video frames, making it ideal for fast metric computation.
|
|
211
|
+
- **Blosc provides the best balance:** Using `BloscCodec` with `Memmap` offers significant storage savings with minimal CPU overhead compared to PNG/TIFF.
|
|
212
|
+
- **LMDB for stability:** While slower for sequential video access, LMDB provides robust ACID compliance and is preferred for random-access metadata or small feature vectors.
|
|
213
|
+
|
|
214
|
+
> Full benchmark reports, including memory usage (Peak RSS) and CPU overhead plots, are available in `docs/benchmarks/`.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
194
218
|
## Development
|
|
195
219
|
|
|
196
220
|
This project uses modern Python tooling for dependency management and quality assurance.
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "evaluators"
|
|
7
|
-
version = "2.1.
|
|
7
|
+
version = "2.1.4"
|
|
8
8
|
description = "Various scene understanding and perception evaluation metrics."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13, <3.14"
|
|
@@ -60,13 +60,27 @@ dev = [
|
|
|
60
60
|
"ty>=0.0.17",
|
|
61
61
|
"pytest-timeout>=2.4.0",
|
|
62
62
|
"uv-build>=0.10.9",
|
|
63
|
+
"pycocotools>=2.0.11",
|
|
64
|
+
"seaborn>=0.13.2",
|
|
65
|
+
"pandas>=3.0.2",
|
|
63
66
|
]
|
|
64
67
|
docs = ["numpydoc>=1.9.0", "sphinx>=8.2.3", "sphinx-rtd-theme>=3.0.2"]
|
|
65
68
|
notebooks = ["ipykernel>=6.30.1", "ipywidgets>=8.1.7", "jupyterlab>=4.4.9"]
|
|
69
|
+
fixtures = [
|
|
70
|
+
"transformers>=4.46.0",
|
|
71
|
+
"huggingface-hub>=0.25.0",
|
|
72
|
+
"accelerate>=1.0.0",
|
|
73
|
+
"safetensors>=0.4.5",
|
|
74
|
+
"scipy>=1.14.0",
|
|
75
|
+
"tifffile>=2026.3.3",
|
|
76
|
+
"imagecodecs>=2026.3.6",
|
|
77
|
+
"av>=13.0.0",
|
|
78
|
+
]
|
|
66
79
|
|
|
67
80
|
[tool.ruff]
|
|
68
81
|
target-version = 'py313'
|
|
69
82
|
line-length = 88
|
|
83
|
+
exclude = ["third_party"]
|
|
70
84
|
|
|
71
85
|
[tool.ruff.lint]
|
|
72
86
|
select = [
|
|
@@ -107,8 +121,46 @@ ignore = [
|
|
|
107
121
|
"D203",
|
|
108
122
|
"D107", # docstring on __init__
|
|
109
123
|
"PT011", # pytest.raises too broad (restrictive)
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
# Pre-existing style noise that pervades the codebase.
|
|
125
|
+
"D100",
|
|
126
|
+
"D101",
|
|
127
|
+
"D102",
|
|
128
|
+
"D103",
|
|
129
|
+
"D104",
|
|
130
|
+
"D105",
|
|
131
|
+
"D200",
|
|
132
|
+
"D205",
|
|
133
|
+
"D400",
|
|
134
|
+
"D401",
|
|
135
|
+
"E501", # line length (many docstring tables, type hint lines).
|
|
136
|
+
"N806", # uppercase var names (tensor shape conventions).
|
|
137
|
+
"N812",
|
|
138
|
+
"N813",
|
|
139
|
+
"ARG001", # protocol/ABC stubs frequently take unused args.
|
|
140
|
+
"ARG002",
|
|
141
|
+
"C901", # complexity (compute loops are intrinsically complex).
|
|
142
|
+
"PLR0911",
|
|
143
|
+
"PLR0912",
|
|
144
|
+
"PLR0913",
|
|
145
|
+
"PLR0915",
|
|
146
|
+
"PT019", # fixture leading underscore (intentional in benchmarks).
|
|
147
|
+
"RUF001", # ambiguous unicode (math symbols in docstrings).
|
|
148
|
+
"RUF002",
|
|
149
|
+
"RUF003",
|
|
150
|
+
"PTH100",
|
|
151
|
+
"PTH103",
|
|
152
|
+
"PTH105",
|
|
153
|
+
"PTH109",
|
|
154
|
+
"PTH110",
|
|
155
|
+
"PTH118",
|
|
156
|
+
"PTH123",
|
|
157
|
+
"SIM117", # nested with statements.
|
|
158
|
+
"B017", # assertRaises(Exception) is acceptable in smoke tests.
|
|
159
|
+
"B911", # batched(strict=True).
|
|
160
|
+
"NPY002", # np.random.seed legacy use.
|
|
161
|
+
"PGH003", # blanket `# type: ignore`.
|
|
162
|
+
"E741", # ambiguous variable names (e.g. tensor `l`).
|
|
163
|
+
"PT012",
|
|
112
164
|
]
|
|
113
165
|
|
|
114
166
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -118,6 +170,28 @@ ignore = [
|
|
|
118
170
|
"E402", # Module level import not at top of file
|
|
119
171
|
]
|
|
120
172
|
|
|
173
|
+
[tool.ruff.lint.pydocstyle]
|
|
174
|
+
convention = "numpy"
|
|
175
|
+
ignore-decorators = ["typing.overload", "typing.override"]
|
|
176
|
+
|
|
177
|
+
[tool.ruff.lint.pylint]
|
|
178
|
+
max-args = 12
|
|
179
|
+
max-bool-expr = 8
|
|
180
|
+
max-branches = 16
|
|
181
|
+
|
|
182
|
+
[tool.ruff.lint.flake8-unused-arguments]
|
|
183
|
+
ignore-variadic-names = true
|
|
184
|
+
|
|
185
|
+
[tool.ruff.lint.flake8-import-conventions.aliases]
|
|
186
|
+
"matplotlib.pyplot" = "plt"
|
|
187
|
+
numpy = "np"
|
|
188
|
+
pandas = "pd"
|
|
189
|
+
seaborn = "sns"
|
|
190
|
+
"torchvision.transforms.v2" = "tvt"
|
|
191
|
+
"torchvision.transforms.v2.functional" = "tvt_fn"
|
|
192
|
+
"unipercept" = "up"
|
|
193
|
+
"hydra_zen" = "hz"
|
|
194
|
+
|
|
121
195
|
[tool.ruff.format]
|
|
122
196
|
docstring-code-format = true
|
|
123
197
|
docstring-code-line-length = "dynamic"
|
|
@@ -135,3 +209,9 @@ include = ["sources", "tests", "notebooks", "scripts"]
|
|
|
135
209
|
|
|
136
210
|
[tool.pytest.ini_options]
|
|
137
211
|
timeout = 10
|
|
212
|
+
markers = [
|
|
213
|
+
"integration: integration tests that exercise real pretrained model inference end-to-end (may require network for first-time weight download).",
|
|
214
|
+
"slow: tests with high wall-clock cost.",
|
|
215
|
+
"gpu: tests that require an accelerator (CUDA/ROCm).",
|
|
216
|
+
"serial: tests that must not co-run with siblings (large RAM allocation, GPU contention, or non-thread-safe global state); pytest-xdist groups them onto a single worker via ``xdist_group('serial')``.",
|
|
217
|
+
]
|
|
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING
|
|
|
22
22
|
from ._engine import run_offline_evaluation
|
|
23
23
|
from ._streams import BackendInterface, MetricStream, UnboundStreamWarning
|
|
24
24
|
from .memory import (
|
|
25
|
+
GDSTemporalWriter,
|
|
25
26
|
LMDBTemporalWriter,
|
|
26
27
|
MemmapTemporalWriter,
|
|
27
28
|
MemorySchema,
|
|
@@ -36,6 +37,7 @@ if TYPE_CHECKING:
|
|
|
36
37
|
__version__: str
|
|
37
38
|
__all__ = [
|
|
38
39
|
"BackendInterface",
|
|
40
|
+
"GDSTemporalWriter",
|
|
39
41
|
"LMDBTemporalWriter",
|
|
40
42
|
"MemmapTemporalWriter",
|
|
41
43
|
"MemorySchema",
|
|
@@ -193,12 +193,16 @@ def _evaluate_metrics(
|
|
|
193
193
|
for i, item in enumerate(work_items):
|
|
194
194
|
fragments = item.entry["fragments"]
|
|
195
195
|
sequence = open_virtual_sequence(artifact_path, item.sequence_id, fragments)
|
|
196
|
+
seq_len = len(sequence)
|
|
197
|
+
|
|
198
|
+
for metric in metrics:
|
|
199
|
+
metric.on_sequence_start(seq_len)
|
|
196
200
|
|
|
197
201
|
if show_progress:
|
|
198
202
|
with rich.progress.Progress() as progress:
|
|
199
203
|
task_total = progress.add_task(
|
|
200
204
|
f"Processing item {i + 1}/{len(work_items)}",
|
|
201
|
-
total=len(metrics) *
|
|
205
|
+
total=len(metrics) * seq_len,
|
|
202
206
|
)
|
|
203
207
|
|
|
204
208
|
# O11: Direct iteration avoids DataLoader IPC overhead.
|
|
@@ -222,6 +226,9 @@ def _evaluate_metrics(
|
|
|
222
226
|
for metric in metrics:
|
|
223
227
|
metric.update(**metric._map_batch_to_args(frame))
|
|
224
228
|
|
|
229
|
+
for metric in metrics:
|
|
230
|
+
metric.on_sequence_end()
|
|
231
|
+
|
|
225
232
|
sequence = None # Deallocate
|
|
226
233
|
|
|
227
234
|
# --- AGGREGATION ---
|
|
@@ -238,8 +245,8 @@ def _evaluate_metrics(
|
|
|
238
245
|
metric_name = metric.__class__.__name__
|
|
239
246
|
try:
|
|
240
247
|
score = metric.compute()
|
|
241
|
-
except Exception
|
|
242
|
-
logger.
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.exception("Failed to compute %s", metric_name)
|
|
243
250
|
continue
|
|
244
251
|
|
|
245
252
|
if isinstance(score, MetricResult):
|
|
@@ -173,6 +173,15 @@ class MetricStream(nn.Module):
|
|
|
173
173
|
"""
|
|
174
174
|
self._backend = backend
|
|
175
175
|
|
|
176
|
+
def unbind(self) -> None:
|
|
177
|
+
"""Detach the backend, dropping the stream's reference to the writer.
|
|
178
|
+
|
|
179
|
+
Called by the infrastructure layer at the end of the validation epoch
|
|
180
|
+
so that the writer (and its memory-mapped buffers) can be reclaimed
|
|
181
|
+
without waiting for the next ``bind()`` to overwrite the slot.
|
|
182
|
+
"""
|
|
183
|
+
self._backend = None
|
|
184
|
+
|
|
176
185
|
@torch.compiler.disable
|
|
177
186
|
def update(self, batch: TensorDict) -> None:
|
|
178
187
|
"""
|
|
@@ -220,11 +229,12 @@ class MetricStream(nn.Module):
|
|
|
220
229
|
def _handle_unbound_state(self) -> None:
|
|
221
230
|
"""Handle the missing backend case with configurable tolerance."""
|
|
222
231
|
if self.strict:
|
|
223
|
-
|
|
232
|
+
msg = (
|
|
224
233
|
f"MetricStream '{self.name}' received data but is not bound to "
|
|
225
234
|
"a backend. Ensure DistributedEvaluationCallback is present "
|
|
226
235
|
"in the Trainer or set strict=False."
|
|
227
236
|
)
|
|
237
|
+
raise RuntimeError(msg)
|
|
228
238
|
|
|
229
239
|
if not self._has_warned:
|
|
230
240
|
warnings.warn(
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Coordinator for the Comprehensive Memory Evaluation Suite (CMES).
|
|
3
|
+
|
|
4
|
+
Manages the benchmark grid, spawns worker subprocesses, and aggregates results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import itertools
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import seaborn as sns
|
|
20
|
+
|
|
21
|
+
from .sys_info import get_machine_info, get_machine_slug
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _plot_results(df: pd.DataFrame, out_dir: Path) -> None:
|
|
27
|
+
"""Generate benchmark visualizations."""
|
|
28
|
+
sns.set_theme(style="whitegrid")
|
|
29
|
+
|
|
30
|
+
# 1. FPS Plot (Write & Read)
|
|
31
|
+
plot_df = df.melt(
|
|
32
|
+
id_vars=["backend", "codec", "complexity", "resolution"],
|
|
33
|
+
value_vars=["write_fps_mean", "read_fps_mean"],
|
|
34
|
+
var_name="op",
|
|
35
|
+
value_name="fps",
|
|
36
|
+
)
|
|
37
|
+
plot_df["op"] = plot_df["op"].str.replace("_fps_mean", "").str.capitalize()
|
|
38
|
+
|
|
39
|
+
g = sns.catplot(
|
|
40
|
+
data=plot_df,
|
|
41
|
+
kind="bar",
|
|
42
|
+
x="codec",
|
|
43
|
+
y="fps",
|
|
44
|
+
hue="backend",
|
|
45
|
+
col="op",
|
|
46
|
+
row="resolution",
|
|
47
|
+
height=4,
|
|
48
|
+
aspect=1.5,
|
|
49
|
+
sharey=False,
|
|
50
|
+
)
|
|
51
|
+
g.set(yscale="log")
|
|
52
|
+
g.fig.suptitle("Backend Throughput (FPS) - Log Scale", y=1.02)
|
|
53
|
+
g.savefig(out_dir / "throughput_fps.png")
|
|
54
|
+
plt.close()
|
|
55
|
+
|
|
56
|
+
# 2. MB/s Plot (Write & Read)
|
|
57
|
+
plot_mb = df.melt(
|
|
58
|
+
id_vars=["backend", "codec", "complexity", "resolution"],
|
|
59
|
+
value_vars=["write_mb_s_mean", "read_mb_s_mean"],
|
|
60
|
+
var_name="op",
|
|
61
|
+
value_name="mb_s",
|
|
62
|
+
)
|
|
63
|
+
plot_mb["op"] = plot_mb["op"].str.replace("_mb_s_mean", "").str.capitalize()
|
|
64
|
+
|
|
65
|
+
g = sns.catplot(
|
|
66
|
+
data=plot_mb,
|
|
67
|
+
kind="bar",
|
|
68
|
+
x="codec",
|
|
69
|
+
y="mb_s",
|
|
70
|
+
hue="backend",
|
|
71
|
+
col="op",
|
|
72
|
+
row="resolution",
|
|
73
|
+
height=4,
|
|
74
|
+
aspect=1.5,
|
|
75
|
+
sharey=False,
|
|
76
|
+
)
|
|
77
|
+
g.fig.suptitle("Backend Throughput (MB/s)", y=1.02)
|
|
78
|
+
g.savefig(out_dir / "throughput_mb_s.png")
|
|
79
|
+
plt.close()
|
|
80
|
+
|
|
81
|
+
# 3. Efficiency Plot
|
|
82
|
+
plt.figure(figsize=(12, 6))
|
|
83
|
+
sns.barplot(
|
|
84
|
+
data=df,
|
|
85
|
+
x="codec",
|
|
86
|
+
y="compression_ratio_mean",
|
|
87
|
+
hue="backend",
|
|
88
|
+
)
|
|
89
|
+
plt.title("Storage Efficiency (Compression Ratio)")
|
|
90
|
+
plt.ylabel("Ratio (Raw / Disk)")
|
|
91
|
+
plt.savefig(out_dir / "storage_efficiency.png")
|
|
92
|
+
plt.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _run_single_config(config: dict[str, Any], temp_dir: Path) -> dict[str, Any]:
|
|
96
|
+
"""Spawn a worker for a single run and return the stats."""
|
|
97
|
+
cmd = [
|
|
98
|
+
sys.executable,
|
|
99
|
+
"-m",
|
|
100
|
+
"evaluators.benchmarks.worker",
|
|
101
|
+
"--config",
|
|
102
|
+
json.dumps(config),
|
|
103
|
+
"--temp_dir",
|
|
104
|
+
str(temp_dir),
|
|
105
|
+
]
|
|
106
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
107
|
+
|
|
108
|
+
if result.returncode != 0:
|
|
109
|
+
msg = f"Worker failed with code {result.returncode}: {result.stderr}"
|
|
110
|
+
raise RuntimeError(msg)
|
|
111
|
+
|
|
112
|
+
output_lines = result.stdout.strip().splitlines()
|
|
113
|
+
if not output_lines:
|
|
114
|
+
msg = f"Worker produced no output: {result.stderr}"
|
|
115
|
+
raise RuntimeError(msg)
|
|
116
|
+
|
|
117
|
+
return json.loads(output_lines[-1])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_grid(
|
|
121
|
+
iters: int = 3,
|
|
122
|
+
limit_scenarios: bool = False,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Execute the benchmark grid."""
|
|
125
|
+
slug = get_machine_slug()
|
|
126
|
+
out_dir = Path(f"docs/benchmarks/{slug}")
|
|
127
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
with (out_dir / "machine_info.json").open("w", encoding="utf-8") as f:
|
|
130
|
+
json.dump(get_machine_info(), f, indent=2)
|
|
131
|
+
|
|
132
|
+
backends = ["memmap", "lmdb", "filesystem", "gds"]
|
|
133
|
+
codecs = ["raw", "png", "tiff", "blosc"]
|
|
134
|
+
complexities = ["dense", "ragged", "sparse"]
|
|
135
|
+
resolutions = {
|
|
136
|
+
"feat1d": (1024,),
|
|
137
|
+
"low": (128, 256),
|
|
138
|
+
"mid": (512, 1024),
|
|
139
|
+
"high": (1024, 2048),
|
|
140
|
+
"feat3d": (64, 256, 512),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if limit_scenarios:
|
|
144
|
+
backends, codecs, complexities = (
|
|
145
|
+
["memmap", "lmdb", "gds"],
|
|
146
|
+
["raw", "blosc"],
|
|
147
|
+
["dense"],
|
|
148
|
+
)
|
|
149
|
+
resolutions = {"low": (128, 256)}
|
|
150
|
+
|
|
151
|
+
raw_results: list[dict[str, Any]] = []
|
|
152
|
+
permutations = list(
|
|
153
|
+
itertools.product(backends, codecs, complexities, resolutions.keys())
|
|
154
|
+
)
|
|
155
|
+
total = len(permutations) * iters
|
|
156
|
+
|
|
157
|
+
count = 0
|
|
158
|
+
for b_name, codec_name, comp, res_name in permutations:
|
|
159
|
+
# Skip invalid combinations
|
|
160
|
+
if codec_name in ["png", "tiff"] and res_name in ["feat1d", "feat3d"]:
|
|
161
|
+
continue
|
|
162
|
+
if comp != "dense" and res_name != "mid" and not limit_scenarios:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
hw = resolutions[res_name]
|
|
166
|
+
shape = (1, *hw) if comp != "ragged" else (None, 4)
|
|
167
|
+
dtype = "uint8" if codec_name == "png" else "float32"
|
|
168
|
+
|
|
169
|
+
fields = {"data": {"dtype": dtype, "shape": list(shape), "codec": codec_name}}
|
|
170
|
+
if comp == "sparse":
|
|
171
|
+
fields["uncertainty"] = {
|
|
172
|
+
"dtype": dtype,
|
|
173
|
+
"shape": list(shape),
|
|
174
|
+
"codec": "raw",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
config = {
|
|
178
|
+
"backend": b_name,
|
|
179
|
+
"codec": codec_name,
|
|
180
|
+
"complexity": comp,
|
|
181
|
+
"resolution": res_name,
|
|
182
|
+
"b": 1,
|
|
183
|
+
"t": 10,
|
|
184
|
+
"schema": {"fields": fields},
|
|
185
|
+
"backend_opts": {},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if b_name == "memmap":
|
|
189
|
+
config["backend_opts"]["chunk_size_frames"] = 1000
|
|
190
|
+
elif b_name == "lmdb":
|
|
191
|
+
config["backend_opts"]["map_size"] = 10 * 1024 * 1024 * 1024
|
|
192
|
+
|
|
193
|
+
for run_idx in range(iters):
|
|
194
|
+
count += 1
|
|
195
|
+
logger.info(
|
|
196
|
+
"[%d/%d] %s | %s | %s | %s | Run %d",
|
|
197
|
+
count,
|
|
198
|
+
total,
|
|
199
|
+
b_name,
|
|
200
|
+
codec_name,
|
|
201
|
+
comp,
|
|
202
|
+
res_name,
|
|
203
|
+
run_idx,
|
|
204
|
+
)
|
|
205
|
+
temp_path = Path(
|
|
206
|
+
f"tmp_bench_{b_name}_{codec_name}_{comp}_{res_name}_{run_idx}"
|
|
207
|
+
)
|
|
208
|
+
temp_path.mkdir(exist_ok=True)
|
|
209
|
+
try:
|
|
210
|
+
run_stats = _run_single_config(config, temp_path)
|
|
211
|
+
frames = run_stats["frames"]
|
|
212
|
+
# Calculate raw bytes for MB/s reporting
|
|
213
|
+
element_size = 4 if dtype == "float32" else 1
|
|
214
|
+
raw_bytes_per_frame = (
|
|
215
|
+
np.prod([s if s else 100 for s in shape]) * element_size
|
|
216
|
+
)
|
|
217
|
+
raw_bytes_total = frames * raw_bytes_per_frame
|
|
218
|
+
|
|
219
|
+
disk_bytes = run_stats["disk_bytes"]
|
|
220
|
+
run_stats.update(
|
|
221
|
+
{
|
|
222
|
+
"backend": b_name,
|
|
223
|
+
"codec": codec_name,
|
|
224
|
+
"complexity": comp,
|
|
225
|
+
"resolution": res_name,
|
|
226
|
+
"run": run_idx,
|
|
227
|
+
"write_fps": frames / run_stats["write_s"],
|
|
228
|
+
"read_fps": frames / run_stats["read_s"],
|
|
229
|
+
"write_mb_s": (raw_bytes_total / (1024**2))
|
|
230
|
+
/ run_stats["write_s"],
|
|
231
|
+
"read_mb_s": (raw_bytes_total / (1024**2))
|
|
232
|
+
/ run_stats["read_s"],
|
|
233
|
+
"compression_ratio": raw_bytes_total / disk_bytes
|
|
234
|
+
if disk_bytes > 0
|
|
235
|
+
else 1.0,
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
raw_results.append(run_stats)
|
|
239
|
+
except Exception:
|
|
240
|
+
logger.exception(" FAILED permutation: %s", b_name)
|
|
241
|
+
finally:
|
|
242
|
+
if temp_path.exists():
|
|
243
|
+
shutil.rmtree(temp_path)
|
|
244
|
+
|
|
245
|
+
if not raw_results:
|
|
246
|
+
logger.error("No results collected.")
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
df = pd.DataFrame(raw_results)
|
|
250
|
+
stats = (
|
|
251
|
+
df.groupby(["backend", "codec", "complexity", "resolution"])
|
|
252
|
+
.agg(
|
|
253
|
+
{
|
|
254
|
+
"write_fps": ["mean", "std"],
|
|
255
|
+
"read_fps": ["mean", "std"],
|
|
256
|
+
"write_mb_s": ["mean", "std"],
|
|
257
|
+
"read_mb_s": ["mean", "std"],
|
|
258
|
+
"peak_rss_mb": ["mean", "max"],
|
|
259
|
+
"compression_ratio": ["mean"],
|
|
260
|
+
"avg_cpu": ["mean"],
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
.reset_index()
|
|
264
|
+
)
|
|
265
|
+
stats.columns = ["_".join(col).strip("_") for col in stats.columns.values]
|
|
266
|
+
stats.to_csv(out_dir / "backend_comparison.csv", index=False)
|
|
267
|
+
logger.info("Results saved to %s", out_dir / "backend_comparison.csv")
|
|
268
|
+
|
|
269
|
+
_plot_results(stats, out_dir)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
274
|
+
run_grid(limit_scenarios=True)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scenario generators for benchmarking.
|
|
3
|
+
====================================
|
|
4
|
+
|
|
5
|
+
Provides data generation for different complexity levels (dense, ragged, sparse).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import random
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
from tensordict import TensorDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_ragged_batch(
|
|
15
|
+
b: int, t: int, max_n: int = 150, key: str = "data", dtype: str = "float32"
|
|
16
|
+
) -> TensorDict:
|
|
17
|
+
"""
|
|
18
|
+
Generate a batch of ragged tensors (e.g., variable number of detections).
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
b : int
|
|
23
|
+
Batch size.
|
|
24
|
+
t : int
|
|
25
|
+
Number of time steps.
|
|
26
|
+
max_n : int
|
|
27
|
+
Maximum number of elements in the second dimension.
|
|
28
|
+
key : str
|
|
29
|
+
The key name for the data in the TensorDict.
|
|
30
|
+
dtype : str
|
|
31
|
+
The torch dtype string.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
TensorDict
|
|
36
|
+
A TensorDict of shape (b, t) containing ragged data (padded).
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
torch_dtype = getattr(torch, dtype)
|
|
40
|
+
data_list: list[torch.Tensor] = []
|
|
41
|
+
for _ in range(b * t):
|
|
42
|
+
n = random.randint(1, max_n)
|
|
43
|
+
if dtype.startswith("float"):
|
|
44
|
+
data_list.append(torch.randn(n, 4, dtype=torch_dtype))
|
|
45
|
+
else:
|
|
46
|
+
data_list.append(torch.randint(0, 20, (n, 4), dtype=torch_dtype))
|
|
47
|
+
|
|
48
|
+
# Pad sequences to the max length in the batch
|
|
49
|
+
data = torch.nn.utils.rnn.pad_sequence(data_list, batch_first=True)
|
|
50
|
+
|
|
51
|
+
# Reshape to (b, t, N, 4)
|
|
52
|
+
N = data.shape[1]
|
|
53
|
+
return TensorDict({key: data.view(b, t, N, 4)}, batch_size=[b, t])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def generate_sparse_batch(
|
|
57
|
+
b: int,
|
|
58
|
+
t: int,
|
|
59
|
+
p_sparse: float = 0.3,
|
|
60
|
+
key: str = "data",
|
|
61
|
+
dtype: str = "float32",
|
|
62
|
+
shape: tuple[int, ...] = (1, 512, 512),
|
|
63
|
+
) -> TensorDict:
|
|
64
|
+
"""
|
|
65
|
+
Generate a batch of sparse dictionary keys (optional fields).
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
b : int
|
|
70
|
+
Batch size.
|
|
71
|
+
t : int
|
|
72
|
+
Number of time steps.
|
|
73
|
+
p_sparse : float
|
|
74
|
+
Probability of an optional key being present.
|
|
75
|
+
key : str
|
|
76
|
+
The key name for the data in the TensorDict.
|
|
77
|
+
dtype : str
|
|
78
|
+
The torch dtype string.
|
|
79
|
+
shape : tuple[int, ...]
|
|
80
|
+
The spatial shape of the tensors.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
TensorDict
|
|
85
|
+
A TensorDict of shape (b, t) containing sparse keys.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
torch_dtype = getattr(torch, dtype)
|
|
89
|
+
# Primary data key
|
|
90
|
+
if dtype.startswith("float"):
|
|
91
|
+
data = torch.randn(b, t, *shape, dtype=torch_dtype)
|
|
92
|
+
else:
|
|
93
|
+
data = torch.randint(0, 255, (b, t, *shape), dtype=torch_dtype)
|
|
94
|
+
|
|
95
|
+
batch = TensorDict({key: data}, batch_size=[b, t])
|
|
96
|
+
|
|
97
|
+
# Scenario B: Optional keys present in only some frames
|
|
98
|
+
uncertainty = torch.zeros(b, t, *shape, dtype=torch_dtype)
|
|
99
|
+
mask = torch.rand(b, t) < p_sparse
|
|
100
|
+
if mask.any():
|
|
101
|
+
num_present = mask.sum().item()
|
|
102
|
+
if dtype.startswith("float"):
|
|
103
|
+
uncertainty[mask] = torch.rand(num_present, *shape, dtype=torch_dtype)
|
|
104
|
+
else:
|
|
105
|
+
uncertainty[mask] = torch.randint(
|
|
106
|
+
0, 255, (num_present, *shape), dtype=torch_dtype
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
batch["uncertainty"] = uncertainty
|
|
110
|
+
return batch
|