Skip to content

Memory Management

Optimal memory performance is an important concern in quantum algorithm design, just as in its classical counterpart. The Classiq platform's synthesis engine implements memory management to decrease the number of qubits required to execute the quantum program. This capability stems from harnessing algorithmic-level information on the state of the qubits during each computation stage.

This page describes how memory management is handled in the Classiq platform and how users can aid the engine in creating narrower circuits. It is split into four parts:

  1. Allocating functional qubits
  2. Releasing functional qubits implicitly
  3. Releasing functional qubits explicitly
  4. Allocating and releasing auxiliary qubits automatically

Allocated qubits are at the zero state \(\left|0\right>\) and are taken from the quantum device pool or from the released qubit pool, managed by the engine.

CAUTION! Make sure to validate all user-provided information supplied to the engine. Incorrect instructions will most likely lead to incorrect circuits.

In the following sections, we will explain these methods in detail and provide a few examples.

Functional Qubit Allocation

The Classiq platform automatically allocates qubits from the device pool to populate functional registers when required, namely, when the register does not serve as an output of another function. In addition, users can specify the more restrictive zero input registers. See User Defined Functions. Such specification is required by many functions to be well-defined. For example, a state preparation function converts the state \(\left|0\right>\) to some desired state \(\left|\psi\right>\). Its action on other input states is usually unimportant, and, therefore, this function can be implemented in a multitude of ways. The synthesis engine, in turn, benefits from this selection freedom to improve the circuit's resource allocation. See Classiq's library State Preparation as an example of a function that assumes that its inputs are at the zero state.

In terms of modeling and memory management, there are two main differences between regular inputs and zero inputs:

  1. Zero inputs cannot be connected explicitly by another function during the model construction stage. They can, after memory management optimization, reuse qubits of other functions, but this dependency has not been forced in advance.
  2. When performing inversion, the engine can release zero inputs automatically. This will be discussed in the following section.

Implicit Qubit Release

Aiding zero qubits are a special case of zero input qubits, and they are used implicitly to match input and output sizes. Under certain conditions, as detailed below, they can be released after usage.

Consider, for example, the case of an addition function, to which you add two unsigned integer registers. Generally speaking, the result is larger and requires more qubits for its representation. Even if the computation is done "in place," one more qubit is required as an MSB. Note that the added MSB qubit is independent of the choice of the addition implementation. This new qubit is called an aiding zero qubit. The Classiq engine adds it implicitly.

Suppose you perform an addition operation, use the result, and—with the intent of writing efficient code—uncompute the addition operation to release the added MSB qubit. Inversion does not automatically release the qubit, as described in the inversion documentation. In fact, the qubit necessarily returns to a zero state after uncomputation if the entire operation between computation and uncomputation is "quantum free". You can request to release the addition MSB qubit using the release_by_inverse flag together with the is_inverse flag, as shown.

Note: For the single adder in the example, the aiding qubits are not
auxiliary (see auxiliary qubits) and they play a
different role. They are part of the functional description of the adder
function, enter at a zero state, and exit as outputs. However, the zero-state qubits can be regarded as auxiliary for the composite block
consisting of the adder, the operation in between, and the uncomputation.

Usage

{
  "functions": [
    {
      "name": "main",
      "port_declarations": {
        "model_arg": {
          "name": "model_arg",
          "size": {"expr": "3"},
          "direction": "inout"
        },
        "result": {
          "name": "result",
          "size": {"expr": "4"},
          "direction": "output"
        }
      },
      "body": [
        {
          "function": "Adder",
          "function_params": {
            "left_arg": { "size": 3 },
            "right_arg": 1,
            "inplace_arg": "left"
          },
          "inputs": { "left_arg": "model_arg_in" },
          "outputs": { "sum": "adder_to_multiplier_wire" },
          "is_inverse": false
        },
        {
          "function": "Multiplier",
          "function_params": {
            "left_arg": { "size": 4 },
            "right_arg": 2
          },
          "inputs": { "left_arg": "adder_to_multiplier_wire" },
          "outputs": {
            "product": "result_out",
            "left_arg": "multiplier_to_adder_uncomp_wire"
          }
        },
        {
          "function": "Adder",
          "function_params": {
            "left_arg": { "size": 3 },
            "right_arg": 1,
            "inplace_arg": "left"
          },
          "inputs": { "sum": "multiplier_to_adder_uncomp_wire" },
          "outputs": { "left_arg": "model_arg_out" },
          "is_inverse": true,
          "release_by_inverse": true
        }
      ]
    }
  ]
}
from classiq import Model, RegisterUserInput, QUInt
from classiq.builtin_functions import Adder, Multiplier
from classiq.builtin_functions.binary_ops import ArgToInplace

input_size: int = 3
model_arg = RegisterUserInput(size=input_size)
adder_params = Adder(left_arg=model_arg, right_arg=1, inplace_arg=ArgToInplace.LEFT)
multiplier_params = Multiplier(
    left_arg=RegisterUserInput(size=input_size + 1), right_arg=2
)

model = Model()
model_inputs = model.create_inputs({"model_arg": QUInt[input_size]})

adder_outputs = model.Adder(
    params=adder_params, in_wires={"left_arg": model_inputs["model_arg"]}
)
multiplier_outputs = model.Multiplier(
    params=multiplier_params, in_wires={"left_arg": adder_outputs["sum"]}
)
adder_uncomp_outputs = model.Adder(
    params=adder_params,
    in_wires={"sum": multiplier_outputs["left_arg"]},
    is_inverse=True,
    release_by_inverse=True,
)

model.set_outputs(
    {
        "model_arg": adder_uncomp_outputs["left_arg"],
        "result": multiplier_outputs["product"],
    }
)

In the resulting circuit, shown below, the qubit labeled as zero_sum is released after the second adder (adder_1, marked in blue), which is the inverse of the first adder (adder_0), is applied.

img.png

Explicit Qubit Release

Explicit qubit release refers to the release of functional registers; the outputs of any function. Wiring an output to zero forces its release.

To declare that an output register is in the zero state, wire it to "0" in the textual model or use Model.release_qregs in the SDK. The functionality is available for any function, inverted or not. However, as stressed above, it must be done with caution.

The following example uses explicit outputs to convert a Boolean function. The result is coded on qubits, using Oracle CCX gates that code the result on phases. The X and H gates create a minus state for a phase kickback.

Usage

{
  "functions": [
    {
      "name": "main",
      "port_declarations": {
        "ARG": {
          "name": "ARG",
          "size": {"expr": "2"},
          "direction": "inout"
        }
      },
      "body": [
        {
          "function": "XGate",
          "function_params": {},
          "outputs": { "TARGET": "minus_state_inner_wire" }
        },
        {
          "function": "HGate",
          "function_params": {},
          "inputs": { "TARGET": "minus_state_inner_wire" },
          "outputs": { "TARGET": "minus_state_wire" }
        },
        {
        "function": "CCXGate",
        "function_params": {},
        "inputs": { "TARGET": "minus_state_wire", "CTRL": "ARG_in" },
        "outputs": {
          "TARGET": "minus_state_uncomp_wire",
          "CTRL": "ARG_out"
        }
      },
      {
        "function": "HGate",
        "function_params": {},
        "inputs": { "TARGET": "minus_state_uncomp_wire" },
        "outputs": { "TARGET": "minus_state_uncomp_inner_wire" }
      },
      {
        "function": "XGate",
        "function_params": {},
        "inputs": { "TARGET": "minus_state_uncomp_inner_wire" },
        "outputs": { "TARGET": "0" }
      }
    ]
  }
]
}
from classiq import Model, RegisterUserInput, QUInt
from classiq.builtin_functions import CCXGate, XGate, HGate

model = Model()
model_inputs = model.create_inputs({"ARG": QUInt[2]})

x_outputs = model.XGate(params=XGate())
h_outputs = model.HGate(params=HGate(), in_wires=x_outputs)
ccx_params = CCXGate()
ccx_outputs = model.CCXGate(
    params=ccx_params,
    in_wires={"TARGET": h_outputs["TARGET"], "CTRL": model_inputs["ARG"]},
)
h_uncomp_outputs = model.HGate(
    params=HGate(), in_wires={"TARGET": ccx_outputs["TARGET"]}
)
out = model.XGate(
    params=XGate(),
    in_wires=h_uncomp_outputs,
)["TARGET"]

model.release_qregs(out)

model.set_outputs({"ARG": ccx_outputs["CTRL"]})

Auxiliary Qubits

