Catalyst Command Line Interface

Catalyst includes a standalone command-line tool, catalyst, that compiles quantum programs written in our MLIR dialects into an object file, independent of the Catalyst Python frontend.

This compiler tool combines four stages of compilation:

  1. quantum-opt: Performs the MLIR-level optimizations, including quantum optimizations, and translates the input dialect to the LLVM dialect.

  2. mlir-translate: Translates the input LLVM dialect into LLVM IR.

  3. llvm-opt: Performs lower-level optimization and compilation on the LLVM IR needed for certain features.

  4. llc: Runs LLVM backend code generation and creates the object file.

The catalyst tool will run all stages under the hood by default, but it also has the ability to run each stage individually. For example:

# Run MLIR passes, returning LLVM-dialect MLIR
catalyst --tool=opt input.mlir -o llvm-dialect.mlir

# Translate LLVM-dialect MLIR to LLVM IR
catalyst --tool=translate llvm-dialect.mlir -o llvm-ir.ll

# Run Catalyst's LLVM transformations
catalyst --tool=llvm-opt llvm-ir.ll -o optimized.ll

# Generate object.o from backend-ready LLVM IR
catalyst --tool=llc optimized.ll -o object.o

Stages can be combined for a partial or full execution of the pipeline:

# Run MLIR passes and translation in one invocation, without LLVM transformations
catalyst --tool=opt-translate input.mlir -o llvm-ir.ll

# Run through LLVM transformations in one invocation, without object generation
catalyst --tool=opt-translate-llvm input.mlir -o optimized.ll

# Full compilation from mlir to object code
catalyst input.mlir

Note

If Catalyst is built from source, the catalyst executable will be located in the mlir/build/bin/ directory relative to the root of your Catalyst source directory.

If Catalyst is installed via pip or from wheels, the executable will be located in the bin/ directory relative to the environment’s installation directory.

Usage

catalyst [options] <input file>

Calling catalyst without any options runs the full compilation pipeline using all default configurations, and outputs by default an object file named catalyst_module.o. Use -o object.o to choose another output path.

Command line options

The complete list of options for the Catalyst CLI tool can be displayed by running catalyst --help. As this list contains all available options, including those for configuring LLVM, the options most relevant to the usage of the Catalyst CLI tool are covered in more detail below.

--help

Show available command-line options and exit.

--verbose

Emit verbose progress messages to stderr.

-o <filename>

Destination for the selected tool’s final output. opt produces MLIR. translate, llvm-opt, opt-translate, and opt-translate-llvm produce LLVM IR. all and llc produce object code. With --emit-bytecode, opt produces MLIR bytecode.

IR output can also be written to stdout with -o -, which is the default value. However, object code requires a file destination. The default when -o is omitted in that case is <module-name>.o in the current directory, using catalyst_module as the default module name.

--tool=<opt|translate|llvm-opt|llc>

Select the tool to run individually. Each stage requires appropriate IR as input.

  • opt: Run MLIR passes on MLIR input.

  • translate: Translate LLVM-dialect MLIR to LLVM IR.

  • llvm-opt: Run Catalyst’s applicable LLVM transformations on LLVM IR input.

  • llc: Run backend code generation on backend-ready LLVM IR input.

A combination of stages (starting from MLIR) can also be run. all is the default.

  • opt-translate: Run opt and translate on MLIR input.

  • opt-translate-llvm: Run opt, translate, and llvm-opt on MLIR input.

  • all: Run all stages. LLVM IR input is also accepted, skipping the MLIR stages.

--save-ir-after-each=<pass|pipeline|changed>

Keep intermediate files after each pass or after each pipeline in the compilation. By default, no intermediate files are saved.

  • pass: Keep intermediate files after each transformation/optimization pass.

  • pipeline: Keep intermediate files after each pipeline, where a pipeline is a sequence of transformation/optimization passes.

  • changed: Keep intermediate files after each transformation/optimization pass, but only if the IR was modified by that pass.

--keep-intermediate[=<true|false>]

Keep intermediate files after each pipeline in the compilation. By default, no intermediate files are saved. Using --keep-intermediate is equivalent to using --save-ir-after-each=pipeline.

--use-nameloc-as-prefix[=<true|false>]

Print SSA IDs using their name location, if provided, as prefix. By default, name location information is not used. Name location, or named source location, is a type of source location information that allows attaching a name to a child location.

--{passname}

Enable a specific pass. For example, to enable the cancel-inverses pass, use --cancel-inverses.

Catalyst’s main mlir stage is split up into a sequence of pass pipelines that can also be run individually via this option. In that case, the name of the pipeline is substituted for the pass name. Currently, the following pipelines are available: quantum-compilation-stage, hlo-lowering-stage, gradient-lowering-stage, bufferization-stage, llvm-dialect-lowering-stage, and finally default-catalyst-pipeline which encompasses all the above as the default pipeline used by the Catalyst CLI tool if no pass option is specified.

There is also user-transform-stage, a shorter version of quantum-compilation-stage that stops right after apply-transform-sequence, the pass that applies the user transforms. It is useful when only the program after the user transforms is needed, for example for resource analysis. It is not meant to be followed by the remaining stages.

--catalyst-pipeline=<pipeline1(pass1[;pass2[;...]])[,pipeline2(...)]>

Specify the Catalyst compilation pass pipelines.

A pipeline is composed of a semicolon-delimited sequence of one or more transformation or optimization passes. Multiple pass pipelines can be specified and input as a comma-delimited sequence of pipelines.

For example, if we wanted to specify two pass pipelines, pipe1 and pipe2, where pipe1 applies the passes split-multiple-tapes and apply-transform-sequence, and where pipe2 applies the pass inline-nested-module, we would specify this pipeline configuration as:

