diff --git a/src/CelerisLab/lbm/block_stepper.py b/src/CelerisLab/lbm/block_stepper.py index 8389bd8..4c32bad 100644 --- a/src/CelerisLab/lbm/block_stepper.py +++ b/src/CelerisLab/lbm/block_stepper.py @@ -207,8 +207,11 @@ class PeriodicBlockStepper: """Advance n steps: halo refresh, then core pull/collide, per step. All n steps are enqueued in order on the one stream and the host waits - once before returning (same launches and bits as n step(1) calls). On a - failure the instance is failed and step_count keeps its entry value. + once before returning (same launches and bits as n step(1) calls). + Asynchronous device errors surface at that single wait, so they are not + attributed to the step that caused them; call step(1) repeatedly to + localize one. On any failure the instance is failed and step_count keeps + its entry value. """ if isinstance(n, bool) or not isinstance(n, Integral) or n < 0: raise ValueError("n must be a nonnegative integer") diff --git a/tests/unit/test_block_step_sync.py b/tests/unit/test_block_step_sync.py index 5779a70..51b76ca 100644 --- a/tests/unit/test_block_step_sync.py +++ b/tests/unit/test_block_step_sync.py @@ -78,6 +78,25 @@ class BlockStepSync(unittest.TestCase): run.step(2) self.assertEqual((run.step_count, run._status), (1, "failed")) + def test_launch_failure_mid_batch_fails_closed_without_waiting(self): + run, log = backend() + calls = [] + step = run._step + + def failing(*args, **kwargs): + calls.append(1) + if len(calls) == 2: + raise RuntimeError("launch failure") + return step(*args, **kwargs) + + run._step = failing + with self.assertRaises(RuntimeError): + run.step(3) + self.assertNotIn("sync", [name for name, _ in log]) + self.assertEqual((run.step_count, run._status), (0, "failed")) + with self.assertRaises(RuntimeError): + run.download() + if __name__ == "__main__": unittest.main()