evaluators 2.1.3__tar.gz → 3.0.0__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.
Files changed (93) hide show
  1. {evaluators-2.1.3 → evaluators-3.0.0}/PKG-INFO +29 -4
  2. {evaluators-2.1.3 → evaluators-3.0.0}/README.md +27 -3
  3. {evaluators-2.1.3 → evaluators-3.0.0}/pyproject.toml +92 -3
  4. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/__init__.py +2 -0
  5. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/_engine.py +11 -4
  6. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/_streams.py +12 -4
  7. evaluators-3.0.0/sources/evaluators/benchmarks/coordinator.py +274 -0
  8. evaluators-3.0.0/sources/evaluators/benchmarks/scenarios.py +110 -0
  9. evaluators-3.0.0/sources/evaluators/benchmarks/sys_info.py +228 -0
  10. evaluators-3.0.0/sources/evaluators/benchmarks/worker.py +152 -0
  11. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/cli/__main__.py +1 -5
  12. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/cli/compute.py +50 -10
  13. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/cli/memory.py +93 -80
  14. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/cli/third_party.py +19 -24
  15. evaluators-3.0.0/sources/evaluators/integrations/lightning.py +440 -0
  16. evaluators-3.0.0/sources/evaluators/io/__init__.py +7 -0
  17. evaluators-3.0.0/sources/evaluators/io/mot_challenge.py +136 -0
  18. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/__init__.py +36 -2
  19. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/__init__.py +2 -1
  20. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/_registry.py +56 -10
  21. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/filesystem/_reader.py +4 -7
  22. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/filesystem/_writer.py +4 -2
  23. evaluators-3.0.0/sources/evaluators/memory/backends/gds/__init__.py +3 -0
  24. evaluators-3.0.0/sources/evaluators/memory/backends/gds/reader.py +87 -0
  25. evaluators-3.0.0/sources/evaluators/memory/backends/gds/utils.py +17 -0
  26. evaluators-3.0.0/sources/evaluators/memory/backends/gds/writer.py +168 -0
  27. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/lmdb/_reader.py +44 -28
  28. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/lmdb/_writer.py +18 -2
  29. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/_buffers.py +30 -11
  30. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/_reader.py +32 -11
  31. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/_writer.py +56 -7
  32. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_blosc.py +1 -1
  33. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/config.py +6 -3
  34. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/interfaces.py +11 -22
  35. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/schema.py +1 -3
  36. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/metrics/domain/depth.py +5 -11
  37. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/metrics/domain/dvps.py +137 -79
  38. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/__init__.py +4 -0
  39. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/_utils.py +11 -17
  40. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/panoptic.py +410 -78
  41. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/semantic.py +19 -28
  42. evaluators-3.0.0/sources/evaluators/metrics/domain/segmentation/vendor/__init__.py +7 -0
  43. evaluators-3.0.0/sources/evaluators/metrics/domain/segmentation/vendor/panopticapi.py +252 -0
  44. evaluators-3.0.0/sources/evaluators/metrics/domain/step.py +794 -0
  45. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/__init__.py +16 -0
  46. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_base.py +803 -0
  47. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_classcomb.py +73 -0
  48. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_clear.py +132 -0
  49. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_combine.py +38 -0
  50. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_hota.py +125 -0
  51. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_identity.py +66 -0
  52. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_preprocess.py +263 -0
  53. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_seqdata.py +119 -0
  54. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_similarity.py +208 -0
  55. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/metrics/stats.py +3 -7
  56. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/presets/__init__.py +3 -2
  57. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/presets/cityscapes.py +60 -7
  58. evaluators-3.0.0/sources/evaluators/presets/mot.py +228 -0
  59. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/presets/semkitti.py +60 -1
  60. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/scheduling/_catalog.py +3 -6
  61. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/third_party/__init__.py +0 -1
  62. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/third_party/adapters/__init__.py +0 -1
  63. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/third_party/adapters/coco.py +30 -10
  64. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/third_party/adapters/dvpq.py +5 -21
  65. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/types.py +52 -2
  66. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/utils/comms.py +1 -3
  67. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/utils/configs.py +2 -4
  68. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/utils/tensor_ops.py +5 -8
  69. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/visuals/__init__.py +1 -1
  70. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/visuals/metric.py +7 -14
  71. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/visuals/tools/__init__.py +6 -0
  72. evaluators-3.0.0/sources/evaluators/visuals/tools/_visualizer.py +1414 -0
  73. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/visuals/tools/transform.py +10 -24
  74. evaluators-2.1.3/sources/evaluators/integrations/lightning.py +0 -265
  75. evaluators-2.1.3/sources/evaluators/metrics/domain/step.py +0 -393
  76. evaluators-2.1.3/sources/evaluators/visuals/tools/_visualizer.py +0 -779
  77. {evaluators-2.1.3/sources/evaluators/metrics → evaluators-3.0.0/sources/evaluators/benchmarks}/__init__.py +0 -0
  78. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/constants.py +0 -0
  79. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/filesystem/__init__.py +0 -0
  80. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/lmdb/__init__.py +0 -0
  81. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/__init__.py +0 -0
  82. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/codecs/__init__.py +0 -0
  83. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_png.py +0 -0
  84. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_raw.py +0 -0
  85. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_tiff.py +0 -0
  86. /evaluators-2.1.3/sources/evaluators/metrics/adapters/coco.py → /evaluators-3.0.0/sources/evaluators/metrics/__init__.py +0 -0
  87. /evaluators-2.1.3/sources/evaluators/metrics/adapters/trackeval.py → /evaluators-3.0.0/sources/evaluators/metrics/adapters/coco.py +0 -0
  88. /evaluators-2.1.3/sources/evaluators/metrics/domain/tracking.py → /evaluators-3.0.0/sources/evaluators/metrics/adapters/trackeval.py +0 -0
  89. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/presets/kitti.py +0 -0
  90. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/scheduling/__init__.py +0 -0
  91. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/scheduling/_partitioner.py +0 -0
  92. {evaluators-2.1.3 → evaluators-3.0.0}/sources/evaluators/third_party/__main__.py +0 -0
  93. {evaluators-2.1.3 → evaluators-3.0.0}/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
