Examples

In order to understand how this communication directives introduced by NetQIR work, we present several simple examples.

Every piece of code from now on will begin with the following code block.

%Qubit = type opaque
%Comm = type opaque
@netqir_comm_world = external global %Comm, align 1

So, in order to simplify the examples it is going to be ommited. These instructuctions define the %Qubit and the %Comm datatype along with the deafult communicator @netqir_comm_world. There are other directives that are also going to be common, but the previous ones are used in every example.

Point-to-point communication

In this section we will try to show how point-to-point communication works in NetQIR.

Employement of qsend and qrecv directives

Single file for qsend and qrecv

The following code shows how to use the qsend directive to send a qubit from one node to another.

Example of qsend and qrecv directives.
define void @main(i32 noundef %0, ptr noundef %1) #0 {
    entry:
        ; Variable allocation
        %2 = alloca i32, align 4
        
        ; Init the NetQIR communication and get the rank of the process
        %3 = call i32 @__netqir__init(i32 noundef %0, ptr noundef %1)
        %4 = call i32 @__netqir__comm_rank(%Comm* @netqir_comm_world, ptr %2)

        ; Choose if it is the process receiving or sending the qubit
        %5 = load i32, ptr %2, align 4
        %6 = icmp eq i32 %5, 0
        br i1 %6, label %7, label %9

    ; Process sending
    7:
        %8 = call i32 @__netqir__qsend(%Qubit* null, i32 noundef 1, 
                                       %Comm* @netqir_comm_world)
    ; Process receiving
    9:
        %10 = call i32 @__netqir__qrecv(%Qubit** null, i32 noundef 0, 
                                        %Comm* @netqir_comm_world)
        
        ; End of the program
        %11 = call i32 @__netqir__finalize()
}

; Function declaration
declare i32 @__netqir__init()
declare i32 @__netqir__qsend(%Qubit*, i32, %Comm)
declare i32 @__netqir__qrecv(%Qubit**, i32, %Comm)
declare void @__netqir__finalize()

One file for each directive

This previous program follows a very MPI-like structure, with a single file defining all the processes and an conditional structure to decide if the process is going to send or receive the qubit. It is also possible to have two files, one responsible for sending the qubit and the other for receiving it. Both structures will be perfectly valid, the only important thing is to give both processes the same communicator. But this is going to be the resposability of the backend.

Example of qsend directive in a single file.
define void @main(i32 noundef %0, ptr noundef %1) #0 {
    entry:
        ; Variable allocation
        %2 = alloca i32, align 4
        
        ; Init the NetQIR communication and get the rank of the process
        %3 = call i32 @__netqir__init(i32 noundef %0, ptr noundef %1)
        %4 = call i32 @__netqir__comm_rank(%Comm* @netqir_comm_world, ptr %2)
    
        ; Process sending
        %5 = call i32 @__netqir__qsend(%Qubit* null, i32 noundef 1, 
                                       %Comm* @netqir_comm_world)
        
        ; End of the program
        %6 = call i32 @__netqir__finalize()
}

; Function declaration
declare i32 @__netqir__init()
declare i32 @__netqir__qsend(%Qubit*, i32, %Comm)
declare void @__netqir__finalize()
Example of qrecv directive in a single file.
define void @main(i32 noundef %0, ptr noundef %1) #0 {
    entry:
        ; Variable allocation
        %2 = alloca i32, align 4
        
        ; Init the NetQIR communication and get the rank of the process
        %3 = call i32 @__netqir__init(i32 noundef %0, ptr noundef %1)
        %4 = call i32 @__netqir__comm_rank(%Comm* @netqir_comm_world, ptr %2)
    
        ; Process receiving
        %5 = call i32 @__netqir__qrecv(%Qubit** null, i32 noundef 0, 
                                        %Comm* @netqir_comm_world)
        
        ; End of the program
        %6 = call i32 @__netqir__finalize()
}

; Function declaration
declare i32 @__netqir__init()
declare i32 @__netqir__qrecv(%Qubit*, i32, %Comm)
declare void @__netqir__finalize()

Two examples have been shown for the qsend and qrecv directives: a single file for both process types and one file for each process type. The first example is commonly used by multicore infrastructure, e.g. MPI. On the other hand, the second example is intended for network communications where there is no perfectly coordinated computation.

Sending and receiving and array of qubits

In this case, the structure employed—instead of %Qubit—is %Array. This is because we are going to send and receive a slot of qubits. This structure is the same one that QIR introduces, so the reader is referred to the QIR specification for more information.

Example of qsend and qrecv directives with an array of qubits.
%Array = type opaque

