Pipelines

The primary way to access QAT’s compilation and execution capabilities is through its pipelines API. Pipelines allow us to define the compilation and execution process in a configurable and modular way.

Using QAT pipelines

QAT has a number of pipelines that are pre-defined and ready to use. We start by creating a QAT object.

from qat import QAT
core = QAT()

We can now add a pipeline to it. Let’s add a pipeline that uses the EchoEngine:

from qat.pipelines.waveform import echo8
core.pipelines.add(echo8, default=True)

This will add the “echo8” pipeline to core, which can be used to compile and execute programs using a simulator that simply returns all readouts as zeroes. The “8” in “echo8” specifies that the simulator has 8 qubits available. The pipeline already has its compilation modules chosen so that it is compatible with the echo engine: we’ll cover this in more detail in Compilation. Now we can use this to execute a QASM program.

 1from qat import QAT
 2from qat.pipelines.waveform import echo8
 3from compiler_config.config import CompilerConfig, QuantumResultsFormat
 4
 5core = QAT()
 6core.pipelines.add(echo8, default=True)
 7
 8qasm_str = """
 9OPENQASM 2.0;
10include "qelib1.inc";
11qreg q[2];
12creg c[2];
13h q[0];
14cx q[0], q[1];
15measure q -> c;
16"""
17
18config = CompilerConfig(results_format=QuantumResultsFormat().binary_count())
19results, metrics = core.run(qasm_str, config, pipeline="echo8")

Note that metrics is returned. This is an object that contains various metrics regarding compilation, such as circuits after they have been optimized, or the total number of pulse-level instructions. Since the echo pipeline just returns zeros for readouts, the result returned here is results = {'c': {'00': 1000}}.

We could also achieve the same workflow by compiling and executing separately:

pkg, metrics = core.compile(qasm_str, config, pipeline="echo8")
results, metrics = core.execute(pkg, config, pipeline="echo8")

The package pkg contains the native instructions to be sent to the target (in this case, the echo simulator).

Updateable Pipelines

The pipeline from the previous example was a default pipeline instance that you can import from QAT. But more likely, you would want to configure your own pipeline using your own hardware model and target data for a given type of hardware. Updateable pipelines provide a prescribed way to create pipelines that offer utility to rebuild pipelines using new hardware models or target data. Let’s demonstrate with the Waveform example and the echo engine from the previous section, but by configuring our own.

1from qat.pipelines.waveform import EchoPipeline, PipelineConfig
2from qat.model.loaders.lucy import LucyModelLoader
3
4loader = LucyModelLoader(qubit_count=8)
5config = PipelineConfig(name="echo_pipeline")
6pipeline = EchoPipeline(loader=loader, config=config)
7pipeline.update(reload_model=True)

There’s a few things to notice here. First, the updateable pipeline actually takes ownership of the pipeline instance it creates, and in the way, can be used in-place of the pipeline instance. Secondly, we instantiated it using the model loader, allowing us to reload the model directly from the loader. However, updateable pipelines can be configured using a hardware model directly, and similarly, they can be updated by providing a new model directly. The target data can also be provided at instantiation, and updated to using pipeline.update(target_data=target_data). Finally, each updateable pipeline is paired with a PipelineConfig object that stores configuration data for the pipeline, such as its name, and additional compiler settings.

Compile and Execute (updateable) pipelines

The example seen previously uses a “full pipeline” that is capable of both compiling and executing a program. However, we can also express pipelines that can only compile CompilePipeline or only execute ExecutePipeline. The benefits of this are that

  • We can clearly separate out the compilation and execution steps over distributed systems.

  • We can mix-and-match compilation and execution pipelines. For example, we could compile a program for a specific hardware target, but execute it on a simulator. On the contrary, we could also define multiple compile pipelines that expose different compiler features, but execute them all on the same hardware target.

We can compile and execute against particular pipelines by using QAT.compile and QAT.execute, specifying the pipeline to use.

 1from qat.pipelines.waveform import WaveformCompilePipeline, EchoExecutePipeline, PipelineConfig
 2from qat.model.loaders.lucy import LucyModelLoader
 3from qat import QAT
 4
 5# Define pipelines
 6model = LucyModelLoader(qubit_count=16).load()
 7compile_pipeline = WaveformCompilePipeline(config=PipelineConfig(name="compile"), model=model)
 8execute_pipeline = EchoExecutePipeline(config=PipelineConfig(name="execute"), model=model)
 9
10# Register pipelines
11core = QAT()
12core.pipelines.add(compile_pipeline, default=True)
13core.pipelines.add(execute_pipeline, default=True)
14
15# Execute against pipelines
16qasm_str = """
17OPENQASM 2.0;
18include "qelib1.inc";
19qreg q[2];
20creg c[2];
21h q[0];
22cx q[0], q[1];
23measure q -> c;
24"""
25executable, compile_metrics = core.compile(qasm_str, pipeline="compile")
26results, execute_metrics = core.execute(executable, pipeline="execute")

Default pipelines that are available in QAT

There are a number of pipelines in QAT that are available to use off-the-shelf.

There are also pipelines that use legacy hardware and engines, but wrapped in the new pipeline API:

  • qat.pipelines.legacy.echo: Pipelines that execute using the legacy EchoEngine. The pipelines available by default are legacy_echo8, legacy_echo16, lgeacy_echo32. For a custom amount of qubits, the method get_pipeline can be used.

  • qat.pipelines.legacy.rtcs: Pipelines that execute using the legacy RealtimeChipSimEngine. The only available pipeline is for two qubits, legacy_rtcs2.

  • qat.pipelines.legacy.qiskit: Pipelines that execute using the legacy QiskitEngine. The available pipelines are legacy_qiskit8, legacy_qiskit16 and legacy_qiskit32. For a custom amount of qubits, the method get_pipeline can be used.

Defining custom pipelines

Pipelines in QAT are highly customisable to allow for diverse compilation behaviour for a range of targets, such as live hardware or custom simulators. Compilation is broken down into three parts: the frontend, the middleend and the backend. We will not go into the details of each module here, but they will be covered in Compilation. Similarly, the execution part of the pipeline is defined by two objects: the engine and the runtime. The engine acts as an adapter to the target, and deals with communicating the instructions and results from the runtime to the target. The runtime handles the engine, and deals with software post-processing of the results. See Execution for more details.

See ../notebooks/tutorials/custom_pipeline for a working example of defining a custom pipeline and updateable pipeline.