3
+ Version: 3.0.0
4
4
  Summary: Various scene understanding and perception evaluation metrics.
5
5
  Author: Kurt Stolle
6
6
  Author-email: Kurt Stolle <kurt@computer.org>
@@ -27,6 +27,7 @@ Requires-Dist: pydantic
27
27
  Requires-Dist: opencv-python>=4.13.0.92
28
28
  Requires-Dist: omegaconf>=2.3.0
29
29
  Requires-Dist: hydra-zen>=0.16.0
30
+ Requires-Dist: scipy>=1.14.0
30
31
  Requires-Dist: blosc2>=4.1.2 ; extra == 'blosc2'
31
32
  Requires-Dist: lightning>=2.6.1 ; extra == 'lightning'
32
33
  Requires-Dist: lmdb>=2.2.0 ; extra == 'lmdb'
@@ -103,7 +104,7 @@ from evaluators.metrics.domain.segmentation import SemanticMetric
103
104
  dataset = CityscapesDataset(...)
104
105
  metric = SemanticMetric(
105
106
  num_classes=19,
106
- target_source=dataset,
107
+ target_source=dataset,
107
108
  )
108
109
 
109
110
  ### Define the explicit memory schema
@@ -144,12 +145,12 @@ for batch in dataloader: # Model forward pass
144
145
  "frame_index": batch["frame_index"]
145
146
  }
146
147
  )
147
-
148
+
148
149
  # Finalize
149
150
  writer.close()
150
151
  ```
151
152
 
152
- > Note: `sequence_id` and `frame_index` must be `torch.int64`.
153
+ > Note: `sequence_id` and `frame_index` must be `torch.int64`.
153
154
 
154
155
  ##### 4. Finalize and compute
155
156
 
@@ -232,6 +233,30 @@ See [OFFLINE_EVALUATION.md](OFFLINE_EVALUATION.md) for detailed usage and design
232
233
 
233
234
  ---
234
235
 
236
+ ## Performance
237
+
238
+ `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).
239
+
240
+ ### Throughput (FPS)
241
+
242
+ | Backend | Codec | Resolution | Write FPS | Read FPS | Compression |
243
+ | :--- | :--- | :--- | :--- | :--- | :--- |
244
+ | **Memmap** | Raw | 512x1024 | 250.8 | 501.6 | 1.0x |
245
+ | **Memmap** | Blosc | 512x1024 | 59.7 | 119.3 | 0.85x |
246
+ | **LMDB** | Raw | 512x1024 | 13.3 | 26.7 | 1.0x |
247
+ | **LMDB** | PNG | 512x1024 | 27.2 | 13.6 | 0.62x |
248
+ | **Filesystem**| TIFF | 512x1024 | 124.9 | 249.7 | 0.89x |
249
+
250
+ ### Insights
251
+
252
+ - **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.
253
+ - **Blosc provides the best balance:** Using `BloscCodec` with `Memmap` offers significant storage savings with minimal CPU overhead compared to PNG/TIFF.
254
+ - **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.
255
+
256
+ > Full benchmark reports, including memory usage (Peak RSS) and CPU overhead plots, are available in `docs/benchmarks/`.
257
+
258
+ ---
259
+
235
260
  ## Development