define void @main(i32 noundef %0, ptr noundef %1) #0 {
    entry:
        ; Variable allocation
        %2 = alloca i32, align 4
        %sent_qubits = call %Array* @__quantum__rt__qubit_allocate_array(i64 3)

        ; Init the NetQIR communication and get the rank of the process
        %3 = call i32 @__netqir__init(i32 noundef %0, ptr noundef %1)
        %4 = call i32 @__netqir__comm_rank(%Comm* @netqir_comm_world, ptr %2)

        ; Choose if it is the process receiving or sending the qubit
        %5 = load i32, ptr %2, align 4
        %6 = icmp eq i32 %5, 0
        br i1 %6, label %7, label %9

    ; Process sending
    7:
        %8 = call i32 @__netqir__qsend_array(%Array* %sent_qubits, i32 noundef 1,
                                             i32 noundef 3, %Comm* @netqir_comm_world)
        
    ; Process receiving
    9:
        %10 = call i32 @__netqir__qrecv_array(%Qubit** null, i32 noundef 0,
                                        i32 noundef 3, %Comm* @netqir_comm_world)
        
        ; End of the program
        %11 = call i32 @__netqir__finalize()
}

; Function declaration
declare i32 @__netqir__init()
declare i32 @__netqir__qsend_array(%Array*, i32, i32, %Comm)
declare i32 @__netqir__qrecv_array(%Array**, i32, i32, %Comm)
declare void @__netqir__finalize()

Similar to before, this could be splitted into two different files, one for sending and the other for receiving. In this case, this examplification will be ommited due to the similarity with the previous example.

Teledata vs telegate

Both qsend and qrecv directives have two versions: qsend and qsend_array and qrecv and qrecv_array, this was already mentioned in the previous examples. But, beneath this two versions, two more directives are available for each: on the first case qsend_teledata and qsend_telegate, and on the second case qrecv_array_teledata and qrecv_array_telegate. This directives are used to explicitly determine whether the communication is done with the teledata protocol or with the telegate protocol.

This way of marking the communication protocol is not random. A flag inside the communication directives could be used to determine the protocol. But this flag could represent a barrier for compiler optimizations, because in order to determine which is the protocol it should know the value of the aforementioned flag. This could be a problem when the flag is not a constant.

The use of these directives is exactly the same as the previous ones, the only difference is that the programmer has the control over the protocol used for the communication. But the actual code is exactly the same except for the directive name.

Collective communication

After seing the most basic communication directives, i.e. sending and receiving information between two different parties, we are going to see how collective communication works in NetQIR. This type of communication represents a one to many, or vice versa, way of communicating, where one party sends information to all the others or all the parties send information to one.

This is pretty common in classical computing with, for example, MPI directives such as MPI_Gather, MPI_Scatter, MPI_Bcast, etc. This are operations that, nevertheless, are not directly translatable to quantum computing. There are several reasons for that, but the most important one is the non-cloning theorem. The fact that a quantum state cannot be copied eliminates the possibility of getting a broadcast operation understood as the classical one: one party sends a copy of the information to all the others.

This is why the Collective communication section has introduced several functions that adapt the collective communication paradigm to quantum computing. These functions are the targets of the following examples.

Expose

First, we will start with a simple example. In this case, one process exposes a qubit and all the other nodes perform a CNOT operation on this qubit.

Example of the expose directive.
define void @main(i32 noundef %0, ptr noundef %1) #0 {
    entry:
        ; Variable allocation
        %2 = alloca i32, align 4
        
        ; Init the NetQIR communication and get the rank of the process
        %3 = call i32 @__netqir__init(i32 noundef %0, ptr noundef %1)
        %4 = call i32 @__netqir__comm_rank(%Comm* @netqir_comm_world, ptr %2)

        ; Choose if it is the process receiving or sending the qubit
        %5 = load i32, ptr %2, align 4
        %6 = icmp eq i32 %5, 0
        br i1 %6, label %7, label %9

    ; Process sending
    7:

        %8 = call i32 @__netqir__expose(%Qubit* null, i32 noundef 0, 
                                       %Comm* @netqir_comm_world)
    ; Process receiving
    9:
        %10 = call i32 @__netqir__expose(%Qubit* %a, i32 noundef 0, 
                                       %Comm* @netqir_comm_world)
        %11 = call i32 @__quantum__qis__cnot__body(%Qubit* null, %Qubit* %a)

        ; End of the program
        %12 = call i32 @__netqir__finalize()
}

; Function declaration
declare i32 @__netqir__init()
declare i32 @__netqir__expose(%Qubit*, i32, %Comm)
declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*)
declare void @__netqir__finalize()