diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c043988..b976140 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,11 +85,11 @@ jobs: flux start flux submit --help 2>&1 | grep -E -- '--quantum-vendor' \ || { echo "CLI plugin not discovered"; exit 1; } - - name: vendor backends contribute their own options + - name: the submit options are the same for every vendor run: | export FLUX_CLI_PLUGINPATH=$PWD/cli-plugins out=$(flux start flux submit --help 2>&1) - for opt in --quantum-ibm-backend --quantum-braket-device --quantum-mock-session; do + for opt in --quantum-device --quantum-hold --quantum-hold-max --quantum-wait --quantum-dry-run; do echo "$out" | grep -q -- "$opt" || { echo "missing $opt"; exit 1; } echo " $opt OK" done @@ -111,6 +111,20 @@ jobs: SH flux start bash /tmp/jt.sh + # the core budget over unfinished pairs, rejecting a pair whose classical + # half could not be placed rather than admitting it + - name: jobtap admission control + run: FLUX_QUANTUM_MOCK=1 flux start bash tests/integration/test-admission.sh + + # preemption is destructive, so it runs on its own. Skips itself when the + # runner has fewer than three cores. + - name: jobtap preemption + run: FLUX_QUANTUM_MOCK=1 flux start bash tests/integration/test-preemption.sh + + # unload with a grace timer pending, and reload with jobs running + - name: jobtap unload and reload + run: FLUX_QUANTUM_MOCK=1 flux start bash tests/integration/test-reload.sh + # one submit gives a held classical and a scout, and the qpu stays # allocated until the classical is done - name: end-to-end, single broker @@ -124,6 +138,12 @@ jobs: - name: vendor discovery + --quantum-select run: flux start bash tests/integration/test-select-discovery.sh + - name: IonQ backend end-to-end, against the fake service + run: flux start bash tests/integration/test-ionq-e2e.sh + + - name: IonQ backend end-to-end, account without sessions + run: FAKE_IONQ_NO_SESSIONS=1 flux start bash tests/integration/test-ionq-e2e.sh + # this test expects fluxion already loaded, as rc1 does on a real cluster - name: system-instance test (fluxion preloaded, 2 ranks) run: | @@ -131,7 +151,8 @@ jobs: set -e flux module remove -f sched-simple 2>/dev/null || true flux module load sched-fluxion-resource match-format=rv1 - flux module load sched-fluxion-qmanager + flux module load sched-fluxion-qmanager queue-policy=coschedule \ + || flux module load sched-fluxion-qmanager export FLUX_CLI_PLUGINPATH=$PWD/cli-plugins bash tests/integration/test-system-instance.sh SH diff --git a/.gitignore b/.gitignore index 8a09f29..09021f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,15 @@ dist *.egg-info + +# never commit filled in credentials +.env +*.env +!env.example + +# build and python artifacts +__pycache__/ +*.pyc +*.so + +# a sibling checkout the experiments live in +flux-quantum-experiments/ diff --git a/README.md b/README.md index a52b694..72ee6c7 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,139 @@ # flux-quantum -Quantum + classical coscheduling for Flux, including jobtap, validation, and client plugins. -I am developing this against flux-core **flux-core 0.87.0** and my custom branch of flux -sched with support for hold. The quantum backend is external (e,g., IBM/QRMI, AWS Braket) -and informs the work for the flux-sched reservation being held for classical resources. + +Quantum and classical coscheduling for Flux. One `flux submit` produces a pair. +The user's work is submitted held, and a small scout job that requests the vendor +device opens the vendor session, hands the session id to the held job over its +eventlog, and releases it. Developed against flux-core 0.87.0 and the `add-hold` +branch of https://github.com/vsoch/flux-sched, and also loads against flux-core 0.89.0. + ## Components -- **flux_quantum/cli.py** the CLI plugin that runs in user space and (if needed) can use credentials to look at queue depth or cost. -- **flux_quantum/backends/** the vendor libraries. E.g., handling braket will be different than IBM or qrmi, and how we validate will vary. These also run only in user-space. -- **flux_quantum/selector.py** is generic discover and rank over backends. -- **flux_quantum/validator.py** is going to check that we have the right envars, etc. per a backend choice. -- **flux_quantum/jobtap/quantum.c** the owner-side policy gate (runs in the job manager, holds no credentials). At `job.validate` it enforces vendor policy, rejecting a vendor that isn't permitted/available. The scout and the vendor *selection* live in user space (the CLI plugin), since those need the user's credentials -- jobtap only mutates/enforces. (Selecting a vendor *type* in the graph is a future owned-hardware step; the external-vendor model has no qpu resource to select.) -## Install / discovery (flux-core 0.87.0) -The CLI plugin is found by flux via `{confdir}/cli/plugins`, `{libexecdir}/cli/plugins`, or any directory on `FLUX_CLI_PLUGINPATH`. To install: + +- **flux_quantum/cli.py** submit plugin. Runs as the user, picks a vendor, submits the held classical job and turns the submit itself into the scout. +- **flux_quantum/backends/** one module or package per vendor, IBM through QRMI, AWS Braket, IonQ over its REST API, and a mock. The common submit options are mapped to each vendor's terms there, so a user changes nothing but the vendor and the device. They probe queue depth and cost and open and close the session. They only ever run as the user. +- **flux_quantum/selector.py** discovers vendors from the graph and ranks them by policy. +- **flux_quantum/scout.py** and **wrap.py** the scout, and the wrapper that exports `QUANTUM_SESSION_ID` before the user's command runs. +- **flux_quantum/graph.py**, **qresource.py**, **populate.py** add `qdevice_ -> qpu` to the fluxion graph. +- **flux_quantum/jobtap/quantum.c** owner side policy in the job manager. Allowed vendors, admission against reachable cores, protection of both halves, and optional preemption. Holds no credentials. + +## Install + +The CLI plugin is found through `{confdir}/cli/plugins`, `{libexecdir}/cli/plugins`, or a directory on `FLUX_CLI_PLUGINPATH`. + ```bash -flux python -m pip install -e . # install into the python flux uses -export FLUX_CLI_PLUGINPATH=$PWD/cli-plugins # dir holds ONLY the discovery shim -flux submit --help | grep quantum # options should appear -``` -The plugin search dir (`cli-plugins/`) contains only `quantum.py`, a shim that -imports `QuantumCLIPlugin` from the installed package -- do NOT point -`FLUX_CLI_PLUGINPATH` at `flux_quantum/` itself, or flux will try to import the -package's internal modules as standalone plugins and fail. For a system install, -copy `cli-plugins/quantum.py` into `{confdir}/cli/plugins`. -For the jobtap plugin: +flux python -m pip install -e . # into the python flux uses +export FLUX_CLI_PLUGINPATH=$PWD/cli-plugins # holds only the discovery shim +flux submit --help | grep quantum +``` + +`cli-plugins/` contains only `quantum.py`, a shim that imports the installed package. Do not point `FLUX_CLI_PLUGINPATH` at `flux_quantum/` itself, or flux tries to import every module as a plugin. For a system install copy the shim into `{confdir}/cli/plugins`. + +Populate the graph once, before any job runs. Growing it while jobs hold resources corrupts fluxion. + ```bash -make -C flux_quantum/jobtap && flux jobtap load .../quantum.so -``` -Still a WIP I need to test all this with flux-sched! And we need a cute logo. Very important. -## Testing (token-free) -No real vendor token needed. `FLUX_QUANTUM_MOCK=1` registers `mock`/`mock_busy` -backends (credentials always "present", no API calls) and the scout session is -mocked, so the whole pipeline runs without credentials. -- `testing/setup-mock-registry.sh` derives the real graph and injects `qvendor_*` - markers so the selector has vendors to discover (markers are plain graph vertices - added with `flux inject` -- no token). -- `testing/test-mock-e2e.sh` runs it end to end: plugin discovery -> registry -> - `flux submit --quantum-vendor mock` (held) -> scout (mock session) -> handoff -> run. +flux python -m flux_quantum.populate ibm braket ionq +``` + +Build and load the jobtap plugin. `flux jobtap query quantum.so` shows the settings in force and the capacity numbers. + +```bash +make -C flux_quantum/jobtap +flux jobtap load $PWD/flux_quantum/jobtap/quantum.so vendors=ibm,braket,ionq \ + total_cores=128 reserve_cores=32 protect_types=qpu preempt=1 +``` + +The plugin admits a pair only when its cores plus one for the scout are reachable, and only when a core is free right now for the scout, since nothing can be preempted for a pair until its scout runs and releases the classical half. Both halves run at `boost_priority` (default 1000, the scout one higher so it is placed before its classical reserves). A site with flux-accounting sets its own priorities and should load with `boost_priority=0`. Nothing then orders the scout ahead of its classical, which is an open gap for accounting sites and wants a factor inside flux-accounting rather than a second plugin. + +Preemption is off unless `preempt=1`. With it on, the plugin watches each classical half and cancels other work only when three things hold: the scout has released it, so the session is open and costing money; `settle` seconds later (default 2) it is still not running; and fluxion's annotation says it is reserved for later rather than placed. Then it cancels unprotected running jobs for the shortfall, the classical's cores less what is free, lowest priority first and youngest among equals, once per job. A cancelled job loses its work, which is why the switch is off by default. + +## Submitting + +The options are the same whichever vendor it is. + +``` +--quantum-vendor VENDOR ibm, braket, ionq, or mock under FLUX_QUANTUM_MOCK +--quantum-select POLICY pick the vendor instead: any, queue or cost +--quantum-device NAME the vendor's name for the device: a Braket ARN, an IonQ + backend, a QRMI resource id. Each vendor has a default +--quantum-hold MODE session takes the vendor's real hold, a hybrid job on + Braket, a session on IonQ and IBM. probe submits a front + of queue job and holds nothing. Default session +--quantum-hold-max SECS give the hold up after this long. Default the job's + duration plus 300, or 900 without one +--quantum-wait SECS give up and cancel the held job if the hold is not ours + after this long. Default 0, as long as the scout may run +--quantum-dry-run run on the vendor's simulator. FLUX_QUANTUM_MOCK implies it +``` + +```bash +flux submit --quantum-vendor ionq --quantum-dry-run -n1 -- flux python examples/ionq/workload.py +flux submit --quantum-vendor braket --quantum-device arn:aws:braket:us-east-1::device/qpu/ionq/Forte-Enterprise-1 -t 30m -n4 -- ./work +flux submit --quantum-vendor ibm --quantum-device ibm_kingston --quantum-wait 600 -n4 -- flux python examples/qrmi/workload.py +``` + +Settings that tune an installation rather than describe a job are environment variables with the `FLUX_QUANTUM_` prefix, read at submit time. Each backend's docstring lists its own. The ones that matter most: + +``` +FLUX_QUANTUM_BRAKET_HOLD_INSTANCE instance for the hybrid job, ml.m5.large +FLUX_QUANTUM_BRAKET_SHOTS shots for the probe task, 1 +FLUX_QUANTUM_IONQ_SHOTS shots for the warm-up job, 100 +FLUX_QUANTUM_IBM_TYPE QRMI resource type, qiskit-runtime-service +FLUX_QUANTUM_IBM_SKIP_WARMUP release as soon as the session opens +FLUX_QUANTUM_IBM_SESSION_MODE batch, billed for hardware time only, or + dedicated, billed for wall clock until closed +FLUX_QUANTUM_MOCK_SESSION force the mock's session id +FLUX_QUANTUM_MOCK_QUEUE the mock's simulated queue depth +``` + +## Testing + +`FLUX_QUANTUM_MOCK=1` registers the `mock` and `mock_busy` backends, so the whole pipeline runs without a vendor token. Export it before `flux start`, since the ingest validator is a separate process. The unit tests need no broker. + +```bash +pytest tests/unit +flux start bash tests/integration/test-mock-e2e.sh # one submit, held classical and scout, handoff +flux start -s2 bash tests/integration/test-mock-e2e.sh # the two halves on different ranks +FLUX_QUANTUM_MOCK=1 flux start bash tests/integration/test-admission.sh # jobtap admission +FLUX_QUANTUM_MOCK=1 flux start bash tests/integration/test-preemption.sh # jobtap preemption +FLUX_QUANTUM_MOCK=1 flux start bash tests/integration/test-reload.sh # jobtap unload and reload +flux start bash tests/integration/test-select-discovery.sh # vendors discovered from the graph +flux start bash tests/integration/test-ionq-e2e.sh # the IonQ backend against a fake api.ionq.co +bash tests/integration/test-system-instance.sh # against a running instance +``` + +The e2e and discovery scripts load fluxion themselves. The jobtap and system instance scripts expect fluxion to be the scheduler already, as it is in the CI image. `tests/integration/setup-mock-registry.sh` adds `qdevice_*` vertices to a live graph so the selector has something to discover. + +## IonQ + +`flux_quantum/backends/ionq/` talks to the IonQ v0.4 REST API with the standard library and reads the key from `IONQ_API_KEY`. The session hold opens a session on the backend, warms it up with a one qubit job, and releases the classical job once IonQ reports the session active. The session id is what the classical job is handed, and jobs it submits with that id run inside the session. Sessions are in beta and not every account has them, and an account without them is told to use `--quantum-hold probe`, which submits the warm-up job alone and releases when it starts. + +A dry run sends every job to the simulator, which IonQ does not charge for, with the noise model of the backend asked for, so `--quantum-device qpu.forte-1 --quantum-dry-run` runs on the simulator with Forte's noise. The simulator has no queue, so a dry run exercises the plumbing and not the hold. + +```bash +export IONQ_API_KEY=... +flux submit --quantum-vendor ionq --quantum-dry-run -n1 -- flux python examples/ionq/workload.py +flux submit --quantum-vendor ionq --quantum-device qpu.forte-1 -n1 -- flux python examples/ionq/workload.py +``` + +`python -m flux_quantum.backends.ionq.fake` runs a stand-in for the service, so the backend and the whole pipeline run with no key at all. `IONQ_API_URL` points the backend at it. It listens on the loopback, so on an instance of more than one node start it with `--bind 0.0.0.0` and use the URL it prints, which the other nodes can reach. `FAKE_IONQ_QUEUE=30-90` gives it a device queue, seconds a job waits before it is served, which a started session's jobs skip, and `POST /fake/config` changes it while running. + +## Braket priority probe + +`tests/probe_hold.py` measures the thing this branch depends on: whether a task submitted from outside the hybrid job, carrying its token, is served ahead of the device queue. It opens the hold, submits a control task without the token, then the token task after it, and watches both until the token task finishes. Braket only reports a task's queue, Normal or Priority, and its position when `GetQuantumTask` is asked for `QueueInfo`, so the probe asks, and it reads the finishing order from the service timestamps. Both come from the tasks themselves. The device-wide counters lag a submit by minutes and are not used for the verdict. + +```bash +python3 tests/probe_hold.py --survey # which QPUs are open and have a queue +python3 tests/probe_hold.py # SV1, shows only that the token associates +AWS_DEFAULT_REGION=us-east-1 python3 tests/probe_hold.py \ + --device arn:aws:braket:us-east-1::device/qpu/ionq/Forte-Enterprise-1 \ + --shots 100 --max-seconds 1800 +``` + +`--max-seconds` is the hold, and the token task has to finish inside it, so give a device where tasks take minutes half an hour. The hold is released as soon as the token task finishes, and the control is then followed to its end unless `--cancel-control` is passed. Each run appends a JSON line to `probe-runs.jsonl` with both tasks' queue, position, timestamps and a timeline, and everything printed, the backend's progress included, is also appended to `probe-hold.log` (`--log PATH` to change it, `--log ''` to disable). It costs two tasks at the device rate plus the hold instance, and prints the estimate before creating anything. QuEra Aquila takes an analog program rather than a circuit, and the probe sends one when the ARN is QuEra's. `--check-program` runs the program on the local simulator first, free, and the analog simulator applies Aquila's own limits. ## License -DevTools is distributed under the terms of the MIT license. + +flux-quantum is distributed under the terms of the MIT license. All new contributions must be made under this license. -See [LICENSE](https://github.com/converged-computing/cloud-select/blob/main/LICENSE), -[COPYRIGHT](https://github.com/converged-computing/cloud-select/blob/main/COPYRIGHT), and -[NOTICE](https://github.com/converged-computing/cloud-select/blob/main/NOTICE) for details. +See [LICENSE](LICENSE), [COPYRIGHT](COPYRIGHT), and [NOTICE](NOTICE) for details. SPDX-License-Identifier: (MIT) LLNL-CODE- 842614 diff --git a/examples/ionq/workload.py b/examples/ionq/workload.py new file mode 100755 index 0000000..388f183 --- /dev/null +++ b/examples/ionq/workload.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Example classical workload for IonQ. + +Reads the session the scout opened and the target it was given, submits a +Bell circuit into the session, waits for it and prints the histogram. This +is what the classical half of a pair does. The key comes from the user's +environment, the rest from the scout. + + flux submit --quantum-vendor ionq --quantum-dry-run -n1 \\ + -- flux python examples/ionq/workload.py +""" + +import json +import os +import sys +import time +import urllib.request + +BELL = { + "qubits": 2, + "gateset": "qis", + "circuit": [ + {"gate": "h", "target": 0}, + {"gate": "cnot", "control": 0, "target": 1}, + ], +} + + +def call(method, path, body=None): + url = (os.environ.get("IONQ_API_URL") or "https://api.ionq.co/v0.4").rstrip("/") + req = urllib.request.Request( + url + path, + data=None if body is None else json.dumps(body).encode(), + method=method, + ) + req.add_header("Authorization", "apiKey " + os.environ["IONQ_API_KEY"]) + req.add_header("Content-Type", "application/json") + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read() or b"{}") + + +def main(): + session = os.environ.get("QUANTUM_SESSION_ID") + if not session: + sys.exit("no QUANTUM_SESSION_ID, this job was not started by the scout") + if not os.environ.get("IONQ_API_KEY"): + sys.exit("no IONQ_API_KEY in the environment") + target = os.environ.get("IONQ_BACKEND", "simulator") + print("session:", session) + print("target: ", target) + + body = { + "type": "ionq.circuit.v1", + "name": "flux-quantum-bell", + "shots": 100, + "backend": target, + "input": BELL, + } + # a probe hold hands over a job id, not a session, and a job id is not + # something another job can be submitted into + if not session.startswith("job:"): + body["session_id"] = session + noise = os.environ.get("IONQ_NOISE_MODEL") + if noise: + body["noise"] = {"model": noise} + + job = call("POST", "/jobs", body) + print("job:", job["id"], job.get("status")) + while True: + state = call("GET", "/jobs/{}".format(job["id"])) + if state.get("status") in ("completed", "failed", "canceled"): + break + time.sleep(2) + print("status:", state.get("status")) + if state.get("status") == "completed": + print("results:", call("GET", "/jobs/{}/results".format(job["id"]))) + + +if __name__ == "__main__": + main() diff --git a/examples/qrmi/README.md b/examples/qrmi/README.md new file mode 100644 index 0000000..ccf2ace --- /dev/null +++ b/examples/qrmi/README.md @@ -0,0 +1,131 @@ +# Running against IBM through QRMI + +This is the demo for using qrmi with flux-quantum. + +## Install + +QRMI needs python 3.11 or newer. It is not a dependency of flux-quantum, so + + pip install "qrmi[ibm]" + +Install it into whatever python `flux python` uses, on every node that could run +the scout. qiskit comes along with it and we need that for the warmup task. + +## Credentials + +QRMI reads its config out of the environment. Every variable is prefixed with the +resource id, so for ibm_kingston the api key lives in +`ibm_kingston_QRMI_IBM_QRS_IAM_APIKEY`. Four of them are required. + +The passcode login below can stop accepting credentials. If it does, use the +API key login that follows instead. + +```bash +ibmcloud login -a https://cloud.ibm.com -u passcode -p +``` +```bash +export IBM_CLOUD_TOKEN= +ibmcloud login --apikey $IBM_CLOUD_TOKEN +export IBM_CLOUD_CRN=$(ibmcloud resource service-instances --service-name quantum-computing --output json | jq -r '.[] | {name: .name, crn: .crn}' | jq -r .crn) +``` + +Check before you submit anything. + + flux python -c " + from flux_quantum.backends import get_backend + print(get_backend('ibm').credential_note)" + +Constructing the backend is the check. It raises naming any variable you are +missing, and never prints a value. + + +unset FLUX_QUANTUM_MOCK +flux submit -t 30m \ + --quantum-vendor ibm --quantum-device \ + --quantum-wait 600 \ + -n4 flux python /opt/flux-quantum/examples/qrmi/workload.py + +flux jobs -a +flux job attach # this is where a Premium refusal shows up +flux job eventlog | grep memo + +## Submit + + flux submit -t 30m --quantum-vendor ibm --quantum-device ibm_kingston \ + -n4 flux python examples/qrmi/workload.py + +stdout is the scout id. The classical job id comes back on stderr. If only one +resource has credentials in the environment you can drop +`--quantum-device` and we work it out. + + flux jobs -a + flux job eventlog | grep memo + flux job attach + +## You need an account that can open sessions + +The qiskit-runtime-service type opens a session, which IBM only allows on plans that support sessions (Premium). Open and Pay As You Go plans can submit tasks but not hold a session, IBM returns a 403, and flux-quantum cancels the held classical job. Export `FLUX_QUANTUM_IBM_TYPE=ibm-quantum-system` for direct access instead. + +## A session is not the same as having the QPU + +An IBM session goes active when its first task reaches the head of the queue, +and later tasks in the session inherit that priority. So opening a session +tells you little by itself. After acquiring, the scout submits a small warmup +task (one qubit, one measurement) and waits for it to run before releasing the +classical job. If the warmup never runs, the scout releases the session and +fails the submit instead of starting the classical job against a QPU it does +not have. + + --quantum-wait SECONDS give up after this long. Default 0, + which means wait as long as the + scout job is allowed to live + FLUX_QUANTUM_IBM_SKIP_WARMUP=1 release as soon as the session opens, + no priority guarantee + FLUX_QUANTUM_IBM_READY_TIMEOUT=N default 120, how long to wait for the + backend to report itself up + +The warmup costs one shot. + +## The scout holds the session for as long as the classical runs + +The fluxion allocation and the vendor session start and end together, so +you're billed for the classical job's wall time, not the quantum work. Set +`-t` accordingly. + +A killed scout still releases, since the release runs in a finally block and +SIGTERM is handled. If the node dies, you're down to the vendor's own timeout. + +## Credentials + +The plugin runs in your process at submit time and reads the variables from +your environment. The scout runs as you and uses them too. Only the session +id reaches the classical job, over the job eventlog. + +## What the classical job sees + + QUANTUM_SESSION_ID the open session + _QRMI_JOB_ACQUISITION_TOKEN same value, where QRMI looks + QRMI_JOB_QPU_RESOURCES the resource id + QRMI_JOB_QPU_TYPES the resource type + +All but the first are what the Slurm and LSF QRMI plugins set, so +`get_job_qpu_resources_and_types()` works and a workload written for Slurm runs +here untouched. See workload.py. + +## Other QRMI settings + +QRMI reads these itself, prefixed with the resource id. Put them in `.env` if you +need them. + + _QRMI_IBM_QRS_SESSION_MODE batch or dedicated + _QRMI_IBM_QRS_SESSION_MAX_TTL session lifetime + _QRMI_IBM_QRS_TIMEOUT_SECONDS request timeout + _QRMI_IBM_QRS_SESSION_ID join a session that already exists + +## When it breaks + + flux job attach + +The scout says why it gave up. Credential problems name the variable. Set +`RUST_LOG=debug` before submitting if you want QRMI's own logs, including the +HTTP calls. diff --git a/examples/qrmi/container/Dockerfile b/examples/qrmi/container/Dockerfile new file mode 100644 index 0000000..f3f0b32 --- /dev/null +++ b/examples/qrmi/container/Dockerfile @@ -0,0 +1,147 @@ +# syntax=docker/dockerfile:1 +# The flux-quantum coscheduling stack in one container, for the QRMI demo. +# docker build -f examples/qrmi/container/Dockerfile -t flux-quantum-qrmi --build-arg FLUX_QUANTUM_REF= . + +FROM ubuntu:24.04 +ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 + +ARG FLUX_SECURITY_VERSION=0.15.0 +ARG FLUX_CORE_VERSION=0.87.0 +ARG FLUX_SCHED_REPO=https://github.com/vsoch/flux-sched +ARG FLUX_SCHED_BRANCH=hold +# a shallow clone has no tags to describe, so cmake is told a version. It +# only names the build, the branch is what matters. +ARG FLUX_SCHED_VERSION=0.56.0 +ARG FLUX_QUANTUM_REPO=https://github.com/converged-computing/flux-quantum +# the branch to clone, as the cluster's start-script does. Empty means use the +# build context, this checkout, which is the only way to get work that is not +# pushed yet into the image. +ARG FLUX_QUANTUM_REF=braket-hybrid +# pip spec for qrmi. Pin here, e.g. "qrmi[ibm]==0.26.0", for a reproducible image +ARG QRMI_SPEC="qrmi[ibm]" +ARG INSTALL_IBMCLOUD=1 + +# build and runtime dependencies of flux-core and flux-sched, the list from +# the AMI build script, in ubuntu 24.04 package names +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl wget git jq vim less sudo apt-utils \ + build-essential pkg-config autotools-dev libtool libffi-dev \ + autoconf automake make gcc g++ cmake ninja-build flex \ + lua5.4 liblua5.4-dev lua-posix \ + python3-dev python3-pip python3-yaml \ + libsodium-dev libzmq3-dev libczmq-dev libjansson-dev \ + munge libmunge-dev libncursesw5-dev liblz4-dev libsqlite3-dev \ + uuid-dev libhwloc-dev libevent-dev libarchive-dev libpam-dev \ + libboost-dev libboost-graph-dev libboost-system-dev \ + libboost-filesystem-dev libboost-regex-dev libyaml-cpp-dev \ + libedit-dev \ + && rm -rf /var/lib/apt/lists/* + +# flux-core's python bindings need cffi and ply, and qrmi later needs a newer +# cffi than debian ships. A debian cffi has no pip RECORD, so pip cannot +# replace it and the qrmi install fails. Install these with pip from the +# start, like the CI image does, so pip owns them and can upgrade them. +RUN python3 -m pip install --break-system-packages --no-cache-dir \ + --upgrade --ignore-installed cffi ply pyyaml jsonschema + +WORKDIR /opt + +# flux-core 0.87 needs flux-security 0.13 or newer +RUN wget -q https://github.com/flux-framework/flux-security/releases/download/v${FLUX_SECURITY_VERSION}/flux-security-${FLUX_SECURITY_VERSION}.tar.gz \ + && tar -xzf flux-security-${FLUX_SECURITY_VERSION}.tar.gz \ + && cd flux-security-${FLUX_SECURITY_VERSION} \ + && ./configure --prefix=/usr --sysconfdir=/etc \ + && make -j"$(nproc)" && make install && ldconfig \ + && cd /opt && rm -rf flux-security-${FLUX_SECURITY_VERSION}* + +# prefix /usr and sysconfdir /etc, like the AMI. flux python is then +# /usr/bin/python3, the python everything below installs into. +RUN wget -q https://github.com/flux-framework/flux-core/releases/download/v${FLUX_CORE_VERSION}/flux-core-${FLUX_CORE_VERSION}.tar.gz \ + && tar -xzf flux-core-${FLUX_CORE_VERSION}.tar.gz \ + && cd flux-core-${FLUX_CORE_VERSION} \ + && ./configure --prefix=/usr --sysconfdir=/etc --runstatedir=/run/flux \ + --with-flux-security --disable-docs \ + && make -j"$(nproc)" && make install && ldconfig \ + && cd /opt && rm -rf flux-core-${FLUX_CORE_VERSION}* + +# CMAKE_INSTALL_SYSCONFDIR must be the absolute /etc. flux-sched derives its +# rc1.d path from it before adopting flux-core's prefix, and a relative "etc" +# lands the fluxion rc script in /usr/etc/flux/rc1.d where flux-core never +# looks, so fluxion silently never loads and the instance runs sched-simple. +RUN git clone --depth 1 -b "${FLUX_SCHED_BRANCH}" "${FLUX_SCHED_REPO}" /opt/flux-sched \ + && cd /opt/flux-sched && git log --oneline -1 \ + && FLUX_SCHED_VERSION=${FLUX_SCHED_VERSION} cmake -B build \ + -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_SYSCONFDIR=/etc \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + && cmake --build build -j"$(nproc)" \ + && cmake --build build -t install && ldconfig \ + && test -x /etc/flux/rc1.d/01-sched-fluxion \ + && test -f /etc/flux/modprobe/modprobe.d/fluxion.toml \ + && rm -rf /opt/flux-sched/build + +# qrmi into the python flux uses. The flux-quantum ibm backend still speaks +# the pre-0.23 names (ResourceType.IBMQiskitRuntimeService, the QRMI_IBM_QRS_ +# variables), which qrmi keeps as deprecated aliases until 2026-11-21. Fail +# the build now, rather than at the first submit, if the installed qrmi has +# dropped them. +RUN python3 -m pip install --break-system-packages --no-cache-dir "${QRMI_SPEC}" +RUN python3 - <<'PY' +import qrmi, qiskit +from qrmi import ResourceType +print("qrmi", getattr(qrmi, "__version__", "?"), "qiskit", qiskit.__version__) +assert hasattr(ResourceType, "IBMQiskitRuntimeService"), ( + "this qrmi has dropped ResourceType.IBMQiskitRuntimeService. Pin an older " + "qrmi with --build-arg QRMI_SPEC='qrmi[ibm]==' or migrate " + "flux_quantum/backends/qrmi.py to the ibm-quantum-compute-service names") +PY + +RUN if [ "${INSTALL_IBMCLOUD}" = 1 ]; then \ + curl -fsSL https://clis.cloud.ibm.com/install/linux | sh \ + && ibmcloud version; \ + fi + +# the demo runs as a user, the scout and the classical job both run as the +# submitting user and that is where the credentials live +ARG USERNAME=ubuntu +RUN echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} \ + && chmod 0440 /etc/sudoers.d/${USERNAME} \ + && mkdir -p /etc/flux/system/jobtap /etc/flux/system/conf.d + +# flux-quantum, from the branch or from this checkout. Editable, so a bind +# mount of the repository over /opt/flux-quantum picks up edits without a +# rebuild. The jobtap plugin is rebuilt from source, since a checkout may +# carry a quantum.so built against another flux-core. The scripts under +# examples/qrmi/container/bin come from the same tree, so a cloned branch +# has to carry this directory already. +COPY --chown=${USERNAME}:${USERNAME} . /opt/flux-quantum-checkout +RUN if [ -n "${FLUX_QUANTUM_REF}" ]; then \ + git clone --depth 1 -b "${FLUX_QUANTUM_REF}" "${FLUX_QUANTUM_REPO}" /opt/flux-quantum \ + && rm -rf /opt/flux-quantum-checkout; \ + else \ + mv /opt/flux-quantum-checkout /opt/flux-quantum; \ + fi \ + && cd /opt/flux-quantum && (git log --oneline -1 || echo "flux-quantum: this checkout") \ + && { test -d /opt/flux-quantum/examples/qrmi/container/bin \ + || { echo "flux-quantum: ${FLUX_QUANTUM_REF:-the checkout} has no examples/qrmi/container/bin." \ + "Push the directory, or build with --build-arg FLUX_QUANTUM_REF= to use this checkout." >&2; exit 1; }; } \ + && python3 -m pip install --break-system-packages --no-cache-dir -e /opt/flux-quantum \ + && flux python -c "from flux_quantum.cli import QuantumCLIPlugin; print('flux_quantum importable')" \ + && rm -f /opt/flux-quantum/flux_quantum/jobtap/quantum.so \ + && make -C /opt/flux-quantum/flux_quantum/jobtap \ + && cp /opt/flux-quantum/flux_quantum/jobtap/quantum.so /etc/flux/system/jobtap/ \ + && chown -R ${USERNAME}:${USERNAME} /opt/flux-quantum \ + && install -m 0755 /opt/flux-quantum/examples/qrmi/container/bin/* /usr/local/bin/ \ + && chmod +x /opt/flux-quantum/examples/qrmi/ibm-run.sh \ + && ln -sf /opt/flux-quantum/examples/qrmi/ibm-run.sh /usr/local/bin/ibm-run + +# FLUX_CLI_PLUGINPATH holds only the discovery shim, never the package. +# FLUX_QUANTUM_MOCK registers the token free mock vendor. It has to be in the +# environment before flux start, because the ingest validator is a separate +# process. Real vendors are registered regardless, so it stays on. +ENV FLUX_CLI_PLUGINPATH=/opt/flux-quantum/cli-plugins \ + FLUX_QUANTUM_MOCK=1 \ + FLUX_QUANTUM_JOBTAP=/etc/flux/system/jobtap/quantum.so + +USER ${USERNAME} +WORKDIR /opt/flux-quantum/examples/qrmi/container +CMD ["flux-quantum-start"] diff --git a/examples/qrmi/container/Dockerfile.dockerignore b/examples/qrmi/container/Dockerfile.dockerignore new file mode 100644 index 0000000..d91dd13 --- /dev/null +++ b/examples/qrmi/container/Dockerfile.dockerignore @@ -0,0 +1,15 @@ +# Context is the repository root. Keep the scratch and the build trees out. +.git +.claude +.devcontainer +.vscode +flux-sched +flux_quantum.egg-info +**/__pycache__ +**/*.pyc +**/*.so +*.jsonl +*.log +*.patch +*.diff +*.png diff --git a/examples/qrmi/container/README.md b/examples/qrmi/container/README.md new file mode 100644 index 0000000..0cebc4c --- /dev/null +++ b/examples/qrmi/container/README.md @@ -0,0 +1,284 @@ +# flux-quantum with QRMI + +This container builds a single node Flux instance you can start, and test a pipeline for Flux with QRMI first without a token (mock) and then run one cheap coscheduled job against an IBM QPU through QRMI. Feel free to tweak that and run a more expensive one, if you please. + +| component | version | why? | +|---------------|------------------------------------------|-----------------------------------------| +| ubuntu | 24.04, python 3.12 | qrmi needs python 3.11 or newer | +| flux-security | 0.15.0 | release tarball | +| flux-core | 0.87.0 | release tarball, prefix /usr | +| flux-sched | vsoch/flux-sched branch `hold` | git, cmake, sysconfdir /etc | +| flux-quantum | branch `braket-hybrid`, or this checkout | git clone like the cluster, editable | +| qrmi | latest `qrmi[ibm]` on PyPI, with qiskit | pip | +| ibmcloud cli | latest | clis.cloud.ibm.com | + +Since this is in a container, we have no systemd or munge. Keep in mind these would be part of a production setup, and are part of the setup where we are developing. The instance is started with `flux start`, run as the `ubuntu` user, and the scheduler is configured through `--config-path` with the same two config tables the cluster writes to `/etc/flux/system/conf.d`. Everything the scout and the classical job do is the same. + +## 1. Build + +From the repository root: + +```bash +uri=ghcr.io/converged-computing/flux-quantum:qrmi-demo-10-8-2026 +docker build -f examples/qrmi/container/Dockerfile -t $uri . +``` + +flux-quantum is cloned from the `braket-hybrid` branch, the same clone the +cluster's start-script makes, so the image runs what is pushed. To put this +checkout in instead, uncommitted changes and all, pass an empty ref. The +build context is the repository root either way, and `Dockerfile.dockerignore` +keeps the build trees out of it. + +```bash +docker build -f examples/qrmi/container/Dockerfile -t flux-quantum-qrmi \ + --build-arg FLUX_QUANTUM_REF= . +``` + +A cloned branch has to carry this directory already, since the scripts are +installed from it. Until it is pushed, build from the checkout. + +To build another flux-sched branch or flux-core, or to pin qrmi: + +```bash +docker build -f examples/qrmi/container/Dockerfile -t flux-quantum-qrmi \ + --build-arg FLUX_SCHED_BRANCH=add-hold \ + --build-arg FLUX_CORE_VERSION=0.87.0 \ + --build-arg QRMI_SPEC='qrmi[ibm]==0.26.0' . +``` + +## 2. Design + +Let's talk about the coscheduling design. We add a flux quantum client plugin, which is an interface to customize flux commands for submit, run, batch, etc. With flux-quantum, we can add flags however we like that will say "I want to run a quantum job." Our strategy is simple. We take a request for hybrid quantum work (classical and quantum resources) and split it into a hybrid pair. + +- A classical job that is "held" until specifically told to release (a form of reservation) +- A scout job or probe that will create a quantum session, monitor it, and release the classical resources when it is ready. + +With this approach, no classical resources are running until the quantum is ready. Smaller jobs can backfill into the space with lower priority that can be pre-empted. The design uses priority and ensures that we do not accept more hybrid jobs than could be supported given the size of the cluster resources, and contender pre-emptible jobs. Full experiments to demonstrate the approach are pending - we have only mocked the setup due to funding limitations. + +## 3. Run the container + +The pair runs as the user (you) and for this case you are the Ubuntu user in the container. QRMI reads the credentials from your environment, so the credentials come in as environment variables. For the token free +mock, none are needed. + +```bash +docker run -it --rm $uri +``` + +You will be in a Flux instance. Take a look. + +```bash +flux resource list +``` +```console + STATE NNODES NCORES NGPUS NODELIST + free 1 8 0 eda311ea73f7 + allocated 0 0 0 + down 0 0 0 +``` + +Take a look to see that fluxion is handling scheduling. + +```bash +flux module list +``` + +Take a look that we have the quantum plugin loaded. + +```bash +flux jobtap list +quantum.so +``` +```bash +$ flux jobtap query quantum.so | jq +{ + "name": "quantum.so", + "path": "/etc/flux/system/jobtap/quantum.so", + "total_cores": 16, + "reserve_cores": 0, + "preempt": false, + "settle": 2.0, + "boost_priority": 1000, + "vendors": "ibm,mock,braket,ionq", + "protect_types": "qpu", + "free_cores": 16, + "preemptible_cores": 0, + "promised_cores": 0, + "tracked_jobs": 0, + "pending_decisions": 0 +} +``` + +The plugin above is for validation, ensuring that we can support a vendor, that the request is feasible given that size of the cluster and the plugin will also be able to restore state on reload of Flux. Note that when you log in, the entrypoint is a `flux start --config-path=... flux-quantum-init` and note the config path. The config is what loads the jobtap plugin, and configures the job manager. + +```toml +[sched-fluxion-qmanager] +queue-policy = "coschedule" + +[job-manager] +plugins = [ + { load = "/etc/flux/system/jobtap/quantum.so", + conf = { vendors = "ibm,mock,braket,ionq", protect_types = "qpu", + total_cores = 8, reserve_cores = 0 } } +] +``` + +We use a `coschedule` queue policy that lets a held job reserve. + + +## 4. Run a Mock Example + +Let's start with a mock example, and check the setup before anything else: + +```bash +flux-quantum-check # fluxion, policy, jobtap, cli plugin, graph, qrmi +flux-quantum-check --ibm # also the credentials, by name +``` + +Here is a full demo without you having to think (mocked) + +```bash +flux-quantum-demo mock +``` + +Some notes on the output: + +- **the submit** returns the scout id on stdout and says `held classical job ` on stderr. One submit, two jobs. +- **flux jobs** shows the classical in `SCHED` while the scout runs. It is held, with its cores reserved, until the scout releases it. +- **the scout's output** says it opened the session, waited for priority, released the classical, then waited for the classical to finish before closing the session. The qpu vertex is allocated to the scout the whole time. +- **the classical eventlog** carries a `memo` with `quantum_session` before its `alloc`. That memo is the handoff. No shared filesystem is involved, which is why the two halves can land on different nodes. +- **the classical's output** prints the session it was given. + +And here is doing it by hand: + +```bash +flux submit --quantum-vendor mock -n1 -- flux python /opt/flux-quantum/examples/qrmi/container/workload.py + +# Look at the jobs to see the scout and classical +flux jobs -a +flux job attach +flux job eventlog | grep memo +flux job attach +``` + +`FLUX_QUANTUM_MOCK_QUEUE=30 flux submit ...` gives the mock a queue so the scout visibly waits, and `flux-quantum-start -s 2` gives the pair two ranks to land on. + +## 5. IBM through QRMI + +### Credentials + +QRMI reads its configuration from the environment. The easiest thing is just to export them to the environment in the container. Exporting them in the shell inside the running instance is enough. flux copies your environment into both halves of the pair, and the ingest validator checks, by name, that the variables the chosen resource needs are in the job's environment. It never reads the values and has no credentials of its own. Note that you usually need to login to ibmcloud. This does not usually work for me and I will copy paste the quick credentials from the IBM Cloud interface. + +```bash +export IBM_CLOUD_TOKEN=... +ibmcloud login --apikey "$IBM_CLOUD_TOKEN" +export IBM_CLOUD_CRN=$(ibmcloud resource service-instances --service-name quantum-computing --output json | jq -r '.[0].crn') +export RESOURCE=ibm_kingston +``` + +You can check credentials are set: + +### Choose a backend + +Then choose a backend: + +```bash +flux python -c " +from qiskit_ibm_runtime import QiskitRuntimeService +import os +svc = QiskitRuntimeService(channel='ibm_quantum_platform', + token=os.environ['IBM_CLOUD_TOKEN'], instance=os.environ['IBM_CLOUD_CRN']) +for b in svc.backends(operational=True, simulator=False): + print(b.name, b.status().pending_jobs)" +``` + +Export given your choice: + +```bash +export ibm_kingston_QRMI_IBM_QRS_ENDPOINT=https://quantum.cloud.ibm.com/api/v1 +export ibm_kingston_QRMI_IBM_QRS_IAM_ENDPOINT=https://iam.cloud.ibm.com +export ibm_kingston_QRMI_IBM_QRS_IAM_APIKEY=$IBM_CLOUD_TOKEN +export ibm_kingston_QRMI_IBM_QRS_SERVICE_CRN=$IBM_CLOUD_CRN +``` + +```bash +flux-quantum-check --ibm +``` + +Your plan has to allow sessions. The `qiskit-runtime-service` type opens a real session, which IBM permits on plans that support them. On a plan that does not, IBM answers 403, the scout explains, and cancels the held +classical job. + + +### Do a test run + +`ibm-run` submits a classical job that prints the session and exits. The billed window is the one shot warm-up task plus a few seconds, and it ends with what IBM says it charged. + +```bash +RESOURCE=ibm_kingston ibm-run +SKIP_WARMUP=1 ibm-run # free: opens and closes a session, no task +``` + +### Do a session run + +`flux-quantum-demo ibm` is the same pair with this directory's `workload.py` as the classical half. The workload joins the session the scout opened, transpiles a two qubit Bell circuit for the device, runs it +through the QRMI SamplerV2 with 100 shots, and prints the counts. It asks before submitting. + +```bash +RESOURCE=ibm_kingston flux-quantum-demo ibm +SHOTS=50 WALLTIME=3m flux-quantum-demo ibm -y +``` + +The above shows a lot of how it works and metadata. If you need to debug further, in another terminal you can shell into the container to check the queue: + +```bash +docker exec -it sweet_mclean bash + +# This will be at a similar path +flux proxy local:///tmp/flux-2Ge9tP/local-0 bash +flux resource list +``` +```console +STATE NNODES NCORES NGPUS NODELIST + free 1 7 0 1dcdc3d5f315 + allocated 1 1 0 1dcdc3d5f315 + down 0 0 0 +``` +Export your access key again: + +```bash +export IBM_CLOUD_TOKEN=... +export IBM_CLOUD_CRN=$(ibmcloud resource service-instances --service-name quantum-computing --output json | jq -r '.[0].crn') +``` + +Then look at the queue: + +```bash +$ flux python -c " +import os +from qiskit_ibm_runtime import QiskitRuntimeService +svc = QiskitRuntimeService(channel='ibm_quantum_platform', token=os.environ['IBM_CLOUD_TOKEN'], instance=os.environ['IBM_CLOUD_CRN']) +print('pending on ibm_kingston:', svc.backend('ibm_kingston').status().pending_jobs) +for j in svc.jobs(limit=3): print(j.job_id(), j.status())" +``` + +For my testing, I saw previous jobs, and the queue depth. + +```bash +qiskit_runtime_service._discover_account:WARNING:2026-10-08 21:11:12,194: Loading account with the given token. A saved account will not be used. +pending on ibm_kingston: 13 +db40assvf2bc73ctt390 QUEUED +d9uj04535hes73fk0950 DONE +d9uip7343mgs73et1g4g CANCELLED +``` + +To show the command here that the script prints: + +```bash +flux submit -t 5m --quantum-vendor ibm --quantum-device ibm_kingston --quantum-wait 1800 \ + -n1 -- flux python examples/qrmi/container/workload.py --shots 100 +flux jobs -a +flux job attach # a 403 or a credential problem shows up here +flux job eventlog | grep memo +flux job attach # metadata, circuit, status, counts +``` + +`--quantum-wait` is how long the scout waits for the one shot warm-up task to leave the queue. An IBM session goes active when its first task reaches the head of the queue. If it times out the scout closes the session and cancels the classical. diff --git a/examples/qrmi/container/bin/flux-quantum-check b/examples/qrmi/container/bin/flux-quantum-check new file mode 100755 index 0000000..66fdba1 --- /dev/null +++ b/examples/qrmi/container/bin/flux-quantum-check @@ -0,0 +1,103 @@ +#!/bin/bash +# Verify the stack from inside the instance, before spending anything. +# Every check prints what it found. Hard failures make the exit code nonzero. +# +# flux-quantum-check inside flux-quantum-start +# flux-quantum-check --ibm also check the IBM credentials by name +set -u +rc=0 +ok() { echo " ok $*"; } +bad() { echo " FAIL $*"; rc=1; } +warn() { echo " warn $*"; } + +echo "== versions ==" +flux version | sed 's/^/ /' +flux python -c "import sys; print(' flux python', sys.version.split()[0], sys.executable)" +flux python -c "import flux_quantum; print(' flux_quantum', flux_quantum.__file__)" || bad "flux_quantum is not importable from flux python" + +echo "== instance ==" +if flux resource list >/dev/null 2>&1; then + flux resource list | sed 's/^/ /' +else + bad "no instance. Run this inside flux-quantum-start"; exit 1 +fi + +echo "== scheduler ==" +if flux module list | grep -qE "sched-fluxion-(resource|qmanager)"; then + ok "fluxion is the scheduler" +else + bad "fluxion is not loaded, sched-simple cannot hold a job" +fi +policy=$(flux config get sched-fluxion-qmanager.queue-policy 2>/dev/null || true) +if [ "$policy" = coschedule ]; then ok "queue-policy coschedule"; else bad "queue-policy is '${policy:-unset}', want coschedule"; fi + +echo "== jobtap plugin ==" +if flux jobtap list 2>/dev/null | grep -q quantum; then + ok "quantum.so loaded" + flux jobtap query quantum.so 2>/dev/null | flux python -c " +import json, sys +try: + q = json.load(sys.stdin) +except Exception: + sys.exit(0) +for k in ('vendors', 'protect_types', 'total_cores', 'reserve_cores', 'preempt', 'settle', 'boost_priority'): + if k in q: + print(' {} = {}'.format(k, q[k])) +" +else + warn "quantum.so is not loaded, no admission control (flux dmesg | grep quantum)" +fi + +echo "== cli plugin ==" +if flux submit --help 2>&1 | grep -q -- --quantum-vendor; then + ok "flux submit has the --quantum options (FLUX_CLI_PLUGINPATH=${FLUX_CLI_PLUGINPATH:-unset})" +else + bad "no --quantum options. FLUX_CLI_PLUGINPATH=${FLUX_CLI_PLUGINPATH:-unset}" +fi + +echo "== vendor devices in the graph ==" +# the same find RPC populate uses, so this works without the flux-sched +# python commands installed +found=$(flux python -c " +import flux +from flux_quantum import graph +print(' '.join('qdevice_' + v for v in sorted(graph.vendors_present(graph.get_live_graph(flux.Flux())))))" 2>/dev/null) +if [ -n "$found" ]; then ok "$found"; else bad "no qdevice vertices, run flux python -m flux_quantum.populate ibm mock"; fi + +echo "== qrmi ==" +if flux python -c "import qrmi, qiskit; print(' qrmi', getattr(qrmi, '__version__', '?'), 'qiskit', qiskit.__version__)" 2>/dev/null; then + ok "qrmi and qiskit import" + flux python -c " +from qrmi import ResourceType +print(' legacy ibm type present:', hasattr(ResourceType, 'IBMQiskitRuntimeService'))" +else + bad "qrmi does not import. pip install 'qrmi[ibm]' needs python 3.11 or newer" +fi +if command -v ibmcloud >/dev/null; then + ok "ibmcloud $(ibmcloud version 2>/dev/null | head -1)" +else + warn "no ibmcloud cli, set IBM_CLOUD_CRN by hand for ibm-run" +fi + +echo "== mock vendor ==" +if [ -n "${FLUX_QUANTUM_MOCK:-}" ]; then ok "FLUX_QUANTUM_MOCK set, mock vendor registered"; else warn "FLUX_QUANTUM_MOCK unset, no token free rehearsal"; fi + +if [ "${1:-}" = "--ibm" ]; then + echo "== ibm credentials (names only, never values) ==" + # constructing the backend is the check, it raises naming what is missing + if out=$(flux python -c " +from flux_quantum.backends import get_backend +print(get_backend('ibm').credential_note)" 2>&1); then + ok "$out" + else + bad "$(echo "$out" | tail -1)" + fi +fi + +echo +if [ "$rc" -eq 0 ]; then + echo "flux-quantum-check: all good" +else + echo "flux-quantum-check: something is off, see FAIL above" +fi +exit "$rc" diff --git a/examples/qrmi/container/bin/flux-quantum-demo b/examples/qrmi/container/bin/flux-quantum-demo new file mode 100755 index 0000000..da3d9e3 --- /dev/null +++ b/examples/qrmi/container/bin/flux-quantum-demo @@ -0,0 +1,133 @@ +#!/bin/bash +# One coscheduled pair, end to end, with the output that shows the handoff. +# +# flux-quantum-demo mock token free rehearsal, the mock vendor +# flux-quantum-demo ibm the real thing, IBM through QRMI. Costs. +# flux-quantum-demo ibm -y skip the confirmation +# +# The ibm run needs QRMI credentials for one resource in the environment, +# either the four _QRMI_IBM_QRS_* variables (see ../env.example), +# or IBM_CLOUD_TOKEN plus IBM_CLOUD_CRN plus RESOURCE, from which the four are +# derived here the same way ibm-run derives them. +# +# RESOURCE the QRMI resource id, an IBM backend name like ibm_kingston. +# Optional when exactly one resource is configured +# SHOTS shots for the Bell circuit the classical job submits, 100 +# WALLTIME the classical job's -t, 5m. The session lives this long at most +# WAIT --quantum-wait, how long the scout waits for the warm-up task +# to reach the head of the queue, 1800. Waiting is free +# NTASKS cores for the classical job, 1 +# OUT where the transcript goes, ~/flux-quantum-demo-