Skip to content

✨ Add MQTRef-QIR conversions #1091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 122 commits into
base: main
Choose a base branch
from

Conversation

li-mingbao
Copy link
Contributor

Description

This PR introduces conversion passes between the mqtdyn dialect and the LLVM IR that adheres to the QIR specifications for dynamic qubit allocations. Currently it mainly focuses on the conversion within MLIR between the mqtdyn dialect and the llvmlir dialect and vice versa. Once the IR consists only of llvm dialect operations it can be convert to LLVM IR with the built-in passes from the mlir-translate module and back.

Checklist:

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

Li-ming Bao and others added 30 commits June 13, 2025 15:34
@li-mingbao
Copy link
Contributor Author

li-mingbao commented Aug 19, 2025

@burgholzer sorry for pinging you once again.

I incorporated some feedback already to make the conversion more compliant to the base profile of QIR instead of the reference files but I am still working on it. The main changes for now where the following:

  1. Adding support for static addressed qubits
  2. Add the necessary attributes for the main function (mainly the numResults and numQubits)
  3. Proper conversion of negative controlled operations
  4. Adjusting some tests
  5. Creating 4 different blocks for the function (entryblock/2 mainblocks/outputblock) that the base profile requires.

This transforms the following program:

module {
    func.func @bellStateStaticQubit() attributes {passthrough = ["entry_point"]}  {
        %q0 = mqtref.qubit 0
        %q1 = mqtref.qubit 1
        mqtref.h() %q0
        mqtref.x() %q1 ctrl %q0
        %m0 = "mqtref.measure"(%q0) : (!mqtref.Qubit) -> i1
        %m1 = "mqtref.measure"(%q1) : (!mqtref.Qubit) -> i1
        return
    }
}

To this:

module {
  llvm.mlir.global internal constant @mlir.llvm.nameless_global_1("r1\00") {addr_space = 0 : i32, dso_local}
  llvm.mlir.global internal constant @mlir.llvm.nameless_global_0("r0\00") {addr_space = 0 : i32, dso_local}
  llvm.func @bellStateStaticQubit() attributes {passthrough = ["entry_point", ["output_labeling_schema", "schema_id"], ["qir_profiles", "base_profile"], ["required_num_qubits", "2"], ["required_num_results", "2"]]} {
    %0 = llvm.mlir.addressof @mlir.llvm.nameless_global_1 : !llvm.ptr
    %1 = llvm.mlir.addressof @mlir.llvm.nameless_global_0 : !llvm.ptr
    %2 = llvm.mlir.zero : !llvm.ptr
    %3 = llvm.mlir.constant(-1 : i32) : i32
    llvm.call @__quantum__rt__initialize(%2) : (!llvm.ptr) -> ()
    llvm.br ^bb1
  ^bb1:  // pred: ^bb0
    %4 = llvm.mlir.constant(1 : i64) : i64
    %5 = llvm.inttoptr %4 : i64 to !llvm.ptr
    llvm.call @__quantum__qis__h__body(%2) : (!llvm.ptr) -> ()
    llvm.call @__quantum__qis__cx__body(%5, %2) : (!llvm.ptr, !llvm.ptr) -> ()
    llvm.br ^bb2
  ^bb2:  // pred: ^bb1
    %6 = llvm.call @__quantum__qis__m__body(%2) : (!llvm.ptr) -> !llvm.ptr
    llvm.call @__quantum__rt__result_record_output(%6, %1) : (!llvm.ptr, !llvm.ptr) -> ()
    llvm.call @__quantum__rt__result_update_reference_count(%6, %3) : (!llvm.ptr, i32) -> ()
    %7 = llvm.call @__quantum__rt__read_result(%6) : (!llvm.ptr) -> i1
    %8 = llvm.call @__quantum__qis__m__body(%5) : (!llvm.ptr) -> !llvm.ptr
    llvm.call @__quantum__rt__result_record_output(%8, %0) : (!llvm.ptr, !llvm.ptr) -> ()
    llvm.call @__quantum__rt__result_update_reference_count(%8, %3) : (!llvm.ptr, i32) -> ()
    %9 = llvm.call @__quantum__rt__read_result(%8) : (!llvm.ptr) -> i1
    llvm.br ^bb3
  ^bb3:  // pred: ^bb2
    llvm.return
  }
  llvm.func @__quantum__rt__initialize(!llvm.ptr)
  llvm.func @__quantum__qis__h__body(!llvm.ptr)
  llvm.func @__quantum__qis__cx__body(!llvm.ptr, !llvm.ptr)
  llvm.func @__quantum__qis__m__body(!llvm.ptr) -> !llvm.ptr
  llvm.func @__quantum__rt__result_record_output(!llvm.ptr, !llvm.ptr)
  llvm.func @__quantum__rt__result_update_reference_count(!llvm.ptr, i32)
  llvm.func @__quantum__rt__read_result(!llvm.ptr) -> i1
}

