Simulating Q# programs with QIR runner

ยท 1348 words ยท 7 minutes to read

I recently blogged about the rather unfortunate series of steps that are needed to make the Q# simulator work on arm64 Mac computers, since that platform is sadly not supported out of the box.

In today’s post we are going to kill two birds with one stone - we will make local simulation of Q# programs on arm64 MacOS much easier and we will additionally see how we can simulate Q# programs that happen to be compiled to QIR.

Compiling to QIR ๐Ÿ”—

Q# programs are normally run locally using the default QDK command:

dotnet run

They are then compiled to a .NET based representation and invoked on the full state simulator. This is equivalent to calling:

dotnet run -s QuantumSimulator

That simulator is written in C++ and unfortunately it is incompatible with arm64 Macs. One could say that it is double-incompatible: not only there is no arm64 build of it, but it even fails under Rosetta 2 x64 emulation.

Thankfully, there exists an alternative way of compiling Q# programs - namely to the Quantum Intermediate Representation, QIR. This can be done by using the following command when building the program:

dotnet build <path to Q# program> /p:QirGeneration=true /p:CSharpGeneration=false

This is a low invasive way, which would still allow dotnet run to work with the default full state simulator as before. An alternative way is to update the Q# project file to include these properties explicitly:

<Project Sdk="Microsoft.Quantum.Sdk/0.27.238334">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <QirGeneration>true</QirGeneration>
    <CSharpGeneration>false</CSharpGeneration>
  </PropertyGroup>

</Project>

which in turn allows using the shorthand version of the build command:

dotnet build

This, however, disables the full state simulator, as trying to invoke dotnet run for such Q# project would produce the warning that QIR code cannot be currently executed by the QDK:

Full support for executing projects compiled to QIR is not yet integrated into the Sdk. For more information about the feature, see https://github.com/microsoft/qsharp-compiler/tree/main/src/QsCompiler/QirGeneration. The generated QIR can be executed by manually linking the QIR runtime. See https://github.com/microsoft/qsharp-runtime/tree/main/src/Qir/Runtime for further instructions.

In either case, the program is now compiled to QIR format and dropped into the /qir folder under the current location of the program. The entire program would normally be available in a single intermediate file with the .ll extension, and, naturally, its structure would conform to the QIR spec. The name of the file matches the Q# project file name.

For example, a simple Q# program generating a random bit:

namespace Demos {

    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Measurement;

    @EntryPoint()
    operation Run() : Result {
        use qubit = Qubit();
        H(qubit);
        return M(qubit);
    }
}

produces the following QIR:


%Result = type opaque
%Qubit = type opaque
%Array = type opaque
%String = type opaque

define internal %Result* @Demos__Run__body() {
entry:
  %qubit = call %Qubit* @__quantum__rt__qubit_allocate()
  call void @__quantum__qis__h__body(%Qubit* %qubit)
  %0 = call %Result* @Microsoft__Quantum__Intrinsic__M__body(%Qubit* %qubit)
  call void @__quantum__rt__qubit_release(%Qubit* %qubit)
  ret %Result* %0
}

declare %Qubit* @__quantum__rt__qubit_allocate()

declare %Array* @__quantum__rt__qubit_allocate_array(i64)

declare void @__quantum__rt__qubit_release(%Qubit*)

declare void @__quantum__qis__h__body(%Qubit*)

define internal %Result* @Microsoft__Quantum__Intrinsic__M__body(%Qubit* %qubit) {
entry:
  %bases = call %Array* @__quantum__rt__array_create_1d(i32 1, i64 1)
  %0 = call i8* @__quantum__rt__array_get_element_ptr_1d(%Array* %bases, i64 0)
  %1 = bitcast i8* %0 to i2*
  store i2 -2, i2* %1, align 1
  call void @__quantum__rt__array_update_alias_count(%Array* %bases, i32 1)
  %qubits = call %Array* @__quantum__rt__array_create_1d(i32 8, i64 1)
  %2 = call i8* @__quantum__rt__array_get_element_ptr_1d(%Array* %qubits, i64 0)
  %3 = bitcast i8* %2 to %Qubit**
  store %Qubit* %qubit, %Qubit** %3, align 8
  call void @__quantum__rt__array_update_alias_count(%Array* %qubits, i32 1)
  %4 = call %Result* @__quantum__qis__measure__body(%Array* %bases, %Array* %qubits)
  call void @__quantum__rt__array_update_alias_count(%Array* %bases, i32 -1)
  call void @__quantum__rt__array_update_alias_count(%Array* %qubits, i32 -1)
  call void @__quantum__rt__array_update_reference_count(%Array* %bases, i32 -1)
  call void @__quantum__rt__array_update_reference_count(%Array* %qubits, i32 -1)
  ret %Result* %4
}

define internal void @Microsoft__Quantum__Intrinsic__H__body(%Qubit* %qubit) {
entry:
  call void @__quantum__qis__h__body(%Qubit* %qubit)
  ret void
}

define internal void @Microsoft__Quantum__Intrinsic__H__adj(%Qubit* %qubit) {
entry:
  call void @__quantum__qis__h__body(%Qubit* %qubit)
  ret void
}

define internal void @Microsoft__Quantum__Intrinsic__H__ctl(%Array* %__controlQubits__, %Qubit* %qubit) {
entry:
  call void @__quantum__rt__array_update_alias_count(%Array* %__controlQubits__, i32 1)
  call void @__quantum__qis__h__ctl(%Array* %__controlQubits__, %Qubit* %qubit)
  call void @__quantum__rt__array_update_alias_count(%Array* %__controlQubits__, i32 -1)
  ret void
}

declare void @__quantum__rt__array_update_alias_count(%Array*, i32)

declare void @__quantum__qis__h__ctl(%Array*, %Qubit*)

define internal void @Microsoft__Quantum__Intrinsic__H__ctladj(%Array* %__controlQubits__, %Qubit* %qubit) {
entry:
  call void @__quantum__rt__array_update_alias_count(%Array* %__controlQubits__, i32 1)
  call void @__quantum__qis__h__ctl(%Array* %__controlQubits__, %Qubit* %qubit)
  call void @__quantum__rt__array_update_alias_count(%Array* %__controlQubits__, i32 -1)
  ret void
}

declare %Array* @__quantum__rt__array_create_1d(i32, i64)

declare i8* @__quantum__rt__array_get_element_ptr_1d(%Array*, i64)

declare %Result* @__quantum__qis__measure__body(%Array*, %Array*)

declare void @__quantum__rt__array_update_reference_count(%Array*, i32)

define internal %Result* @Microsoft__Quantum__Intrinsic__Measure__body(%Array* %bases, %Array* %qubits) {
entry:
  call void @__quantum__rt__array_update_alias_count(%Array* %bases, i32 1)
  call void @__quantum__rt__array_update_alias_count(%Array* %qubits, i32 1)
  %0 = call %Result* @__quantum__qis__measure__body(%Array* %bases, %Array* %qubits)
  call void @__quantum__rt__array_update_alias_count(%Array* %bases, i32 -1)
  call void @__quantum__rt__array_update_alias_count(%Array* %qubits, i32 -1)
  ret %Result* %0
}

define i8 @Demos__Run__Interop() #0 {
entry:
  %0 = call %Result* @Demos__Run__body()
  %1 = call %Result* @__quantum__rt__result_get_zero()
  %2 = call i1 @__quantum__rt__result_equal(%Result* %0, %Result* %1)
  %3 = select i1 %2, i8 0, i8 -1
  call void @__quantum__rt__result_update_reference_count(%Result* %0, i32 -1)
  ret i8 %3
}

declare %Result* @__quantum__rt__result_get_zero()

declare i1 @__quantum__rt__result_equal(%Result*, %Result*)

declare void @__quantum__rt__result_update_reference_count(%Result*, i32)

define void @Demos__Run() #1 {
entry:
  %0 = call %Result* @Demos__Run__body()
  %1 = call %String* @__quantum__rt__result_to_string(%Result* %0)
  call void @__quantum__rt__message(%String* %1)
  call void @__quantum__rt__result_update_reference_count(%Result* %0, i32 -1)
  call void @__quantum__rt__string_update_reference_count(%String* %1, i32 -1)
  ret void
}

declare void @__quantum__rt__message(%String*)

declare %String* @__quantum__rt__result_to_string(%Result*)

declare void @__quantum__rt__string_update_reference_count(%String*, i32)

attributes #0 = { "InteropFriendly" }
attributes #1 = { "EntryPoint" }

Running the QIR ๐Ÿ”—

And this is where we come to the core of today’s post. Even though it has been possible to compile the program to QIR this way for a while now, it has not been easy to simulate such QIR output locally - QIR was primarily useful for integrating LLVM-based toolchain for further conversion and integration of the Q# program into relevant hardware instructions sets, but there was no local QIR simulator.

Since recently, however, there is an official QIR Runner, developed under the auspices of the QIR Alliance, in their Github organization. It consists of the core library (QIR standard lib), the backend and the command line runner, all written in Rust. They have not - to my knowledge - been heavily advertised, but there is already some basic auto-generated documentation available on the official QIR Alliance website.

Since the QIR runner has not been “officially” released, the Github project also lacks any published releases, making the download of the tool rather cumbersome. However, the project’s CI/CD pipeline provides ready-made, usable binaries. Those can be accessed by simply going to Github Actions of the project, then picking the latest build of the main branch (at the time of writing it’s this one) and then scrolling all the way down to the published artifacts. There one can find downloadable runners for Windows, Linux and MacOS, though only for x64 architecture. From what I have verified, however, the MacOS one works fine on arm64 processors too - under Rosetta 2 emulation. While everything is written in Rust, it would be great to have arm64 builds available too, but the challenge there are the LLVM bits whose support for arm64 is - to my understanding - not straight forward. For the same reason I did not succeed in building the actual QIR runner on my arm64 Mac - though the core (standard) library and the backend build just fine, and can be incorporated into any project.

Once downloaded, it is possible to run the Q# program compiled to QIR by simply invoking:

qir-runner my-project.ll

This should simulate your program using the QIR runner.

Conclusion ๐Ÿ”—

That’s about it - it is enough to just add the qir-runner to your path, and then you can use it simulate any Q# program you compile to QIR. This is a very exciting alternative, because it works on arm64 Macs (albeit under emulation), it is much faster then the default full state simulator (because it is written in Rust) and it allows us to work with the QIR intermediate code, which is how all quantum vendors are going to be processing our code as well.

There are currently some limitation compared to the default simulator, primarily related to how arguments can be passed into Q# program (they can’t in QIR programs), but overall I have had a ton of success with running my programs with qir-runner.

About


Hi! I'm Filip W., a cloud architect from Zรผrich ๐Ÿ‡จ๐Ÿ‡ญ. I like Toronto Maple Leafs ๐Ÿ‡จ๐Ÿ‡ฆ, Rancid and quantum computing. Oh, and I love the Lowlands ๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP