Skip to content

Commit 835a73d

Browse files
authored
fix(pixel-layout): bound in-flight futures to prevent OOM on large datasets (#216)
Signed-off-by: samiuc <sami.ullah.chat@gmail.com>
1 parent b40605e commit 835a73d

1 file changed

Lines changed: 60 additions & 31 deletions

File tree

docling_eval/evaluators/pixel_layout_evaluator.py

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
import logging
44
import math
55
from collections import defaultdict
6-
from concurrent.futures import Executor, Future, ProcessPoolExecutor, as_completed
6+
from concurrent.futures import (
7+
FIRST_COMPLETED,
8+
Executor,
9+
Future,
10+
ProcessPoolExecutor,
11+
as_completed,
12+
wait,
13+
)
714
from enum import Enum
815
from pathlib import Path
916
from typing import Dict, List, Optional, Tuple, Union
@@ -276,10 +283,13 @@ def __call__(
276283
[]
277284
) # Gather f1 score/image when evaluated on collapsed classes
278285

286+
# Keep at most 2× workers in-flight: enough to saturate the pool while
287+
# preventing unbounded accumulation of large uint64 page arrays in memory.
288+
max_pending = self._concurrency * 2
289+
279290
with ProcessPoolExecutor(max_workers=self._concurrency) as executor:
280-
futures: list[Future] = []
291+
pending: set[Future] = set()
281292

282-
# Submit pages for execution
283293
_log.info("Submitting the documents for evaluation...")
284294
for data in ds_selection:
285295
data_record = DatasetRecordWithPrediction.model_validate(data)
@@ -320,43 +330,37 @@ def __call__(
320330
continue
321331

322332
evaluated_samples += 1
323-
futures.extend(
333+
pending.update(
324334
self._submit_document_evaluation(
325335
executor, doc_id, true_doc, pred_doc
326336
)
327337
)
328338

329-
# Collect the futures
330-
_log.info("Collecting the documents for evaluations...")
339+
# Drain completed futures whenever the window is full
340+
while len(pending) >= max_pending:
341+
done, pending = wait(pending, return_when=FIRST_COMPLETED)
342+
for f in done:
343+
ds_num_pixels += self._collect_page_result(
344+
f,
345+
ds_confusion_matrix,
346+
all_pages_evaluations,
347+
pages_detailed_f1,
348+
pages_collapsed_f1,
349+
)
350+
351+
_log.info("Collecting %d remaining page evaluations...", len(pending))
331352
for future in tqdm(
332-
as_completed(futures),
353+
as_completed(pending),
333354
desc="Multi-label Matrix Layout evaluations",
334355
ncols=120,
335-
total=len(futures),
356+
total=len(pending),
336357
):
337-
page_metrics: MultiLabelMatrixEvaluation
338-
doc_id, page_no, page_pixels, page_metrics = future.result()
339-
340-
page_confusion_matrix: np.ndarray = (
341-
page_metrics.detailed.confusion_matrix
342-
)
343-
ds_num_pixels += page_pixels
344-
ds_confusion_matrix += page_confusion_matrix
345-
doc_page_id = f"{doc_id}-{page_no}"
346-
page_evaluation = PagePixelLayoutEvaluation(
347-
doc_id=doc_id,
348-
page_no=page_no,
349-
num_pixels=page_pixels,
350-
matrix_evaluation=page_metrics,
351-
)
352-
all_pages_evaluations[doc_page_id] = page_evaluation
353-
354-
# Update f1 lists
355-
pages_detailed_f1.append(
356-
page_metrics.detailed.agg_metrics.classes_f1_mean
357-
)
358-
pages_collapsed_f1.append(
359-
page_metrics.collapsed.agg_metrics.classes_f1_mean
358+
ds_num_pixels += self._collect_page_result(
359+
future,
360+
ds_confusion_matrix,
361+
all_pages_evaluations,
362+
pages_detailed_f1,
363+
pages_collapsed_f1,
360364
)
361365

362366
# Compute metrics for the dataset
@@ -379,6 +383,31 @@ def __call__(
379383

380384
return ds_evaluation
381385

386+
@staticmethod
387+
def _collect_page_result(
388+
future: Future,
389+
ds_confusion_matrix: np.ndarray,
390+
all_pages_evaluations: Dict[str, "PagePixelLayoutEvaluation"],
391+
pages_detailed_f1: List[float],
392+
pages_collapsed_f1: List[float],
393+
) -> int:
394+
"""Unpack one completed page future into the shared accumulators.
395+
396+
Returns the number of pixels in the page so the caller can accumulate
397+
``ds_num_pixels`` without needing access to the internal state.
398+
"""
399+
doc_id, page_no, page_pixels, page_metrics = future.result()
400+
ds_confusion_matrix[:] += page_metrics.detailed.confusion_matrix
401+
all_pages_evaluations[f"{doc_id}-{page_no}"] = PagePixelLayoutEvaluation(
402+
doc_id=doc_id,
403+
page_no=page_no,
404+
num_pixels=page_pixels,
405+
matrix_evaluation=page_metrics,
406+
)
407+
pages_detailed_f1.append(page_metrics.detailed.agg_metrics.classes_f1_mean)
408+
pages_collapsed_f1.append(page_metrics.collapsed.agg_metrics.classes_f1_mean)
409+
return page_pixels
410+
382411
def save_evaluations(
383412
self,
384413
benchmark: BenchMarkNames,

0 commit comments

Comments
 (0)