And finally once converted to .ll :

source_filename = "LLVMDialectModule"

@mlir.llvm.nameless_global_1 = internal constant [3 x i8] c"r1\00"
@mlir.llvm.nameless_global_0 = internal constant [3 x i8] c"r0\00"

define void @bellStateStaticQubit() #0 {
  call void @__quantum__rt__initialize(ptr null)
  br label %1

1:                                                ; preds = %0
  call void @__quantum__qis__h__body(ptr null)
  call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr null)
  br label %2

2:                                                ; preds = %1
  %3 = call ptr @__quantum__qis__m__body(ptr null)
  call void @__quantum__rt__result_record_output(ptr %3, ptr @mlir.llvm.nameless_global_0)
  call void @__quantum__rt__result_update_reference_count(ptr %3, i32 -1)
  %4 = call i1 @__quantum__rt__read_result(ptr %3)
  %5 = call ptr @__quantum__qis__m__body(ptr inttoptr (i64 1 to ptr))
  call void @__quantum__rt__result_record_output(ptr %5, ptr @mlir.llvm.nameless_global_1)
  call void @__quantum__rt__result_update_reference_count(ptr %5, i32 -1)
  %6 = call i1 @__quantum__rt__read_result(ptr %5)
  br label %7

7:                                                ; preds = %2
  ret void
}
declare void @__quantum__rt__initialize(ptr)
declare void @__quantum__qis__h__body(ptr)
declare void @__quantum__qis__cx__body(ptr, ptr)
declare ptr @__quantum__qis__m__body(ptr)
declare void @__quantum__rt__result_record_output(ptr, ptr)
declare void @__quantum__rt__result_update_reference_count(ptr, i32)
declare i1 @__quantum__rt__read_result(ptr)

attributes #0 = { "entry_point" "output_labeling_schema"="schema_id" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="2" }

!llvm.module.flags = !{!0}

!0 = !{i32 2, !"Debug Info Version", i32 3}

As you can see the mainly the conversion is not entirely correct, mainly because of the awkward measure conversion. I am not exactly sure what the best way to convert it is.
The conversion to __quantum__qis__m__body(ptr) seems more straightforward and similar to the mqtref implementation and the result of the measurement can be obtained by calling the __quantum__rt__read_result(ptr) operation. The problem here is that these operations violate the base profile by providing a return value.
In comparison to that, there is the __quantum__qis__mz__body(%Qubit*, %Result* writeonly)) operation that is more fitting for the base profile. The problem here is that there is not really an equivalent operation in the mqtref dialect more specifically for %Result.
Also, I think for now I will adjust the measure conversion to just the measure operation and ignore the output recording for now. So in the end it is just a 1:1 conversion.

For the gate operations I still have to flip the order of arguments as I saw that the control qubits come first and then the target qubits. But is the order within the control qubits still the same? So

mqtref.x %q0 ctrl %q1, %q2

would be

@__quantum__qis__ccx__body (%q1, %q2, %q0)

Lastly, the base profile requires that some module flags with metadata should be set.
From what I have seen this is not possible in mlir with LLVM 19/20 and is only later supported in LLVM 22. Currently if one converts a .ll file to mlir and back, the flags simply disappear except the debug flag.

@burgholzer
Copy link
Member

sorry for pinging you once again.

No worries. Feel free to ping me anytime.

