Quantum Resource Manager and Compiler Infrastructure (QRM&CI) 0.1.0
Getting Started - Deploying QRM&CI

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.

Choose your deployment

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.

Prerequisites

Runtime prerequisites

  • A RabbitMQ broker that is reachable from every process you start.
  • For the standalone daemon and the distributed worker, access to a QDMI driver and device at runtime.
  • A writable log directory, or an override for QRMCI_LOG_DIR.
  • A configuration source: a TOML file, environment variables, or both.

Build prerequisites

  • CMake 3.25 or newer
  • A C++23-capable compiler
  • MQSS Integration and Deployment Framework, MQSS Quantum Compilation Suite, MQSS Scheduler, and MQSS Submitter
  • The system packages listed in the two Dockerfiles, apps/standalone/Dockerfile and apps/distributed/Dockerfile

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:

  1. Path A - build the container images from apps/standalone/Dockerfile and apps/distributed/Dockerfile.
  2. Path B - build from source in a local environment that provides the toolchain and the system packages.

Path A: run the container images

No prebuilt QRM&CI images are published, so build the images from this repository first. The Makefile provides a build target for each image:

# To build Docker image with the standalone daemon
make docker-build-standalone
# To build Docker images for both selector and worker
make docker-build-distributed
# To build Docker image for selector only
make docker-build-distributed-selector
# To build Docker image for worker only
make docker-build-distributed-worker

Run the daemons

The containers below need a reachable RabbitMQ broker. The Makefile defines one run target per shipped image:

Standalone

make docker-run-standalone

Distributed

Start the selector and the workers as separate containers:

make docker-run-distributed-selector
make docker-run-distributed-worker

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):

cd apps/standalone # or apps/distributed
cp .env.example .env
docker compose up --build

Neither compose file bundles a RabbitMQ broker — set QRMCI_AMQP_HOST in .env to a reachable one, same as the -e overrides above.

Path B: build from source

From the repository root, configure and build into a disposable build/ directory:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel

For a deploy-focused source build, you can keep the application targets enabled and disable the test targets:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DQRMCI_BUILD_UNIT_TESTS=OFF \
-DQRMCI_BUILD_INTEGRATION_TESTS=OFF
cmake --build build --parallel

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:

  • build/apps/standalone/qrmcid-standalone
  • build/apps/distributed/qrmcid-distributed-selector
  • build/apps/distributed/qrmcid-distributed-worker

Relevant build options for a deployer:

  • QRMCI_BUILD_APPS (default ON) controls whether the daemon binaries are built
  • QRMCI_BUILD_UNIT_TESTS (default ON) controls the unit test targets
  • QRMCI_BUILD_INTEGRATION_TESTS (default ON) controls the integration test targets

Run the daemons

The commands below require a RabbitMQ and QDMI environment that matches your configuration.

Standalone

QRMCI_CONFIG_FILE=path/to/qrmci.toml build/apps/standalone/qrmcid-standalone

Distributed

Start the selector and worker in separate terminals:

QRMCI_CONFIG_FILE=path/to/qrmci-distributed-selector.toml \
build/apps/distributed/qrmcid-distributed-selector
QRMCI_CONFIG_FILE=path/to/qrmci-distributed-worker.toml \
build/apps/distributed/qrmcid-distributed-worker

Stopping a daemon

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.

Runtime configuration