Auxiliary qubits are not part of the description of a quantum function, since they are implementation dependent. As such, only the synthesis engine is responsible for their allocation and release. See Auxiliaries and Uncomputation an example of defining a custom function that has an implementation with auxiliary qubits.

Currently, the Classiq platform supports auxiliary qubits that enter a function in a zero state and exit as such.

In the example below, implement \(C^4\)Ry gate using V Chain. Qubits 0, 1, 2, 3 serve as control qubits, qubit 5 as the target, and qubit 4, marked in red, is an auxiliary.

img.png

Overriding Strict Zero IOs

This paragraph is for advanced usage only. Make sure you fully understand what you are about to do before using this option.

Some of Classiq's library functions, such as the State Preparation, have their inputs defined as zero inputs. This tells the engine to automatically allocate qubits for these inputs and release them when performing an inversion (see discussion above). The user can choose to switch to manual management by setting the flag strict_zero_ios to False during the function call.

Overriding this flag will allow to connect the zero registers to other functions during the model construction stage.

When this flag is set to False, it is the user's responsibility to ensure that zero inputs are fed with zero state qubits. In addition, the toggling will disable the ability of an inverted function to automatically release qubits and it is the user's responsibility to do that manually if needed (see how to perform that in explicit qubit release).

CAUTION! Since functions with zero inputs are only defined under such an assumption, overriding this setting and connecting zero input registers to non-zero qubits will lead to undefined behavior.

The example below demonstrates the use of this flag via a multiplexed state preparation. For every computational basis state input, at most one state preparation operates, ensuring that the zero input requirement is satisfied. This knowledge serves to reduce the required number of qubits since the second state preparation does not need to draw new qubits from the device pool.

{
  "functions": [
    {
      "name": "main",
      "body": [
        {
          "function": "HGate",
          "outputs": {
            "TARGET": "hadamard_out"
          }
        },
        {
          "function": "StatePreparation",
          "function_params": {
            "probabilities": {
              "pmf": [
                0.05,
                0.11,
                0.13,
                0.23,
                0.27,
                0.12,
                0.03,
                0.06
              ]
            },
            "error_metric": {
              "KL": {
                "upper_bound": 0.01
              }
            }
          },
          "control_states": [
            {
              "num_ctrl_qubits": 1,
              "ctrl_state": "0",
              "name": "control"
            }
          ],
          "inputs": {
            "control": "hadamard_out"
          },
          "outputs": {
            "OUT": "sp_0_out",
            "control": "control"
          },
          "name": "state_preparation_0"
        },
        {
          "function": "StatePreparation",
          "function_params": {
            "probabilities": {
              "pmf": [
                0.27,
                0.12,
                0.03,
                0.06,
                0.05,
                0.11,
                0.13,
                0.23
              ]
            },
            "error_metric": {
              "KL": {
                "upper_bound": 0.01
              }
            }
          },
          "strict_zero_ios": false,
          "control_states": [
            {
              "num_ctrl_qubits": 1,
              "ctrl_state": "1",
              "name": "control"
            }
          ],
          "inputs": {
            "IN": "sp_0_out",
            "control": "control"
          },
          "name": "state_preparation_1"
        }
      ]
    }
  ]
}
from classiq import ControlState, Model, show, synthesize
from classiq.builtin_functions import HGate, StatePreparation

error_metric = {"KL": {"upper_bound": 0.01}}

multiplexed_sp_model = Model()
hadamard = multiplexed_sp_model.HGate(HGate())
sp_zero_state = multiplexed_sp_model.StatePreparation(
    StatePreparation(
        probabilities=(0.05, 0.11, 0.13, 0.23, 0.27, 0.12, 0.03, 0.06),
        error_metric=error_metric,
    ),
    control_states=[ControlState(ctrl_state="0", name="control")],
    in_wires={"control": hadamard["TARGET"]},
    call_name="state_preparation_0",
)
multiplexed_sp_model.StatePreparation(
    StatePreparation(
        probabilities=(0.27, 0.12, 0.03, 0.06, 0.05, 0.11, 0.13, 0.23),
        error_metric=error_metric,
    ),
    control_states=[ControlState(ctrl_state="1", name="control")],
    strict_zero_ios=False,
    in_wires={"IN": sp_zero_state["OUT"], "control": sp_zero_state["control"]},
    call_name="state_preparation_1",
)
quantum_program = synthesize(multiplexed_sp_model.get_model())
show(quantum_program)

img.png