Compile & run Quantum Intermediate Representation (QIR) programs on Rigetti QCS.
The easiest way to start is by downloading the latest release.
Within the release, you will find a README.md
pertaining to the specific version you've downloaded,
based on this template. Those instructions cover the basics
to compile an executable that can run quantum programs on Rigetti QPUs or the QVM.
graph TD
A[Take QIR Input 'input.bc'] --> B[Determine format: 'shot-count' OR 'unitary'] --> C[Choose Output Type]
C -->|Binary Executable?| D[transform]
C -->|Quil?| E[transpile-to-quil]
D -->|generates: Rigetti-targeted 'output.bc'| F[clang w/ QCS SDK + libhelper] -->G[./program]
E -->|generates: shot-count, Quil program string, output recording map| H[submit job to QCS API]
In order to build this crate, a supported LLVM version must be installed and available on your PATH
(you must be able to run llvm-config
). The supported versions are listed in Cargo.toml
under
[features]
.
To build the CLI:
cargo build --bin qcs-sdk-qir --features llvm13-0
To run the unit and snapshot tests:
cargo test --features llvm13-0
To test your changes alongside the shared libraries, it might be helpful to reuse the release
scripts and test a fully integrated toolset. To do so, it's recommended to use cargo-make
. Install
it by running:
cargo install cargo-make
Use the following task and configurations to build the SDK with your changes:
cargo make release-quick # skips the Rust unit and snapshot tests & and other checks
After this, you can skip building various components if a new build is not necessary:
# any or all of the following env vars can be used:
NO_C_SDK_BUILD=1 NO_QIR_SDK_BUILD=1 NO_HELPER_LIB_BUILD=1 cargo make release-quick
# NO_C_SDK_BUILD: skips the git clone & build of the `qcs-sdk-c` library (default: 0)
# NO_QIR_SDK_BUILD: skips the build of this repo (default: 0)
# NO_HELPER_LIB_BUILD: skips the build of the helper lib (default: 0)
To reset your environment and clean up release build artifacts, run:
cargo make release-clean
Refer to the included README.md
in your own local release build, which will help you test your
changes if you are looking to transform and compile QIR programs.
For a full end-to-end integration test against both qvm
and QPUs, you can use:
cargo make release-test-e2e <TARGET> <PROGRAM>
# TARGET = one of: qvm, Aspen-11
# PROGRAM = path to an LLVM bitcode (.bc) file to transform and compile into executable format
cargo make release-test-e2e
# defaults to:
# cargo make release-test-e2e qvm tests/fixtures/programs/reduction.bc
# use any of {qvm,Aspen-11}, assuming you can access the QPUs.
# using the default test program
cargo make release-test-e2e Aspen-11
# using a different test program (note: target must be provided, args are positional)
cargo make release-test-e2e Aspen-11 ./input.bc
Given an input QIR program that might look like this:
%Qubit = type opaque
%Result = type opaque
declare void @__quantum__qis__h__body(%Qubit*) local_unnamed_addr
declare void @__quantum__qis__mz__body(%Qubit*, %Result*) local_unnamed_addr
declare i1 @__quantum__qis__read_result__body(%Result*) local_unnamed_addr
; simple function which measures a single qubit and that's it.
define internal fastcc void @QuantumApplication__Run__body() unnamed_addr {
entry:
br label %body
body:
; shot count variable
%0 = phi i64 [ %2, %body ], [ 1, %entry ]
; measure a given qubit index
tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* null)
%1 = tail call i1 @__quantum__qis__read_result__body(%Result* null)
; shot count branch
%2 = add nuw nsw i64 %0, 1
%3 = icmp ult i64 %0, 42
br i1 %3, label %body, label %exit
exit:
ret void
}
This library will:
- Read and parse the LLVM bitcode
- Recurse through all functions called from the entrypoint. Within each function body:
- Identify a shot-count loop with quantum instructions within any basic blocks traversed
- Transform those basic blocks to instead send an equivalent Quil program for execution using the QCS C SDK.
- Output the resulting LLVM IR, which - if the input program matched the assumptions made by this library - no longer contains calls to QIR intrinsics and thus may be compiled for any supported architecture using QIR-unaware compilers such as
gcc
andclang
.
After this process is complete, the above snippet might look like this (once disassembled):
; ModuleID = 'program.bc'
source_filename = "./test/fixtures/programs/measure.ll"
%Qubit = type opaque
%Result = type opaque
%Executable = type opaque
%ExecutionResult = type opaque
@parameter_memory_region_name = private unnamed_addr constant [12 x i8] c"__qir_param\00", align 1
@quantum_processor_id = private unnamed_addr constant [9 x i8] c"Aspen-10\00", align 1
@quil_program = private unnamed_addr constant [35 x i8] c"DECLARE ro BIT[0]\0AMEASURE 1 ro[0]\0A\00", align 1
declare void @__quantum__qis__h__body(%Qubit*) local_unnamed_addr
declare void @__quantum__qis__mz__body(%Qubit*, %Result*) local_unnamed_addr
declare i1 @__quantum__qis__read_result__body(%Result*) local_unnamed_addr
define internal fastcc void @QuantumApplication__Run__body() unnamed_addr {
entry:
br label %body_execution
body: ; preds = %body_execution, %body
%0 = phi i64 [ %2, %body ], [ 1, %body_execution ]
%1 = call i1 @get_readout_bit(%ExecutionResult* %5, i64 %0, i64 0)
%2 = add nuw nsw i64 %0, 1
%3 = icmp ult i64 %0, 42
br i1 %3, label %body, label %exit
body_execution: ; preds = %entry
%4 = call %Executable* @executable_from_quil(i8* getelementptr inbounds ([35 x i8], [35 x i8]* @quil_program, i32 0, i32 0))
call void @wrap_in_shots(%Executable* %4, i32 42)
%5 = call %ExecutionResult* @execute_on_qpu(%Executable* %4, i8* getelementptr inbounds ([9 x i8], [9 x i8]* @quantum_processor_id, i32 0, i32 0))
call void @panic_on_failure(%ExecutionResult* %5)
br label %body
exit: ; preds = %body
ret void
}
declare %Executable* @executable_from_quil(i8*)
declare %ExecutionResult* @execute_on_qpu(%Executable*, i8*)
declare i1 @get_readout_bit(%ExecutionResult*, i64, i64)
declare void @panic_on_failure(%ExecutionResult*)
declare void @set_param(%Executable*, i8*, i32, double)
declare void @wrap_in_shots(%Executable*, i32)
define i32 @main() {
entry:
call void @QuantumApplication__Run__body()
ret i32 0
}
Read the following for a more detailed run-down on usage / development:
Extra Dependencies for Full QIR Conversion
These are not required if only transforming unitary QIR to Quil.
- A C compiler, such as
gcc
orclang
, which supports LLVM 11 at a minimum. For OSX users, this means XCode version >= 12.5. - The QCS SDK shared library, which may be built or downloaded as described here.
You'll also need to compile the shared "helper" library contained in the helper
directory. This
small shared library is used to reduce the complexity required within this crate's LLVM
transformations.
cd helper
./build.sh
Once that's compiled, make sure to set the relevant environment variables to point to it, within the terminal where you'll be transpiling and running your QIR programs:
# Linux
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/qcs-sdk-qir/helper
# OSX
export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:/path/to/qcs-sdk-qir/helper
# Windows? (To be verified)
export PATH=$PATH:/path/to/qcs-sdk-qir/helper
To transpile an input QIR program, run the CLI. Note that you must specify your LLVM version corresponding with your installed version or this will fail with dozens of errors:
cargo run --features llvm13-0 transform path/to/input.bc path/to/output.bc --add-main-entrypoint
Use the --help
flag to view all options, such as whether to target the QVM or a QPU:
cargo run --features llvm13-0 transform --help
QIR may only be transpiled in this way if:
- An entrypoint function exists (TBD)
- Every function within the call tree from the entrypoint:
- Contains at least one basic block
- All basic blocks within transpiled functions which contain at least one QIR intrinsic must satisfy the basic block preconditions:
- Begin with a
phi
instruction; the operand specifying the current block must reference a variable which we will call the shot count variable; the other operand must reference a constant value of 1. This initializes the shot count variable. - At least one QIR intrinsic, and any number of classical instructions. The return value of any of these classical instructions may not be used as an operand to any QIR intrinsic invocation.
- A shot count termination sequence as the final three instructions of the block:
- An increment:
add
of constant1
to the shot count variable. - A comparison between the shot count variable and a constant, which we will refer to as the shot count.
- A conditional branch instruction, which targets the basic block if the comparison fails and a different block if it succeeds.
- An increment:
- Begin with a
- Basic blocks which do not match the pattern described here are passed through unchanged. If that leaves QIR intrinsics unaltered, then further compilation by a classical toolchain (ie
gcc
) will likely fail.
Now that you've got an output .bc
file, you can inspect it by disassembling it (optional): llvm-dis output.bc
, assuming that's what you named your output file in the CLI command above.
Next, compile it by linking to the two shared libraries in use: the helper library (here in this repo) and the QCS SDK (installed as part of Setup). Here's an example using gcc
:
gcc -Lpath/to/qcs-sdk-c-shared-lib -lqcs -L./helper -lhelper output.bc -o program
That produces an executable program
which you can then run to execute your program on QCS. Happy computing!
To transpile an input QIR program to Quil, run the CLI as shown here, following the LLVM-related instructions above. Note that this command only works for "simple" QIR modules which satisfy the following:
- All quantum instructions are contained within a single basic block, labeled
body
, within the entrypoint function. - That function itself makes no function calls within the
body
block. - The
body
basic block satisfies the basic block preconditions described above in QIR Preconditions.
cargo run --features llvm13-0 transpile-to-quil path/to/input.bc
This will write the Quil program and shot count to stdout
.
Run the CLI with RUST_LOG
set in order to view logs emitted during the transformation process. Values include the verbose debug
as well as info
, warning
, and error
.
Example:
Compiling llvm-sys v110.0.2
error: No suitable version of LLVM was found system-wide or pointed
to by LLVM_SYS_110_PREFIX.
Consider using `llvmenv` to compile an appropriate copy of LLVM, and
refer to the llvm-sys documentation for more information.
llvm-sys: https://crates.io/crates/llvm-sys
llvmenv: https://crates.io/crates/llvmenv
--> .../.cargo/registry/src/github.com-1ecc6299db9ec823/llvm-sys-110.0.2/src/lib.rs:486:1
|
486 | / std::compile_error!(concat!(
487 | | "No suitable version of LLVM was found system-wide or pointed
488 | | to by LLVM_SYS_",
489 | | env!("CARGO_PKG_VERSION_MAJOR"),
... |
496 | | llvmenv: https://crates.io/crates/llvmenv"
497 | | ));
| |___^
First, make sure you do in fact have the same LLVM version installed and on your PATH
as you've
specified with the --features
option in cargo
. Run llvm-config --version
to confirm. If not,
that needs fixing first.
If you do, perhaps you first tried to build the crate before LLVM was installed and configured. Run
cargo clean -p llvm-sys
to clear the build and then retry.
You may also want to try setting the LLVM_SYS_<version>_PREFIX
environment variable to point to
the LLVM installation you want to use. For example, if you've installed LLVM 13 via Homebrew on
macOS, try export LLVM_SYS_130_PREFIX=/usr/local/opt/llvm
.
$ gcc -L../qcs-sdk-c -lqcs -L./helper -lhelper program.bc -o program
ld: library not found for -lhelper
Did you build the helper library in ./helper
? See Setup.
On OSX, it might look like this:
$ ./program
dyld: Library not loaded: libhelper.dylib
Referenced from: .../qcs-qir-sdk/./program
Reason: image not found
The fix: set your LD_LIBRARY_PATH
/ DYLD_LIBRARY_PATH
/ PATH
to include the relevant directory as described above in Setup.
Make sure you're running the dependencies specified by the QCS SDK, namely quilc
.