Skip to content

Commit 753428e

Browse files
committed
🐛 Preserve successful programs after status errors
Keep each indexed outcome independent when a later sibling's status query fails. Remove obsolete batch-format guidance now that program lists own submission. Assisted-by: GPT-6 via Codex
1 parent acc2259 commit 753428e

3 files changed

Lines changed: 55 additions & 24 deletions

File tree

‎docs/qir/index.md‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,3 @@ records are suppressed, and unsupported operations produce a failed QDMI job.
350350
Both profiles preserve global phase and logical wire order, including SWAPs.
351351
LLVM target triples must match the host architecture and operating system
352352
because the JIT executes in process.
353-
354-
The generic submission APIs reject the QDMI batch-job format. Batch jobs contain
355-
job handles rather than serialized program bytes and require a separate typed
356-
API.

‎python/mqt/core/plugins/qdmi_batch.py‎

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -197,30 +197,40 @@ def _refresh(self, indices: Sequence[int], *, wait: bool = False) -> Iterator[li
197197
groups.setdefault(id(handle), []).append(index)
198198
for group in groups.values():
199199
pending = [i for i in group if self._entries[i].attempts[-1].status not in _TERMINAL]
200-
if pending:
201-
handle = self._entries[pending[0]].attempts[-1].handle
202-
assert handle is not None
203-
stage = "wait" if wait else "status"
204-
try: # ruff:ignore[too-many-statements-in-try-clause] Keep shared query failures on every affected entry.
205-
if wait and not handle.wait():
206-
msg = "Timed out waiting for the QDMI job."
207-
raise TimeoutError(msg) # ruff:ignore[raise-within-try] Record timeouts with other wait failures.
208-
stage = "status"
209-
aggregate = handle.check()
210-
# Resolve individual outcomes before attributing an aggregate failure.
211-
for index in pending:
212-
attempt = self._entries[index].attempts[-1]
200+
if not pending:
201+
yield group
202+
continue
203+
handle = self._entries[pending[0]].attempts[-1].handle
204+
assert handle is not None
205+
stage = "wait" if wait else "status"
206+
try:
207+
if wait and not handle.wait():
208+
msg = "Timed out waiting for the QDMI job."
209+
raise TimeoutError(msg) # ruff:ignore[raise-within-try] Record timeouts with other wait failures.
210+
stage = "status"
211+
aggregate = handle.check()
212+
except BaseException as exc:
213+
for index in pending:
214+
attempt = self._entries[index].attempts[-1]
215+
self._set_attempt(index, replace(attempt, status=None))
216+
self.record_failure(index, stage, exc)
217+
if not isinstance(exc, Exception):
218+
raise
219+
else:
220+
# Resolve individual outcomes before attributing an aggregate failure.
221+
for index in pending:
222+
attempt = self._entries[index].attempts[-1]
223+
try:
213224
status = aggregate
214225
if aggregate in {Job.Status.FAILED, Job.Status.CANCELED}:
215226
status = handle.get_program_status(attempt.program_index) or aggregate
216-
self._set_attempt(index, replace(attempt, status=status))
217-
except BaseException as exc:
218-
for index in pending:
219-
attempt = self._entries[index].attempts[-1]
227+
except BaseException as exc:
220228
self._set_attempt(index, replace(attempt, status=None))
221-
self.record_failure(index, stage, exc)
222-
if not isinstance(exc, Exception):
223-
raise
229+
self.record_failure(index, "status", exc)
230+
if not isinstance(exc, Exception):
231+
raise
232+
else:
233+
self._set_attempt(index, replace(attempt, status=status))
224234
yield group
225235

226236
def statuses(self) -> tuple[Job.Status | None, ...]:

‎test/python/plugins/test_qdmi_batch.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,31 @@ def test_native_read_failure_never_replaces_execution(stage: str) -> None:
381381
assert fixture.native_calls == [[0, 1, 2]]
382382

383383

384+
def test_later_native_status_read_failure_keeps_confirmed_siblings() -> None:
385+
"""A failed status download cannot erase earlier or later successful programs."""
386+
fixture = BatchFixture(native=True)
387+
fixture.batch.submit()
388+
shared = fixture.jobs[0]
389+
shared.check.return_value = Job.Status.FAILED
390+
cause = RuntimeError("status unavailable")
391+
shared.get_program_status.side_effect = [Job.Status.DONE, cause, Job.Status.DONE, Job.Status.DONE]
392+
393+
with pytest.raises(RuntimeError, match="status unavailable") as caught:
394+
fixture.batch.complete()
395+
assert caught.value.__cause__ is cause
396+
assert [entry.result for entry in fixture.batch.entries] == ["0", None, "2"]
397+
assert [entry.attempts[-1].status for entry in fixture.batch.entries] == [
398+
Job.Status.DONE,
399+
None,
400+
Job.Status.DONE,
401+
]
402+
assert fixture.submitted == [0, 1, 2]
403+
404+
fixture.batch.complete()
405+
assert [entry.result for entry in fixture.batch.entries] == ["0", "1", "2"]
406+
assert fixture.submitted == [0, 1, 2]
407+
408+
384409
def test_uncertain_native_admission_has_no_single_fallback() -> None:
385410
"""A native submission error marks all selected programs uncertain without replay."""
386411
fixture = BatchFixture(native=True)

0 commit comments

Comments
 (0)