Repository: pytorch/ignite

Issue:
DATALOADER_STOP_ITERATION should not be triggered if `Engine.run(data=None, ...)`
Reported in this issue: https://github.com/pytorch/ignite/issues/3189#issuecomment-1902560344

Repro code:
```python
from ignite.engine import Engine, Events


max_epochs = 4
dataset = range(10)


def training_step(engine, _):
    print(f"- {engine.state.epoch} / {engine.state.max_epochs} | {engine.state.iteration}")
    print(f"- batch: {next(engine.state.dataiter)}")
    print('-'*120)


trainer = Engine(training_step)
trainer.state.dataiter = iter(dataset)

@trainer.on(Events.DATALOADER_STOP_ITERATION)
def init_data_iter():
    print('THIS SHOULD NOT BE CALLED')
    trainer.state.dataiter = iter(dataset)

trainer.run(max_epochs=max_epochs, epoch_length=4)
```
gives the output:
```
- 1 / 4 | 1
- batch: 0
------------------------------------------------------------------------------------------------------------------------
- 1 / 4 | 2
- batch: 1
------------------------------------------------------------------------------------------------------------------------
- 1 / 4 | 3
- batch: 2
------------------------------------------------------------------------------------------------------------------------
- 1 / 4 | 4
- batch: 3
------------------------------------------------------------------------------------------------------------------------
THIS SHOULD NOT BE CALLED
- 2 / 4 | 5
- batch: 0
------------------------------------------------------------------------------------------------------------------------
- 2 / 4 | 6
- batch: 1
------------------------------------------------------------------------------------------------------------------------
- 2 / 4 | 7
- batch: 2
------------------------------------------------------------------------------------------------------------------------
- 2 / 4 | 8
- batch: 3
------------------------------------------------------------------------------------------------------------------------
THIS SHOULD NOT BE CALLED
- 3 / 4 | 9
- batch: 0
------------------------------------------------------------------------------------------------------------------------
- 3 / 4 | 10
- batch: 1
------------------------------------------------------------------------------------------------------------------------
- 3 / 4 | 11
- batch: 2
------------------------------------------------------------------------------------------------------------------------
- 3 / 4 | 12
- batch: 3
------------------------------------------------------------------------------------------------------------------------
THIS SHOULD NOT BE CALLED
- 4 / 4 | 13
- batch: 0
------------------------------------------------------------------------------------------------------------------------
- 4 / 4 | 14
- batch: 1
------------------------------------------------------------------------------------------------------------------------
- 4 / 4 | 15
- batch: 2
------------------------------------------------------------------------------------------------------------------------
- 4 / 4 | 16
- batch: 3
------------------------------------------------------------------------------------------------------------------------
```



Discussion:

Resolving patch:
diff --git a/ignite/engine/engine.py b/ignite/engine/engine.py
--- a/ignite/engine/engine.py
+++ b/ignite/engine/engine.py
@@ -1037,15 +1037,23 @@ def _run_once_on_dataset_as_gen(self) -> Generator[State, None, float]:
 
             while True:
                 self.state.batch = self.state.output = None
+
                 try:
                     # Avoid Events.GET_BATCH_STARTED triggered twice when data iter is restarted
                     if self.last_event_name != Events.DATALOADER_STOP_ITERATION:
-                        self._fire_event(Events.GET_BATCH_STARTED)
-                        yield from self._maybe_terminate_or_interrupt()
+                        # We should not trigger GET_BATCH_STARTED, GET_BATCH_COMPLETED, DATALOADER_STOP_ITERATION events
+                        # if no data was provided to engine.run(data=None, ...)
+                        if self.state.dataloader is not None:
+                            self._fire_event(Events.GET_BATCH_STARTED)
+                            yield from self._maybe_terminate_or_interrupt()
 
                     self.state.batch = next(self._dataloader_iter)
-                    self._fire_event(Events.GET_BATCH_COMPLETED)
-                    yield from self._maybe_terminate_or_interrupt()
+
+                    # We should not trigger GET_BATCH_STARTED, GET_BATCH_COMPLETED, DATALOADER_STOP_ITERATION events
+                    # if no data was provided to engine.run(data=None, ...)
+                    if self.state.dataloader is not None:
+                        self._fire_event(Events.GET_BATCH_COMPLETED)
+                        yield from self._maybe_terminate_or_interrupt()
 
                     iter_counter += 1
                     should_exit = False
@@ -1074,8 +1082,11 @@ def _run_once_on_dataset_as_gen(self) -> Generator[State, None, float]:
                             )
                         break
 
-                    self._fire_event(Events.DATALOADER_STOP_ITERATION)
-                    yield from self._maybe_terminate_or_interrupt()
+                    # We should not trigger GET_BATCH_STARTED, GET_BATCH_COMPLETED, DATALOADER_STOP_ITERATION events
+                    # if no data was provided to engine.run(data=None, ...)
+                    if self.state.dataloader is not None:
+                        self._fire_event(Events.DATALOADER_STOP_ITERATION)
+                        yield from self._maybe_terminate_or_interrupt()
 
                     self._setup_dataloader_iter()
                     should_exit = True
