From 8bb9a8492744f664146076eabdb2de080250a7f6 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 2 Oct 2026 14:24:30 +0800 Subject: [PATCH 1/2] Advance past invalid MMDet images --- .../integrations/mmdet/mmdet_dataset_.py | 11 ++-- .../tests/test_mmdet_invalid_sample_loop.py | 66 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 python/tests/test_mmdet_invalid_sample_loop.py diff --git a/python/deeplake/integrations/mmdet/mmdet_dataset_.py b/python/deeplake/integrations/mmdet/mmdet_dataset_.py index eaa4183976..9a827473e4 100644 --- a/python/deeplake/integrations/mmdet/mmdet_dataset_.py +++ b/python/deeplake/integrations/mmdet/mmdet_dataset_.py @@ -418,17 +418,20 @@ def __getitem__(self, idx): try: sample = self.dataset[idx] if self.transform: - return self.transform(sample) + result = self.transform(sample) else: out = {} for col in self.column_names: out[col] = sample[col] - return out + result = out + self.last_successful_index = idx + return result except InvalidImageError as e: print(f"Error processing data at index {idx}: {e}") if self.last_successful_index == -1: - self.last_successful_index = idx + 1 - idx = self.last_successful_index + idx += 1 + else: + idx = self.last_successful_index continue diff --git a/python/tests/test_mmdet_invalid_sample_loop.py b/python/tests/test_mmdet_invalid_sample_loop.py new file mode 100644 index 0000000000..47f52063b6 --- /dev/null +++ b/python/tests/test_mmdet_invalid_sample_loop.py @@ -0,0 +1,66 @@ +import ast +import typing +from pathlib import Path + + +class InvalidImageError(Exception): + pass + + +def _load_dataset_class(): + source = ( + Path(__file__).resolve().parents[1] + / "deeplake" + / "integrations" + / "mmdet" + / "mmdet_dataset_.py" + ) + parsed = ast.parse(source.read_text(encoding="utf-8")) + class_node = next( + node + for node in parsed.body + if isinstance(node, ast.ClassDef) and node.name == "MMDetTorchDataset" + ) + isolated = ast.Module(body=[class_node], type_ignores=[]) + ast.fix_missing_locations(isolated) + namespace = { + "Dataset": object, + "Optional": typing.Optional, + "Sequence": typing.Sequence, + "Callable": typing.Callable, + "InvalidImageError": InvalidImageError, + } + exec(compile(isolated, str(source), "exec"), namespace) + return namespace["MMDetTorchDataset"] + + +def test_initial_consecutive_invalid_images_advance_to_next_valid(monkeypatch): + dataset_class = _load_dataset_class() + + class Column: + name = "image" + + class Schema: + columns = [Column()] + + class Dataset: + schema = Schema() + + def __init__(self): + self.read_indices = [] + + def __len__(self): + return 3 + + def __getitem__(self, index): + self.read_indices.append(index) + if index in (0, 1): + raise InvalidImageError("decode failed") + return {"image": index} + + source = Dataset() + wrapped = dataset_class(source) + + assert wrapped[0] == {"image": 2} + assert source.read_indices == [0, 1, 2] + assert wrapped.last_successful_index == 2 From 2962ae09f84a433089b821875d715a4826f04833 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Sat, 3 Oct 2026 02:22:09 +0800 Subject: [PATCH 2/2] fix(mmdet): bound invalid sample retries and reject pipeline skips --- .../integrations/mmdet/mmdet_dataset_.py | 35 +++- .../tests/test_mmdet_invalid_sample_loop.py | 171 ++++++++++++++---- 2 files changed, 157 insertions(+), 49 deletions(-) diff --git a/python/deeplake/integrations/mmdet/mmdet_dataset_.py b/python/deeplake/integrations/mmdet/mmdet_dataset_.py index 9a827473e4..923160c96a 100644 --- a/python/deeplake/integrations/mmdet/mmdet_dataset_.py +++ b/python/deeplake/integrations/mmdet/mmdet_dataset_.py @@ -414,7 +414,16 @@ def __len__(self): return len(self.dataset) def __getitem__(self, idx): - while True: + size = len(self.dataset) + if idx < 0: + idx += size + if not 0 <= idx < size: + raise IndexError("Dataset index out of range") + attempted = set() + next_index = (idx + 1) % size + last_error = None + for _ in range(size): + attempted.add(idx) try: sample = self.dataset[idx] if self.transform: @@ -424,15 +433,23 @@ def __getitem__(self, idx): for col in self.column_names: out[col] = sample[col] result = out - self.last_successful_index = idx - return result + if result is not None: + self.last_successful_index = idx + return result + last_error = ValueError("Transform rejected the sample") except InvalidImageError as e: - print(f"Error processing data at index {idx}: {e}") - if self.last_successful_index == -1: - idx += 1 - else: - idx = self.last_successful_index - continue + last_error = e + print(f"Error processing data at index {idx}: {last_error}") + fallback = self.last_successful_index + if 0 <= fallback < size and fallback not in attempted: + idx = fallback + else: + while next_index in attempted and len(attempted) < size: + next_index = (next_index + 1) % size + idx = next_index + raise RuntimeError( + f"No valid sample found in {size} dataset rows" + ) from last_error class MMDetDataset(MMDetTorchDataset): diff --git a/python/tests/test_mmdet_invalid_sample_loop.py b/python/tests/test_mmdet_invalid_sample_loop.py index 47f52063b6..7a5c098dc6 100644 --- a/python/tests/test_mmdet_invalid_sample_loop.py +++ b/python/tests/test_mmdet_invalid_sample_loop.py @@ -1,66 +1,157 @@ -import ast -import typing +"""Import the real wrapper normally; isolate unavailable training dependencies. + +These exercise wrapper retry behavior, not a full MMDet/Deep Lake training run. +The real exception and array helper modules are imported from this source tree. +""" + +import importlib +import sys from pathlib import Path +from types import ModuleType, SimpleNamespace +import pytest -class InvalidImageError(Exception): - pass +@pytest.fixture +def wrapper_module(monkeypatch): + source = Path(__file__).resolve().parents[1] / "deeplake" + for name in list(sys.modules): + if name == "deeplake" or name.startswith("deeplake."): + monkeypatch.delitem(sys.modules, name) -def _load_dataset_class(): - source = ( - Path(__file__).resolve().parents[1] - / "deeplake" - / "integrations" - / "mmdet" - / "mmdet_dataset_.py" + def module(name, package_path=None, **attrs): + value = ModuleType(name) + if package_path is not None: + value.__path__ = [str(package_path)] + value.__dict__.update(attrs) + monkeypatch.setitem(sys.modules, name, value) + + module("deeplake", source, Dataset=object) + module("deeplake.integrations", source / "integrations") + module("deeplake.integrations.mm", source / "integrations/mm") + module("deeplake.integrations.mmdet", source / "integrations/mmdet") + module("deeplake.types", TypeKind=SimpleNamespace()) + module("mmdet", [], __version__="2.28.1") + module("mmdet.apis", []) + module("mmdet.apis.train", auto_scale_lr=lambda *a, **k: None) + module( + "mmdet.core", + eval_map=None, + eval_recalls=None, + BitmapMasks=object, + PolygonMasks=object, ) - parsed = ast.parse(source.read_text(encoding="utf-8")) - class_node = next( - node - for node in parsed.body - if isinstance(node, ast.ClassDef) and node.name == "MMDetTorchDataset" + module("mmcv", []) + module("mmcv.utils", print_log=lambda *a, **k: None) + module("terminaltables", AsciiTable=object) + module("deeplake.integrations.mmdet.mmdet_utils_") + module( + "deeplake.integrations.mmdet.test_", single_gpu_test=None, multi_gpu_test=None ) - isolated = ast.Module(body=[class_node], type_ignores=[]) - ast.fix_missing_locations(isolated) - namespace = { - "Dataset": object, - "Optional": typing.Optional, - "Sequence": typing.Sequence, - "Callable": typing.Callable, - "InvalidImageError": InvalidImageError, - } - exec(compile(isolated, str(source), "exec"), namespace) - return namespace["MMDetTorchDataset"] - - -def test_initial_consecutive_invalid_images_advance_to_next_valid(monkeypatch): - dataset_class = _load_dataset_class() + before_import = set(sys.modules) + loaded = importlib.import_module("deeplake.integrations.mmdet.mmdet_dataset_") + assert ( + Path(loaded.__file__).resolve() + == source / "integrations/mmdet/mmdet_dataset_.py" + ) + try: + yield loaded + finally: + for name in set(sys.modules) - before_import: + if name.startswith("deeplake."): + sys.modules.pop(name, None) - class Column: - name = "image" - class Schema: - columns = [Column()] +def make_dataset(wrapper_module, size=3, invalid=(), transform=None): + error = importlib.import_module( + "deeplake.integrations.mm.exceptions" + ).InvalidImageError class Dataset: - schema = Schema() + schema = SimpleNamespace(columns=[SimpleNamespace(name="image")]) def __init__(self): + self.invalid = set(invalid) self.read_indices = [] def __len__(self): - return 3 + return size def __getitem__(self, index): + assert 0 <= index < size, "retry must stay inside dataset bounds" self.read_indices.append(index) - if index in (0, 1): - raise InvalidImageError("decode failed") + assert ( + len(self.read_indices) <= size * 2 + 1 + ), "repeated invalid fallback did not terminate" + if index in self.invalid: + raise error("image", ValueError("decode failed")) return {"image": index} source = Dataset() - wrapped = dataset_class(source) + return source, wrapper_module.MMDetTorchDataset(source, transform=transform) + +def test_initial_consecutive_invalid_images_advance_to_next_valid(wrapper_module): + source, wrapped = make_dataset(wrapper_module, invalid=(0, 1)) assert wrapped[0] == {"image": 2} assert source.read_indices == [0, 1, 2] assert wrapped.last_successful_index == 2 + + +def test_all_invalid_is_bounded_and_reports_no_valid_sample(wrapper_module): + source, wrapped = make_dataset(wrapper_module, invalid=(0, 1, 2)) + with pytest.raises(RuntimeError, match="No valid sample found"): + wrapped[2] + assert source.read_indices == [2, 0, 1] + assert wrapped.last_successful_index == -1 + + +def test_known_successful_fallback_is_retained(wrapper_module): + source, wrapped = make_dataset(wrapper_module, invalid=(0,)) + assert wrapped[2] == {"image": 2} + assert wrapped[0] == {"image": 2} + assert source.read_indices == [2, 0, 2] + + +def test_formerly_good_fallback_becoming_invalid_does_not_loop(wrapper_module): + source, wrapped = make_dataset(wrapper_module) + wrapped[2] + source.invalid.update((0, 2)) + assert wrapped[0] == {"image": 1} + assert source.read_indices == [2, 0, 2, 1] + assert wrapped.last_successful_index == 1 + + +def test_pipeline_none_is_rejected_before_recording_success(wrapper_module): + source, wrapped = make_dataset( + wrapper_module, transform=lambda row: None if row["image"] != 2 else row + ) + assert wrapped[0] == {"image": 2} + assert source.read_indices == [0, 1, 2] + assert wrapped.last_successful_index == 2 + + +def test_all_pipeline_rejections_are_bounded(wrapper_module): + source, wrapped = make_dataset(wrapper_module, transform=lambda row: None) + with pytest.raises(RuntimeError, match="No valid sample found"): + wrapped[0] + assert source.read_indices == [0, 1, 2] + assert wrapped.last_successful_index == -1 + + +@pytest.mark.parametrize("index", [-4, 3]) +def test_out_of_range_input_remains_index_error(wrapper_module, index): + source, wrapped = make_dataset(wrapper_module) + with pytest.raises(IndexError): + wrapped[index] + assert source.read_indices == [] + + +def test_negative_index_and_empty_dataset(wrapper_module): + source, wrapped = make_dataset(wrapper_module) + assert wrapped[-1] == {"image": 2} + assert source.read_indices == [2] + source, wrapped = make_dataset(wrapper_module, size=0) + with pytest.raises(IndexError): + wrapped[0] + assert source.read_indices == []