This guide is for deploying the shipped QRM&CI daemons. It takes you from choosing a deployment mode to starting the process, configuring it, and checking that tasks flow through it.
If you need to change the pipeline itself, see Build custom QRM&CI workflow. If you need to work on QRM&CI as a contributor, see Development Guide.
| Deployment | Source | Use it when... |
|---|---|---|
| Standalone | apps/standalone | one process should run the full pipeline |
| Distributed selector | apps/distributed/selector | backend selection should run separately from device access |
| Distributed worker | apps/distributed/worker | compilation, scheduling, and submission should run in the device-owning process |
In distributed mode, one selector is paired with one or more workers. Each worker publishes its own backend status, and the selector chooses among the backends it has heard from.
To run more than one worker against the same selector, give each worker a distinct qdmiDeviceId/QRMCI_SUBMITTER_QDMI_DEVICE_ID and a distinct compiler.queue/QRMCI_COMPILER_QUEUE (see apps/distributed/docker-compose.yaml's commented-out second-worker example). A worker publishes its own dispatch queue alongside its status, and the selector forwards each task to that queue, so two workers sharing one queue would race to consume each other's tasks; two workers sharing one device ID with different queues is instead rejected outright as an identity collision, and the first worker's entry is kept.
You do not install the MQSS components yourself: the cmake/Find*.cmake modules fetch them from GitHub at configure time, at the revisions pinned in cmake/DependenciesVersion.cmake. Both build paths therefore need network access, and git must be able to reach the MQSS repositories from the build environment. The first configure is correspondingly long.
There are two ways to build:
No prebuilt QRM&CI images are published, so build the images from this repository first. The Makefile provides a build target for each image:
The containers below need a reachable RabbitMQ broker. The Makefile defines one run target per shipped image:
Start the selector and the workers as separate containers:
Each docker-run-* target is a convenience wrapper around docker run --rm -it <image>:<tag> for an image that already exists locally. None of them mounts a config file or passes environment overrides, so the container starts on the compiled-in defaults (see "Selected defaults" below) — which is almost never what you want. For a real deployment, run docker run yourself with -e QRMCI_CONFIG_FILE=... and a mounted config file, or with the individual QRMCI_* overrides.
For a self-contained local deployment, use the docker-compose.yaml files instead: apps/standalone/docker-compose.yaml builds and runs qrmcid-standalone, and apps/distributed/docker-compose.yaml builds and runs the selector and worker together. Both load every QRMCI_* variable from a companion .env file (copy .env.example next to it and edit it):
Neither compose file bundles a RabbitMQ broker — set QRMCI_AMQP_HOST in .env to a reachable one, same as the -e overrides above.
From the repository root, configure and build into a disposable build/ directory:
For a deploy-focused source build, you can keep the application targets enabled and disable the test targets:
This skips building and running the tests; GTest must still be discoverable at configure time, because the top-level CMakeLists.txt requires it unconditionally.
The shipped binaries are:
Relevant build options for a deployer:
The commands below require a RabbitMQ and QDMI environment that matches your configuration.
Start the selector and worker in separate terminals:
All three shipped daemons install handlers for SIGINT and SIGTERM: the signal sets a termination flag, the current loop turn finishes, and the process exits with status 0. This applies to both paths. It is also why the containers stop cleanly — docker stop sends SIGTERM, and Ctrl+C in a docker run --rm -it session sends SIGINT.
QRM&CI assembles configuration in three layers, from lowest to highest precedence:
Once assembled, the configuration is validated as a whole; a present-but-invalid value is always a startup error naming the exact dotted key, regardless of which layer supplied it:
Copy the matching example file, edit it for your environment, and point QRMCI_CONFIG_FILE at the copy you want to use.
| Binary | Example file | What the process reads |
|---|---|---|
| qrmcid-standalone | apps/standalone/config/qrmci-standalone.example.toml | [common] for RabbitMQ, logs, task/result/scheduler queues, and poll interval; [selector].selectionPolicy for its own backend selection; [submitter] for QDMI settings, job wait timeout, and backend registry timing |
| qrmcid-distributed-selector | apps/distributed/config/qrmci-distributed-selector.example.toml | [common]; [selector] for backend status queue, selector logging, selection policy, and registry timing. The forwarding queue for a selected task comes from the dispatch queue each worker published in its own status, not from a local [compiler] setting |
| qrmcid-distributed-worker | apps/distributed/config/qrmci-distributed-worker.example.toml | [common]; [selector].backendStatusQueue as the publish target; [compiler] as the inbound task queue; [submitter] for QDMI settings, job wait timeout, and backend registry timing |
For a single file documenting every Config field (including the poll-interval/timeout/registry-TTL/logger-name fields above that have no QRMCI_* override), see apps/qrmci.full-reference.example.toml — it is a reference, not a deployable template, since no one binary reads every section in it.
Keep credentials out of the TOML file when you can. Supply deployment-specific secrets through environment variables at runtime instead.
The documented runtime overrides in src/Config.cpp are:
Receive timeouts, common.stagePollInterval, submitter.jobWaitTimeout, and backend-registry timing are file-only; they have no environment override. Nothing declares a QDMI version: the driver's version is detected from the driver itself.
The compiled-in defaults are development values, not deployment values. If you do not override them:
Two of those defaults matter most. The credentials are RabbitMQ's stock guest/guest, and the QDMI target is the example driver — so an unconfigured daemon starts happily against an example device rather than refusing to start. The name of the default results queue, test.results.queue, is a fair summary of the whole set. Override at minimum the connection settings, the credentials, the QDMI driver/device, and the queue names before any real deployment.
Each daemon logs to the console and, when the log directory is writable, to daemon.log under QRMCI_LOG_DIR (default /var/log/qrmci). Watch for, in order:
A device that fails later, once the daemon is running, does not stop it: the refresh logs Could not refresh the backend status: [<kind>] ... and skips that turn, so the registry entry's own time-to-live governs how long the last good snapshot stands and a device that recovers is picked up on the next slot. The worker additionally publishes nothing in that turn, rather than telling the selector it is healthy.
If the broker is unreachable, the daemon does not exit. Each turn logs a warning of the form Could not read from queue '<queue>': ... and the loop retries, so the daemon connects on its own once RabbitMQ becomes available. A steady stream of those warnings means the broker, credentials, or queue names are wrong — not that the daemon has died.
The tests/integration/ checks are self-contained: each one spawns and tears down its own qrmcid-standalone (or, for TestSubmitTaskDistributed, both distributed daemons) rather than talking to a deployment you started yourself, so they verify this build's own pipeline end to end rather than a daemon you have already deployed elsewhere. For the baseline submission path, build the integration targets (-DQRMCI_RUN_INTEGRATION_TESTS=ON, see Development Guide) and run:
A successful run means the spawned daemon selected a backend, compiled the submitted task, and executed it, and a result rather than a cancellation came back on the result queue. To check an already-deployed daemon instead of this build's own, send a mqss.QuantumTask to its configured input queue with any RabbitMQ client and read the result back from the queue you set as result_destination — tests/integration/TestSubmitTask.cpp shows the message shape.