--catalyst-pipeline="pipe1(split-multiple-tapes;apply-transform-sequence),pipe2(inline-nested-module)"

--workspace=<path>

The workspace directory where intermediate files are saved. The default is the current working directory.

Note that the workspace directory must exist before running catalyst with this option.

--module-name=<name>

The module name used for diagnostics, auxiliary compiler artifacts, and the default object filename <module-name>.o. The default is "catalyst_module". An explicit -o path takes precedence over the default object filename.

--async-qnodes[=<true|false>]

Enable asynchronous QNodes.

--checkpoint-stage=<stage name>

Define a checkpoint stage, used to indicate that the compiler should start only after reaching the given stage. The stages that are currently available are:

  • MLIR: mlir (start with first MLIR stage), {pipeline} such as any of the built-in pipeline names described under the --{passname} option, OR any custom pipeline names if the --catalyst-pipeline={pipeline(...),...} option is used.

  • LLVM: LLVMIRTranslation (start with first LLVM stage), CoroOpt, O2Opt, Enzyme. Note that CoroOpt (Coroutine lowering), O2Opt (O2 optimization), and Enzyme (automatic differentiation) passes are only run conditionally as needed.

--dump-catalyst-pipeline[=<true|false>]

Print (to stderr) the pipeline(s) that will be run.

Examples

To illustrate how to use the Catalyst CLI tool, consider the simple MLIR code, my_circuit.mlir, which defines a function my_circuit that implements a single-qubit quantum circuit that applies the sequence of gates \(R_x(\theta) \to H \to H \to R_x(\theta)\) to the input qubit for some rotation angle \(\theta\):

module {
  func.func @my_circuit(%in_qubit: !quantum.bit, %angle: f64) -> !quantum.bit {
    %0 = quantum.custom "RX"(%angle) %in_qubit : !quantum.bit
    %1 = quantum.custom "Hadamard"() %0 : !quantum.bit
    %2 = quantum.custom "Hadamard"() %1 : !quantum.bit
    %3 = quantum.custom "RX"(%angle) %2 : !quantum.bit
    return %3 : !quantum.bit
  }
}

We’ll use the Catalyst CLI tool to run the quantum-opt compiler to perform the MLIR-level optimizations and translate the input to the LLVM dialect. We’ll define a pass pipeline that applies two quantum-optimization passes:

  1. cancel-inverses, which removes any operations that are applied next to their (self-)inverses or adjoint, in this case the two adjacent Hadamard gates.

  2. merge-rotations, which combines rotation gates of the same type that act sequentially, in this case the two RX gates the become adjacent after the two Hadamard gates have been removed by the cancel-inverses pass.

To apply these two passes to our my_circuit function, we can do so as follows:

pipe(cancel-inverses;merge-rotations)

Finally, we’ll use the option --mlir-print-ir-after-all to print the resulting MLIR after each pass that is applied, and the -o option to set the name of the output IR file:

catalyst my_circuit.mlir \
    --tool=opt \
    --catalyst-pipeline="pipe(cancel-inverses;merge-rotations)" \
    --mlir-print-ir-after-all \
    -o my_circuit-llvm.mlir

Running this command will output the following intermediate IR to the console:

// -----// IR Dump After CancelInversesPass (cancel-inverses) //----- //
module {
  func.func @my_circuit(%arg0: !quantum.bit, %arg1: f64) -> !quantum.bit {
    %out_qubits = quantum.custom "RX"(%arg1) %arg0 : !quantum.bit
    %out_qubits_0 = quantum.custom "RX"(%arg1) %out_qubits : !quantum.bit
    return %out_qubits_0 : !quantum.bit
  }
}


// -----// IR Dump After MergeRotationsPass (merge-rotations) //----- //
module {
  func.func @my_circuit(%arg0: !quantum.bit, %arg1: f64) -> !quantum.bit {
    %0 = arith.addf %arg1, %arg1 : f64
    %out_qubits = quantum.custom "RX"(%0) %arg0 : !quantum.bit
    return %out_qubits : !quantum.bit
  }
}

and produce a new file my_circuit-llvm.mlir containing the resulting module in the LLVM dialect:

module {
  func.func @my_circuit(%arg0: !quantum.bit, %arg1: f64) -> !quantum.bit {
    %0 = arith.addf %arg1, %arg1 : f64
    %out_qubits = quantum.custom "RX"(%0) %arg0 : !quantum.bit
    return %out_qubits : !quantum.bit
  }
}

We can see in the intermediate IR after the cancel-inverses pass that the two adjacent Hadamard gates were removed and that the two RX gates were merged into one after the merge-rotations pass, with the input angle to the single RX gate being the sum of the two input angles to the original two gates. The result in my_circuit-llvm.mlir contains the final, optimized MLIR.

For a list of transformation passes currently available in Catalyst, see the Catalyst’s Transformation Library documentation. The available passes are also listed in the catalyst --help message.

MLIR Plugins

mlir-opt-like tools are able to take plugins as inputs. These plugins are shared objects that include dialects and passes written by third parties. This means that you can write dialects and passes that can be used with catalyst and quantum-opt.

As an example, the LLVM repository includes a very simple plugin. To build it, simply run make plugin and the standalone plugin will be built in the root directory of the Catalyst project.

With this, you can now run your own passes by using the following flags:

catalyst --load-dialect-plugin=$YOUR_PLUGIN --load-pass-plugin=$YOUR_PLUGIN $YOUR_PASS_NAME file.mlir

Concretely for the example plugin, you can use the following command:

catalyst --tool=opt --load-pass-plugin=standalone/build/lib/StandalonePlugin.so --load-dialect-plugin=standalone/build/lib/StandalonePlugin.so --pass-pipeline='builtin.module(standalone-switch-bar-foo)' a.mlir