236
261
 
237
262
  This project uses modern Python tooling for dependency management and quality assurance.
@@ -62,7 +62,7 @@ from evaluators.metrics.domain.segmentation import SemanticMetric
62
62
  dataset = CityscapesDataset(...)
63
63
  metric = SemanticMetric(
64
64
  num_classes=19,
65
- target_source=dataset,
65
+ target_source=dataset,
66
66
  )
67
67
 
68
68
  ### Define the explicit memory schema
@@ -103,12 +103,12 @@ for batch in dataloader: # Model forward pass
103
103
  "frame_index": batch["frame_index"]
104
104
  }
105
105
  )
106
-
106
+
107
107
  # Finalize
108
108
  writer.close()
109
109
  ```
110
110
 
111
- > Note: `sequence_id` and `frame_index` must be `torch.int64`.
111
+ > Note: `sequence_id` and `frame_index` must be `torch.int64`.
112
112
 
113
113
  ##### 4. Finalize and compute
114
114
 
@@ -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.3"
7
+ version = "3.0.0"
8
8
  description = "Various scene understanding and perception evaluation metrics."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13, <3.14"
@@ -35,6 +35,7 @@ dependencies = [
35
35
  "opencv-python>=4.13.0.92",
36
36
  "omegaconf>=2.3.0",
37
37
  "hydra-zen>=0.16.0",
38
+ "scipy>=1.14.0",
38
39
  ]
39
40
 
40
41
  [project.optional-dependencies]
@@ -60,13 +61,27 @@ dev = [
60
61
  "ty>=0.0.17",
61
62
  "pytest-timeout>=2.4.0",
62
63
  "uv-build>=0.10.9",
64
+ "pycocotools>=2.0.11",
65
+ "seaborn>=0.13.2",
66
+ "pandas>=3.0.2",
63
67
  ]
64
68
  docs = ["numpydoc>=1.9.0", "sphinx>=8.2.3", "sphinx-rtd-theme>=3.0.2"]
65
69
  notebooks = ["ipykernel>=6.30.1", "ipywidgets>=8.1.7", "jupyterlab>=4.4.9"]
70
+ fixtures = [
71
+ "transformers>=4.46.0",
72
+ "huggingface-hub>=0.25.0",
73
+ "accelerate>=1.0.0",
74
+ "safetensors>=0.4.5",
75
+ "scipy>=1.14.0",
76
+ "tifffile>=2026.3.3",
77
+ "imagecodecs>=2026.3.6",
78
+ "av>=13.0.0",
79
+ ]
66
80
 
67
81
  [tool.ruff]
68
82
  target-version = 'py313'
69
83
  line-length = 88
84
+ exclude = ["third_party"]
70
85
 
71
86
  [tool.ruff.lint]
72
87
  select = [
@@ -103,12 +118,54 @@ ignore = [
103
118
  "SIM118", # usage of 'x in dict.keys()' -- conflicts with `tensordict`.
104
119
  "PLR2004", # magic numbers
105
120
  "PLW2901", # for loop variable overwritten
121
+ # Lazy/deferred imports are a deliberate, pervasive pattern (optional deps:
122
+ # matplotlib, codecs, heavy adapters) used to keep import and CLI startup
123
+ # cheap; PLC0415 (import-should-be-at-top) fights that design codebase-wide.
124
+ "PLC0415",
106
125
  "D212",
107
126
  "D203",
108
127
  "D107", # docstring on __init__
109
128
  "PT011", # pytest.raises too broad (restrictive)
110
-
111
-
129
+ # Pre-existing style noise that pervades the codebase.
130
+ "D100",
131
+ "D101",
132
+ "D102",
133
+ "D103",
134
+ "D104",
135
+ "D105",
136
+ "D200",
137
+ "D205",
138
+ "D400",
139
+ "D401",
140
+ "E501", # line length (many docstring tables, type hint lines).
141
+ "N806", # uppercase var names (tensor shape conventions).
142
+ "N812",
143
+ "N813",
144
+ "ARG001", # protocol/ABC stubs frequently take unused args.
145
+ "ARG002",
146
+ "C901", # complexity (compute loops are intrinsically complex).
147
+ "PLR0911",
148
+ "PLR0912",
149
+ "PLR0913",
150
+ "PLR0915",
151
+ "PT019", # fixture leading underscore (intentional in benchmarks).
152
+ "RUF001", # ambiguous unicode (math symbols in docstrings).
153
+ "RUF002",
154
+ "RUF003",
155
+ "PTH100",
156
+ "PTH103",
157
+ "PTH105",
158
+ "PTH109",
159
+ "PTH110",
160
+ "PTH118",
161
+ "PTH123",
162
+ "SIM117", # nested with statements.
163
+ "B017", # assertRaises(Exception) is acceptable in smoke tests.
164
+ "B911", # batched(strict=True).
165
+ "NPY002", # np.random.seed legacy use.
166
+ "PGH003", # blanket `# type: ignore`.
167
+ "E741", # ambiguous variable names (e.g. tensor `l`).
168
+ "PT012",
112
169
  ]
