Skip to content

Hardware overview

One process-wide HardwareManager registers eight providers — seven connectable plus a non-functional Google scaffold; the kernel connects simulator itself at startup, so a local target is always available.

All hardware state — connections, credentials, jobs — is shared across WebSocket connections. The wire protocol is on the hardware messages page.

For hardware_submit, the kernel prepares a provider-appropriate payload from your source code: the simulator takes raw source; real providers get a circuit object extracted from the exec’d namespace, searched in a provider-aware framework order; Q# is special-cased (QIR for Azure, rejected elsewhere).

ProviderSearch orderWhat submit actually does
simulatorn/a — raw sourceRe-runs the source through the executor pipeline (any framework, Q# included). Completes synchronously.
ibmqiskit → cirq → cudaqtranspile(circuit, backend) + SamplerV2.run — effectively requires a Qiskit QuantumCircuit.
ionqqiskit → cirq → cudaqqiskit-ionq backend.run(circuit, shots) — Qiskit circuits.
braketqiskit → cirq → cudaqPasses the extracted object straight to AwsDevice.runno conversion is performed; whether a Qiskit/Cirq object is accepted is up to the Braket SDK.
azureqiskit → cirq → cudaqtarget.submit(circuit, shots) — translation relies on azure-quantum’s SDK plugins. Q# source is compiled to QIR first. Deep dive.
quantinuumqiskit → cirq → cudaqAuto-converts Qiskit/Cirq circuits to pytket via pytket-extensions (clear install hint if missing), compiles, then process_circuit.
nvidiacudaq → qiskit → cirqcudaq.sample on the selected GPU/CPU target — needs a CUDA-Q kernel (or (kernel, *args) tuple). Completes synchronously.
googleScaffolded only: connect always returns false; submission raises. Honest status: not usable yet.

Q# programs can run on the simulator and Azure Quantum only; other providers reject Q# with a message suggesting Azure or a Qiskit rewrite.

Keys the kernel’s connect() actually reads from the hardware_connect.credentials object — scripted clients must use these exact names:

ProviderRequired keysOptional keys (default)Get one at
simulatoralways available
ibmtokeninstance (ibm-q/open/main)quantum.ibm.com
ionqtokenIonQ console (or use Braket)
nvidianeeds cudaq importable, nothing else
braketaccess_key_id, secret_access_keyregion (us-east-1), session_tokenAWS IAM with AmazonBraketFullAccess
azuresubscription_id, resource_group, workspace_namelocation (eastus)portal.azure.com — no token field; auth uses the Azure default credential chain
quantinuumtoken (Nexus API token)nexus.quantinuum.com — requires pytket-quantinuum<0.26 (see note below); the token is validated at connect with an authenticated device-list call (was accepted unvalidated before unreleased main)

On a successful connect, non-empty credentials persist to the OS keyring, and the next kernel start auto-reconnects silently (failures are logged, not surfaced — the provider just shows as disconnected).

hardware_submit
├─ routing/compilation raised ──────────▶ error envelope (no job exists)
JobHandle
├─ provider error at submit ────────────▶ failed (terminal)
├─ synchronous provider (simulator,
│ nvidia) ─────────────────────────────▶ complete (terminal)
queued ──▶ running ──▶ complete (terminal)
│ │
│ ├─ provider failure ─────────▶ failed (terminal)
└──────────┴─ hardware_cancel ──────────▶ failed (no "cancelled" wire status)
any non-terminal status + kernel restart ──▶ stale (re-submit to run again)
unrecognized provider state ───────────────▶ unknown

Two sharp conventions, both documented with replay-tested fixtures on the hardware messages page:

  • Failed handle vs error envelope. A failed submission arrives either as an error envelope (no job exists) or as a normal hardware_job_submitted whose handle has status: "failed" — handle both.
  • hardware_status never re-queries job status — it only refreshes queue_position. Poll hardware_results to discover completion.

Job metadata persists to ~/.nuclei/jobs.json (override the directory with NUCLEI_DATA_DIR): atomic temp-file-and-rename writes, capped at 200 entries, terminal jobs pruned after 7 days — the prune runs on every write and at kernel start when the store is loaded, so expired records disappear even if you never submit another job. The write happens immediately at submit, so a crash between submit and first poll can’t orphan the record. Individual records can also be removed on demand with hardware_dismiss.

What does not persist is the provider SDK handle. After a kernel restart:

  • Terminal jobs (complete/failed) keep their stored status — history survives.
  • Non-terminal jobs come back as stale: polling can’t resume and results are unrecoverable through Nuclei — re-submit to run again. The job usually did keep running provider-side; you can find it in the provider’s own console, but note the kernel’s job id is a locally generated UUID, not the provider’s id, so cross-referencing is manual.