QPU vs. Simulator (QVM)
Last updated
Last updated
The process of developing quantum software often begins with testing against a QVM. Eventually, you'll come to a point where you're ready to run your programs against a real Quantum Processor Unit (QPU).
This guide will focus specifically on the differences between QPUs and QVMs when targeting them via the Quil SDK.
While QVMs typically run in your current environment and therefore don't have special access requirements, QPUs are remote resources hosted by QCS. As such, a QPU will only be accessible to you during a reservation window. You must also have network access to the QPU, which is most easily obtained by using your provisioned JupyterLab IDE.
When moving from a QVM to a QPU, it's important to consider qubit topology. Qubit topology describes which qubits can be used together in quantum operations. For a QVM, this is a fully-connected graph — meaning any qubit has an edge to all other qubits. This is not necessarily the case for a QPU, where only select edges are present.
You can inspect a QPU's topology when making a QPU reservation on the QCS website, as shown below.
On a QPU, qubits and qubit pairs also have differing fidelities for certain operations. You can view this information by clicking Device Calibration
under a QPU when making a reservation, as shown above.
Assume your quantum program includes an operation on qubits 0 and 1. On a QVM, your program may run well, but what about on a particular QPU? What if these qubits are not connected in the desired QPU's topology? Or perhaps they are connected but have poor fidelity for the operation in question. To run your program on the desired QPU, you can take advantage of compiler rewiring.
Rewiring is a step that the Quil compiler will perform to select optimal qubits for you. The qubits selected will be based on a QPU's topology and calibration information. Rewiring will happen by default, if needed, but rewiring behavior can also be explicitly overridden using a compiler directive.
To learn more about rewiring, including how to override default behavior, see the pyQuil documentation on rewiring.
If you'd like to simulate the topology and fidelities of a real QPU, you can create a QVM based on a QPU using the as_qvm
parameter of get_qc()
:
Note: This does not simulate the noise model of the QPU — only the qubit topology and operator fidelities.
The QVM and a real QPU return the data generated by a program quite differently. When possible, our SDKs attempt to abstract away these differences by fitting the data into a convenient shot_count * region_length
matrix for each memory region. However, in some cases, data returned by the QPU won't fit this shape. It's important to know the differences between the two platforms so the readout data can be wrangled into a form that is most useful for you.
First, to clarify some terminology:
Memory Region: an entire, strongly-typed, array-like region of memory allocated by a Quil program, such as ro
or theta
Memory Reference: a single element/offset/location within a Memory Region, such as ro[2]
Readout Data: data collected from analog readout performed by the control system, generally following post-processing (such as classification)
Memory: here, used to refer to classical memory, and specifically the contents of the Memory Regions allocated by the Quil program
The QVM uses a memory model. You declare memory regions in a program and the final state of those memory regions are returned at the end of every shot. If you execute a program with a DECLARE ro BIT[2]
instruction and run it for 4 shots, you'll get back a 4x2 matrix for the memory region ro
. For this reason, data returned from the QVM naturally fits into a shot_count x region_length
matrix.
Programs executed on a QPU emit readout data to declared memory regions more like streams. While the QVM can only hold one value per memory reference and thus returns the last one in each shot, a QPU will return all values written to each reference in each shot. Because of this, it can't be assumed that a program will always result in shot_count * region_length
matrices for every memory region. For example, this situation can occur in programs that re-use qubits or use dynamic control flow, where the program can emit a different amount of values per shot. In these cases, it's best for the author of the program to construct the matrix they need from the raw readout data.
Readout data is derived from analog measurements performed by the QPUs control system that has usually gone through some post processing (for example, to classify a qubit measurement). The specifics of how high level gates like MEASURE
run on a QPU are defined by low-level Quil-T calibrations. Understanding a bit about how a typical MEASURE
instruction is calibrated will help us understand how raw QPU readout data is structured.
Consider the following calibration definition:
The first line of a calibration determines what instructions the body of the calibration will be used for. In this case, this calibration will match any MEASURE 0 addr
instruction in a program, where addr
can be any valid memory reference (i.e. ro[0]
) and can be used as a parameter inside of the calibration body. In the calibration body, we see that q0_unclassified
is declared as 2 real numbers (so that it can hold a single complex number). The measure operation is performed by applying a PULSE
to the "ro_tx"
frame and then performing a corresponding CAPTURE
from the "ro_rx"
frame that writes the complex result to q0_unclassified
. The PRAGMA FILTER-NODE
statements that follow all perform some type of post-processing. In this case, the complex result in q0_unclassified
gets classified as a qubit measurement, 0 or 1 . The PRAGMA LOAD-MEMORY
statements emit the data as readout values. In this calibration both q0_unclassified
and q0
are written as readout values. Notice that q0
gets written to "addr"
, the memory reference in the instruction that the calibration matched.
With that context, lets finally take a look at raw QPU readout data. It's composed of two parts:
mappings
is determined during compilation, and maps the memory reference used in an instruction (i.e. ro[0]
) to the identifier used for readout inside the matching calibration for the instruction (i.e. q0
).
readout_values
maps the identifier used in a calibration (i.e. q0
) to a list of the readout values written to that identifier across all shots.
You can use a memory reference as a key to mappings
to determine the key to use in readout_values
to get all the values associated with a memory reference in your program.
Note that readout_values
will contain any value written to by a PRAGMA LOAD-MEMORY
statement in a calibration. For example, in the above calibration, PRAGMA LOAD-MEMORY q0_unclassified
means the complex measurements of qubit 0 will appear in readout_values
under the key q0_unclassified
.
Active reset — enabled by using the Quil RESET
instruction at the beginning of a program — forces all qubits to their ground states using a series of measurement and conditional feedback operations. This is useful when running sequential programs against a QPU, as it reduces the time needed for qubits to return to their ground states between programs.
Active reset has no effect when running programs against a QVM, as a QVM will start each shot with its qubits in their ground states regardless of active reset.
It may seem theoretically trivial to use active reset when targeting a QPU. However, its use can have side effects in practice, as active reset has its own associated fidelity (as seen in the figure under QPU Topology & Fidelities) — it's important to be aware of this when moving from a QVM to a QPU.