113
170
 
114
171
  [tool.ruff.lint.per-file-ignores]
@@ -118,6 +175,28 @@ ignore = [
118
175
  "E402", # Module level import not at top of file
119
176
  ]
120
177
 
178
+ [tool.ruff.lint.pydocstyle]
179
+ convention = "numpy"
180
+ ignore-decorators = ["typing.overload", "typing.override"]
181
+
182
+ [tool.ruff.lint.pylint]
183
+ max-args = 12
184
+ max-bool-expr = 8
185
+ max-branches = 16
186
+
187
+ [tool.ruff.lint.flake8-unused-arguments]
188
+ ignore-variadic-names = true
189
+
190
+ [tool.ruff.lint.flake8-import-conventions.aliases]
191
+ "matplotlib.pyplot" = "plt"
192
+ numpy = "np"
193
+ pandas = "pd"
194
+ seaborn = "sns"
195
+ "torchvision.transforms.v2" = "tvt"
196
+ "torchvision.transforms.v2.functional" = "tvt_fn"
197
+ "unipercept" = "up"
198
+ "hydra_zen" = "hz"
199
+
121
200
  [tool.ruff.format]
122
201
  docstring-code-format = true
123
202
  docstring-code-line-length = "dynamic"
@@ -135,3 +214,13 @@ include = ["sources", "tests", "notebooks", "scripts"]
135
214
 
136
215
  [tool.pytest.ini_options]
137
216
  timeout = 10
217
+ # Vendored reference implementations under third_party/ ship their own
218
+ # environment-dependent test suites (e.g. TrackEval); never collect them as
219
+ # part of this project's tests. Mirrors the ruff `exclude = ["third_party"]`.
220
+ addopts = "--ignore=third_party"
221
+ markers = [
222
+ "integration: integration tests that exercise real pretrained model inference end-to-end (may require network for first-time weight download).",
223
+ "slow: tests with high wall-clock cost.",
224
+ "gpu: tests that require an accelerator (CUDA/ROCm).",
225
+ "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')``.",
226
+ ]
@@ -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) * len(sequence),
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 as e:
242
- logger.error("Failed to compute %s: %s", metric_name, e)
248
+ except Exception:
249
+ logger.exception("Failed to compute %s", metric_name)
243
250
  continue
244
251
 
245
252
  if isinstance(score, MetricResult):
@@ -265,7 +272,7 @@ def _save_plots(
265
272
  force: bool,
266
273
  ) -> None:
267
274
  """Call ``plot()`` on each metric and save the figure to *out_path*."""
268
- import matplotlib.pyplot as plt
275
+ import matplotlib.pyplot as plt # lazy: matplotlib is optional
269
276
 
270
277
  for metric in metrics:
271
278
  name = metric.__class__.__name__
@@ -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
  """
@@ -207,9 +216,7 @@ class MetricStream(nn.Module):
207
216
 
208
217
  # 2. Plumbing / Renaming
209
218
  if self.input_map:
210
- # We invert the map to {model_key: metric_key} for the rename operation
211
- # rename_map = {v: k for k, v in self.input_map.items()}
212
- # filtered_batch = filtered_batch.rename_keys_(rename_map, strict=False)
219
+ # We map model keys to metric keys as defined in the input map.
213
220
  for metric_key, model_key in self.input_map.items():
214
221
  if model_key in filtered_batch.keys():
215
222
  filtered_batch.rename_key_(model_key, metric_key)
@@ -220,11 +227,12 @@ class MetricStream(nn.Module):
220
227
  def _handle_unbound_state(self) -> None:
221
228
  """Handle the missing backend case with configurable tolerance."""
222
229
  if self.strict:
223
- raise RuntimeError(
230
+ msg = (
224
231
  f"MetricStream '{self.name}' received data but is not bound to "
225
232
  "a backend. Ensure DistributedEvaluationCallback is present "
226
233
  "in the Trainer or set strict=False."
227
234
  )
235
+ raise RuntimeError(msg)
228
236
 
229
237
  if not self._has_warned:
230
238
  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)