@@ -1198,12 +1209,18 @@ def _run_once_on_dataset_legacy(self) -> float:
                 try:
                     # Avoid Events.GET_BATCH_STARTED triggered twice when data iter is restarted
                     if self.last_event_name != Events.DATALOADER_STOP_ITERATION:
-                        self._fire_event(Events.GET_BATCH_STARTED)
-                        self._maybe_terminate_legacy()
+                        # We should not trigger GET_BATCH_STARTED, GET_BATCH_COMPLETED, DATALOADER_STOP_ITERATION events
+                        # if no data was provided to engine.run(data=None, ...)
+                        if self.state.dataloader is not None:
+                            self._fire_event(Events.GET_BATCH_STARTED)
+                            self._maybe_terminate_legacy()
 
                     self.state.batch = next(self._dataloader_iter)
-                    self._fire_event(Events.GET_BATCH_COMPLETED)
-                    self._maybe_terminate_legacy()
+                    # We should not trigger GET_BATCH_STARTED, GET_BATCH_COMPLETED, DATALOADER_STOP_ITERATION events
+                    # if no data was provided to engine.run(data=None, ...)
+                    if self.state.dataloader is not None:
+                        self._fire_event(Events.GET_BATCH_COMPLETED)
+                        self._maybe_terminate_legacy()
 
                     iter_counter += 1
                     should_exit = False
@@ -1232,8 +1249,11 @@ def _run_once_on_dataset_legacy(self) -> float:
                             )
                         break
 
-                    self._fire_event(Events.DATALOADER_STOP_ITERATION)
-                    self._maybe_terminate_legacy()
+                    # We should not trigger GET_BATCH_STARTED, GET_BATCH_COMPLETED, DATALOADER_STOP_ITERATION events
+                    # if no data was provided to engine.run(data=None, ...)
+                    if self.state.dataloader is not None:
+                        self._fire_event(Events.DATALOADER_STOP_ITERATION)
+                        self._maybe_terminate_legacy()
 
                     self._setup_dataloader_iter()
                     should_exit = True


Test patch:
diff --git a/tests/ignite/engine/test_engine.py b/tests/ignite/engine/test_engine.py
--- a/tests/ignite/engine/test_engine.py
+++ b/tests/ignite/engine/test_engine.py
@@ -601,8 +601,8 @@ def _test_check_triggered_events(self, data, max_epochs, epoch_length, exp_iter_
             Events.EPOCH_COMPLETED: max_epochs,
             Events.ITERATION_STARTED: max_epochs * epoch_length,
             Events.ITERATION_COMPLETED: max_epochs * epoch_length,
-            Events.GET_BATCH_STARTED: max_epochs * epoch_length,
-            Events.GET_BATCH_COMPLETED: max_epochs * epoch_length,
+            Events.GET_BATCH_STARTED: max_epochs * epoch_length if data is not None else 0,
+            Events.GET_BATCH_COMPLETED: max_epochs * epoch_length if data is not None else 0,
             Events.DATALOADER_STOP_ITERATION: (max_epochs - 1) if exp_iter_stops is None else exp_iter_stops,
         }
 
@@ -617,7 +617,7 @@ def _test_run_check_triggered_events(self):
         self._test_check_triggered_events(
             list(range(100)), max_epochs=5, epoch_length=150, exp_iter_stops=150 * 5 // 100
         )
-        self._test_check_triggered_events(None, max_epochs=5, epoch_length=150)
+        self._test_check_triggered_events(None, max_epochs=5, epoch_length=150, exp_iter_stops=0)
 
     def test_run_check_triggered_events_list(self):
         self._test_run_check_triggered_events()
@@ -1146,6 +1146,25 @@ def train_step(engine, batch):
         assert trainer.state.epoch == 20
         assert trainer.state.dataloader is None
 
+    def test_engine_no_data_events(self):
+        # Reproduces the issue https://github.com/pytorch/ignite/issues/3190
+        max_epochs = 4
+        dataset = range(10)
+
+        def training_step(engine, _):
+            assert engine.state.dataloader is None
+
+        trainer = Engine(training_step)
+        trainer.state.dataiter = iter(dataset)
+
+        @trainer.on(Events.DATALOADER_STOP_ITERATION)
+        @trainer.on(Events.GET_BATCH_STARTED)
+        @trainer.on(Events.GET_BATCH_COMPLETED)
+        def should_not_be_called():
+            assert False, trainer.last_event_name
+
+        trainer.run(max_epochs=max_epochs, epoch_length=4)
+
     @pytest.mark.parametrize("data, epoch_length", [(None, 10), (range(10), None)])
     def test_engine_run_resume(self, data, epoch_length):
         # https://github.com/pytorch/ignite/wiki/Roadmap#runresume-logic-improvements