QRM&CI assembles configuration in three layers, from lowest to highest precedence:

  1. compiled-in defaults from include/qrmci/ConfigDefaults.h.in and the in-class defaults in include/qrmci/Config.h
  2. an optional TOML file. QRM&CI reads the path in QRMCI_CONFIG_FILE if that variable is set; otherwise it looks for config/qrmci.toml relative to the process's working directory. Because that path is relative, a daemon started by systemd or inside a container will not find a config file unless you set QRMCI_CONFIG_FILE or control the working directory. A missing file is not an error — the daemon runs on defaults plus environment. A file that exists but does not parse is a startup error.
  3. environment variables

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:

  • an unknown top-level table or an unknown key inside [common], [common.connection], [selector], [selector.backendRegistry], [compiler], [submitter], or [submitter.backendRegistry]
  • a value of the wrong type for a documented field (e.g. a string where an integer is expected)
  • a malformed or partially-numeric QRMCI_AMQP_PORT, or a port outside 1..65535
  • an empty host, qrmciQueue, backendStatusQueue, compiler.queue, qdmiDriver, qdmiDeviceName, or qdmiDeviceId
  • stagePollInterval, selector.backendRegistry.entryTimeToLive, submitter.backendRegistry.entryTimeToLive, or backendStatusPublishInterval that is not greater than zero
  • a negative taskReceiveTimeout or jobWaitTimeout
  • submitter.backendRegistry.entryTimeToLive not greater than submitter.backendStatusPublishInterval (a registry entry must outlive the interval between the publications that refresh it)
  • a selector.selectionPolicy other than exactly "lowest-name" or "smallest-sufficient"

TOML files

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.

Environment variables

The documented runtime overrides in src/Config.cpp are:

  • QRMCI_CONFIG_FILE
  • QRMCI_AMQP_HOST
  • QRMCI_AMQP_PORT
  • QRMCI_AMQP_USER
  • QRMCI_AMQP_PASSWORD
  • QRMCI_AMQP_VHOST
  • QRMCI_QRMCI_QUEUE
  • QRMCI_SCHEDULER_QUEUE
  • QRMCI_COMPILER_QUEUE
  • QRMCI_RESULTS_QUEUE
  • QRMCI_SUBMITTER_QUEUE
  • QRMCI_BACKEND_STATUS_QUEUE
  • QRMCI_LOG_DIR
  • QRMCI_SUBMITTER_QDMI_DRIVER
  • QRMCI_SUBMITTER_QDMI_DEVICE_NAME
  • QRMCI_SUBMITTER_QDMI_DEVICE_ID
  • QRMCI_SUBMITTER_QDMI_CLIENT_TOKEN

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.

Selected defaults

The compiled-in defaults are development values, not deployment values. If you do not override them:

  • RabbitMQ host 127.0.0.1, port 5672, vhost / (the three production Dockerfiles set QRMCI_AMQP_HOST=localhost explicitly as their compiled-in image default; localhost always means the current container's own loopback address, on every Docker installation, not just Docker Desktop — it only works if a broker runs inside that same container.
  • RabbitMQ credentials guest / guest
  • log directory /var/log/qrmci
  • task queue qrmci.tasks.queue
  • compiler queue compiler.tasks.queue
  • scheduler queue scheduler.tasks.queue
  • submitter queue submitter.tasks.queue
  • backend status queue backend.status.queue
  • results queue test.results.queue
  • QDMI driver /usr/local/lib/qrmci/libqdmi_example_driver.so (a filesystem path, not a library stem, matching where the runtime container images install it — see the device boundary section), device C++ Device with 5 qubits, client token token

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.

Confirm the daemon started

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:

  1. no configuration error — a malformed TOML file makes the process log Failed to load configuration: ... and exit with status 1
  2. the device opening cleanly — a driver that cannot be loaded, a device name the driver does not report, or a QDMI version this build cannot speak all make the standalone daemon and the worker log Could not open the QDMI device: ... and exit non-zero. A device that opens but cannot be read logs Could not read the QDMI device: [<kind>] ... and also exits non-zero: a daemon with no backend to offer does not start.
  3. the backends the process knows about. The standalone daemon and the worker log Available backends: followed by one Backend: <name>, Qubits: <n>, Status: <n> line per entry, taken from their live QDMI connection. The selector starts with an empty registry and fills it from the worker status messages that arrive on [selector].backendStatusQueue.
  4. the start banner — Starting MQSS QRM&CI Daemon., Starting MQSS QRM&CI Distributed Backend Selector., or Starting MQSS QRM&CI Distributed Worker.
  5. a quiet poll loop. With no work on the intake queue, a healthy daemon logs nothing further; it wakes every common.stagePollInterval, refreshes its backend registry entry, and polls the queue again.

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.

Verify it works

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:

ctest --test-dir build/tests --output-on-failure -R '^TestSubmitTask$'

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.

Where to go next