Repository navigation
QFw: wait out a provider's HTTP 429 in the shim, poll less often, and cap a device's jobs - #172
Merged
Merged
Conversation
A chemistry workflow on the ORNL q20 failed on 2026-10-23 with "QRMI task_start failed: [QrmiError_] error in response: status code 429 Too Many Requests". The QRMI driver raised the first 429 it saw. The QRMI driver now waits a 429 out for each call of a job's lifecycle: task_start, task_status and task_result. IQM blocks an account that runs over its request quota for 30 seconds, so a status poll can be refused as well as a submission. The wait doubles from 2 to 60 seconds, with jitter, and stops when the job's timeout runs out: the circuit's timeout, else job-timeout-seconds, else 300 seconds. That timeout now counts from the first submission attempt, so time spent waiting out a rate limit comes off it. Any other failure is raised at once, as before. QRMI 0.26 classifies a 429 as nothing more than its base QrmiError, and keeps only the status in the message, so there is no Retry-After to honor. IQM, IBM's QRS and QCS and Pasqal Cloud all spell the status "429 Too Many Requests". IBMQuantumSystem retries a 429 inside QRMI. The QDMI path is unchanged: QDMI-on-IQM and the Braket library retry a 429 themselves, and QDMI reports a failure without its HTTP status. A device-access max-concurrent-jobs caps how many of the device's circuits are at the provider at once. It is the device profile's max_provider_queue_depth, so the QPM controller holds the rest queued and dispatches each as another finishes. It goes through both whitelists, select_qpu and resolve_descriptor. Unset, the device adds no limit of its own. The controller's operator limit, max_inflight, is 1 unless an operator raises it, and the smaller of the two wins. A value that is not a whole number of at least 1 stops the service at start. The man page, the provider guide and the frontend contract document the key. The benchmarking span table names the new rate-limited span event. Signed-off-by: Doug Oucharek <dougso@me.com>
With one circuit in flight, a chemistry run on the ORNL q20 (job 82) got past task_start and then failed on a 429 to task_status. The q20 limits the request rate, and the shim polled every second: QFwBackend sends no poll_interval, and no device-access key set one. Unless a circuit names its own poll_interval, the first wait between status polls is now the device's poll-interval-seconds, else 1 second, and each poll that finds the job unfinished doubles it, up to 60 seconds. That is how IQM's own QDMI library polls. A circuit that names a poll_interval is polled at that fixed interval, as before. Both the QRMI and the QDMI driver poll this way, and no wait runs past the job's deadline. poll-interval-seconds goes through both whitelists, and a value that is not a number fails before anything is submitted. The QDMI driver still ends a job on its first failed check. The device library has already retried a rate-limited request by then, and QDMI reports the failure without its HTTP status. Signed-off-by: Doug Oucharek <dougso@me.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The shim QPM now copes with a provider that rate-limits it with HTTP 429, so one 429 no longer fails a workflow.
task_start,task_statusandtask_resultare each tried again after a 429. The wait doubles from 2 to 60 seconds, with jitter drawn from the upper half of each wait. It stops when the job's timeout runs out: the circuit'stimeout, elsejob-timeout-seconds, else 300 seconds. The timeout now counts from the first submission attempt, so a rate-limit wait comes off it rather than adding to it. A cancel ends a wait at once. Any other failure is raised at once, as before.poll_interval, the first wait between status polls is the device's newpoll-interval-seconds, else 1 second. Each poll that finds the job unfinished doubles it, up to 60 seconds, which is how IQM's own QDMI library polls (IQM_QDMI_device_job_wait). A circuit that names apoll_interval, such asexamples/measure_shim_execution.pysends, is polled at that fixed interval, as before. Both drivers poll this way, and no wait runs past the job's deadline. The key is in both whitelists, and a value that is not a number fails before anything is submitted.max-concurrent-jobsis a new optional device-access key. It caps how many of a device's circuits are at the provider at once. The shim QPM sets it as the device profile'smax_provider_queue_depth, so the controller holds the rest queued and dispatches each as another finishes (SCHED-007). It is in both whitelists,select_qpuandresolve_descriptor. Unset, the device adds no limit of its own. A value that is not a whole number of at least 1 stops the service at start.qfw-device-access.yaml(5), a new "Providers that rate-limit" section in the shim provider guide, the frontend contract's key list, and the benchmarking span table, which names the newrate-limitedspan event.Why
On janus1 on 2026-10-23, chemistry_he job 76 failed with
QRMI task_start failed: [QrmiError_] error in response: status code 429 Too Many Requests. A rerun that sent one circuit at a time (job 82) got pasttask_startand then failed withQRMI task_status failed: ... 429 Too Many Requests. So the q20 limits the request rate, and the shim was polling every second. IQM blocks an account that runs over its request quota for 30 seconds. QDMI-on-IQM 1.5.0 documents the block and the 10-second quota window. A status poll can be refused during the block as well as a submission, so the retry covers polls and the result fetch too, and polling backs off to spend less of the quota.Findings
A shim QPM already sends one job at a time. The controller's operator limit
max_inflightdefaults to 1 (scheduler_control["dispatch_depth"], sincec953bf2), and nothing in QFw or the cluster raises it. So the estimator's three circuits did not reach IQM at once from one QPM. Job 76's 429 more likely came from the account's request quota. Every request counts against it: status polls, introspection through QRMI and QDMI, and any other client on the same token. That makes the retry the fix.max-concurrent-jobsis the per-device half of SCHED-007, and it matters once an operator raisesmax_inflight. The new end-to-end test pins both behaviours.There is no Retry-After to honor. QRMI 0.26.0 to 0.26.4 drops the response headers. A 429 arrives as the base
qrmi.QrmiError, with the status only in the message:error in response: status code 429 Too Many Requests. The generated client keeps no headers, and the message drops the body too.response body: 429 Too Many Requests <body>Status: 429 Too Many Requests, Fail <body>iqp_retry_policy), so QFw never sees it and the job timeout does not bound it.QDMI cannot take a 429 retry. MQT Core reports a failed call as
Submitting job: <QDMI status>.orChecking job status: <QDMI status>., without the HTTP status, so the shim cannot tell a 429 from any other failure.QdmiDriver._poll_jobstill ends a job on its first failedjob.check(). On IQM that comes only after QDMI-on-IQM has retried the 429 itself, inside a request timeout of an hour. The QDMI driver does take the new poll schedule. The libraries handle most of the rest themselves:SERVICE_QUOTA_EXCEEDEDas throttling.Not covered
target()and the introspection calls still fail on a 429.target()is cached per reservation credential, so that is one call per reservation.task_stopstays best effort. A stop during a block can fail, and the job then finishes at the provider.Tests
tests/mock/test_shim_rate_limit.pyis new, with 40 tests. A fake QRMI resource refuses calls a set number of times, and a fake clock stands in for the waits.poll-interval-secondsis the first wait, a client'spoll_intervalstays fixed, the last poll is made when the timeout runs out, and a bad value fails before submission. The QDMI driver's_poll_jobbacks off the same way.max-concurrent-jobsbecomes the device profile's limit, and a bad value stops the service. An end-to-end run of the real shim QPM with stub drivers shows circuits over the limit queued and dispatched as others finish, under the defaultmax_inflightand a raised one.tests/mock/test_descriptor_config.pytakesmax-concurrent-jobsandpoll-interval-secondsthroughselect_qpuandresolve_descriptor.tests/mock/test_qdmi_profiles.pychecks that QDMI'srun_circuitpolls on the same schedule.Run in the test cluster's
c1against this branch:main(27f1b7e)tests/qiskitwithouttest_qdmi_adapters.pytest_qdmi_adapters.pyneeds the released adapters, which thec1venv lacks onmaintoo. The new tests passed 15 runs in a row and 8 runs under parallel load.