You are correct, that the base profile mandates the use of __quantum__qis__mz__body(%Qubit*, %Result* writeonly)) because functions may not return a value.
We currently do not have an equivalent of the opaque Result type. Our measurements simply return i1 values. This makes it slightly more awkward to convert to QIR.
@DRovara and I discussed about this for a while and I believe our consensus was that returning i1 allows for more direct optimisations.
maybe this is something we have to rethink again.
I believe the output recording is quite essential to make a "complete" QIR program.
If I remember correctly, the QIR spec says something about the %Result in the base profile and that they can be treated very similar to static qubit indices in the sense that a program annotated with ["required_num_results", "2"] may simply use ptr null and ptr inttoptr (i64 1 to ptr) for addressing the classical results. That could work, couldn't it?

As for the controls and targets: yes, most languages put controls first and targets afterwards. The order of the controls does not make a difference (it's a set conceptually). So you might as well use the same ordering as in the MLIR input.

As for the metadata annotations: that seems unfortunate. Just to be sure: this only affects the way going from QIR to MLIR, right? Can we work around that somehow? Maybe by using some concept that is not mandated by QIR, but that would allow us to perform the back-transformation? What are these flags again? Can you point me towards the respective places in the spec?

@li-mingbao
Copy link
Contributor Author

https://github.com/qir-alliance/qir-spec/blob/main/specification/under_development/profiles/Base_Profile.md#module-flags-metadata Here is the link to the module flag section. From what I have seen, MLIR does not support llvm.module flags until LLVM 22. For now I think the best way is to add this information to the attributes of the entry function.

So would you recommend to convert a mqtref.measure operation to a __quantum__qis__mz__body(%Qubit*, %Result* writeonly)) and a @__quantum__rt__result_record_output(%Result*, i8*) operation that points to a internal constant?

So something like this

%m = %m0 = "mqtref.measure"(%q0) : (!mqtref.Qubit) -> i1

Should look like this once it is converted:

call void @__quantum__qis__mz__body(ptr %qubit null, ptr %result writeonly null)
call void @__quantum__rt__result_record_output(ptr %result null,  ptr @mlir.llvm.nameless_global_0))

where both the %Qubit* and %Result* type are just of llvm.ptr type.

@burgholzer
Copy link
Member

https://github.com/qir-alliance/qir-spec/blob/main/specification/under_development/profiles/Base_Profile.md#module-flags-metadata Here is the link to the module flag section. From what I have seen, MLIR does not support llvm.module flags until LLVM 22. For now I think the best way is to add this information to the attributes of the entry function.

yeah. this seems to have landed in llvm/llvm-project#130679 only in March 2025, which is pretty recent.
Adding it as attributes to the entry function seems reasonable.

So something like this

%m0 = "mqtref.measure"(%q0) : (!mqtref.Qubit) -> i1
Should look like this once it is converted:

call void @__quantum__qis__mz__body(ptr %qubit null, ptr %result writeonly null)
call void @__quantum__rt__result_record_output(ptr %result null, ptr @mlir.llvm.nameless_global_0))

where both the %Qubit* and %Result* type are just of llvm.ptr type.

Yeah. that seems like a reasonable assumption.

@li-mingbao
Copy link
Contributor Author

I think I addressed most of the previous issues for now.
This conversion should be more compliant to base profile of QIR.
While the base profile only allows for statically addressed qubits, the conversion works for static and dynamic qubits.
The same goes for the tests which increased the amount of tests but I guess more tests should not be an issue.

Also, there were some questions that I havent answered yet, mainly because I was unsure of the answer.

The main question is of course if this conversion produces valid QIR code.
The answer to this question is I am not sure. I think there is a QIR-runner implementation from MQT but I cant access it, so I am not able to verify if the resulting code works or not.
Also, the LLVM version from the QIR specification is outdated and hasnt been updated yet. This makes some of the conversions more tricky.
Currently the conversion (after converting it to .ll) produces the following code for a statically addressed bell_state:

; ModuleID = 'LLVMDialectModule'
source_filename = "LLVMDialectModule"

@mlir.llvm.nameless_global_1 = internal constant [3 x i8] c"r1\00"
@mlir.llvm.nameless_global_0 = internal constant [3 x i8] c"r0\00"

define void @bellStateStaticQubit() #0 {
  call void @__quantum__rt__initialize(ptr null)
  br label %1

1:                                                ; preds = %0
  call void @__quantum__qis__h__body(ptr null)
  call void @__quantum__qis__cx__body(ptr null, ptr inttoptr (i64 1 to ptr))
  br label %2

2:                                                ; preds = %1
  call void @__quantum__qis__mz__body(ptr null, ptr null)
  call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr))
  br label %3

3:                                                ; preds = %2
  call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr @mlir.llvm.nameless_global_1)
  call void @__quantum__rt__result_record_output(ptr null, ptr @mlir.llvm.nameless_global_0)
  ret void
}

declare void @__quantum__rt__initialize(ptr)

declare void @__quantum__qis__h__body(ptr)

declare void @__quantum__qis__cx__body(ptr, ptr)

declare void @__quantum__qis__mz__body(ptr, ptr) #1

declare void @__quantum__rt__result_record_output(ptr, ptr)

attributes #0 = { "dynamic_qubit_management"="false" "dynamic_result_management"="false" "entry_point" "output_labeling_schema"="schema_id" "qir_major_version"="1" "qir_minor_version"="0" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="2" }
attributes #1 = { "irreversible" }

!llvm.module.flags = !{!0}

!0 = !{i32 2, !"Debug Info Version", i32 3}

I think it would good if during the lowering process from QuantumComputation to MQTRef the entry_point attribute is attached to the main function. This is what I am currently looking for to find the main function.

@li-mingbao li-mingbao requested a review from burgholzer August 21, 2025 12:24
Copy link
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @li-mingbao 👋🏼
Thanks again for yet another iteration on the code! Really nice.
I went through the conversions in very much detail again and left further inline comments.
I'll let @ystade do the next review here. Until then, it would be great if you could explicitly answer on the comments themselves that are open from the last review and this review. Much appreciated 🙏🏼

Comment on lines +149 to +150
// create name and signature of the new function
const StringRef fnName = "__quantum__rt__qubit_allocate_array";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Truly a micro-optimization, but this string could be turned into a static member of the pattern struct so that it is not part of every matchAndRewrite call.
Same goes for the signature below and other similar functions throughout this file.

Comment on lines +335 to +344
// create the new callOp
const auto elemPtr =
rewriter
.create<LLVM::CallOp>(op.getLoc(), fnDecl,
ValueRange{adaptor.getInQreg(), index})
.getResult();

// replace the old operation with a loadOp
rewriter.replaceOpWithNewOp<LLVM::LoadOp>(
op, LLVM::LLVMPointerType::get(ctx), elemPtr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not 100% sure if this load is necessary. I'll leave that for @ystade to comment on when he is back from vacation.

Comment on lines +793 to +794
attributes.emplace_back(
builder.getStrArrayAttr({"dynamic_result_management", "false"}));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be using the flag from the state shouldn't it?

Comment on lines +170 to +172
// replace the int to ptr operation with static qubit
const auto newOp = rewriter.replaceOpWithNewOp<ref::QubitOp>(
op, ref::QubitType::get(rewriter.getContext()), value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems wasteful. It will insert an operation for every single static qubit access.
In the ref dialect, we should be good if we simply define a certain static qubit once and then use that value onwards. Quite similarly to how you cache the pointer in the other conversion (if I understood that correctly).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isnt there only one intToPtr operation in the beginning with a given index and this gets converted exactly once to get a static qubit? The result of this single static qubit conversion is then used for every access.

Comment on lines +296 to +306
// get the new operands from the operandMap
// workaround to avoid unrealized conversion
SmallVector<Value> newOperands;
newOperands.reserve(operands.size());
for (auto const& val : operands) {
if (getState().operandMap.contains(val)) {
newOperands.emplace_back(getState().operandMap.at(val));
} else {
newOperands.emplace_back(val);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe explain why exactly that workaround is required and what it is actually working around?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solves the issue that the TypeConverter did not work for the conversion as !llvm.ptr type needs to be converted to either !mqtref.Qubit or !mqtref.QubitRegister depending on the operation. Since the type typeConverter does not have any information regarding operation but only the types, I had to track which !llvm.ptr is converted to which operation in the map.

}

// remove the prefix and the suffix of the gate name
auto gateName(fnName->substr(16).drop_back(6));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we replace these magic numbers by constants with a usefull name?

Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: li-mingbao <74404929+li-mingbao@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Anything related to C++ code feature New feature or request MLIR Anything related to MLIR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants