Skip to content

Releases: PennyLaneAI/catalyst

Catalyst v0.12.0

15 Jul 13:46
6fc5e13
Compare
Choose a tag to compare

New features since last release

  • A new compilation pass called passes.ppm_compilation has been added to Catalyst to transform Clifford+T gates into Pauli Product Measurements (PPMs) using just one transform, allowing for exploring representations of programs in a new paradigm in logical quantum compilation. (#1750)

    Based on arXiv:1808.02892, this new compilation pass simplifies circuit transformations and optimizations by combining multiple sub-passes into a single compilation pass, where Clifford+T gates are compiled down to Pauli product rotations (PPRs, $\exp(-iP_{{x, y, z}} \theta)$ ) and PPMs:

    • passes.to_ppr: converts Clifford+T gates into PPRs. - passes.commute_ppr: commutes PPRs past non-Clifford PPRs. - passes.merge_ppr_ppm: merges Clifford PPRs into PPMs. - passes.ppr_to_ppm: decomposes both non-Clifford PPRs ($\theta = \tfrac{\pi}{8}$), consuming a magic state in the process, and Clifford PPRs ($\theta = \tfrac{\pi}{4}$) into PPMs. (#1664)
    import pennylane as qml
    from catalyst.passes import ppm_compilation
    
    pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])]
    
    @qml.qjit(pipelines=pipeline, target="mlir")
    @ppm_compilation(decompose_method="clifford-corrected", avoid_y_measure=True, max_pauli_size=2)
    @qml.qnode(qml.device("null.qubit", wires=2))
    def circuit():
        qml.CNOT([0, 1])
        qml.CNOT([1, 0])
        qml.adjoint(qml.T)(0)
        qml.T(1)
        return catalyst.measure(0), catalyst.measure(1)
    >>> print(circuit.mlir_opt)
    ...
    %m, %out:3 = qec.ppm ["Z", "Z", "Z"] %1, %2, %4 : !quantum.bit, !quantum.bit, !quantum.bit
    %m_0, %out_1:2 = qec.ppm ["Z", "Y"] %3, %out#2 : !quantum.bit, !quantum.bit
    %m_2, %out_3 = qec.ppm ["X"] %out_1#1 : !quantum.bit
    %m_4, %out_5 = qec.select.ppm(%m, ["X"], ["Z"]) %out_1#0 : !quantum.bit
    %5 = arith.xori %m_0, %m_2 : i1
    %6:2 = qec.ppr ["Z", "Z"](2) %out#0, %out#1 cond(%5) : !quantum.bit, !quantum.bit
    quantum.dealloc_qb %out_5 : !quantum.bit
    quantum.dealloc_qb %out_3 : !quantum.bit
    %7 = quantum.alloc_qb : !quantum.bit
    %8 = qec.fabricate  magic_conj : !quantum.bit
    %m_6, %out_7:2 = qec.ppm ["Z", "Z"] %6#1, %8 : !quantum.bit, !quantum.bit
    %m_8, %out_9:2 = qec.ppm ["Z", "Y"] %7, %out_7#1 : !quantum.bit, !quantum.bit
    %m_10, %out_11 = qec.ppm ["X"] %out_9#1 : !quantum.bit
    %m_12, %out_13 = qec.select.ppm(%m_6, ["X"], ["Z"]) %out_9#0 : !quantum.bit
    %9 = arith.xori %m_8, %m_10 : i1
    %10 = qec.ppr ["Z"](2) %out_7#0 cond(%9) : !quantum.bit
    quantum.dealloc_qb %out_13 : !quantum.bit
    quantum.dealloc_qb %out_11 : !quantum.bit
    %m_14, %out_15:2 = qec.ppm ["Z", "Z"] %6#0, %10 : !quantum.bit, !quantum.bit
    %from_elements = tensor.from_elements %m_14 : tensor<i1>
    %m_16, %out_17 = qec.ppm ["Z"] %out_15#1 : !quantum.bit
    ...
  • A new function called passes.get_ppm_specs has been added for acquiring statistics after PPM compilation. (#1794)

    After compiling a workflow with any combination of passes.to_ppr, passes.commute_ppr, passes.merge_ppr_ppm, passes.ppr_to_ppm, or passes.ppm_compilation, use passes.get_ppm_specs to track useful statistics of the compiled workflow, including:

    • num_pi4_gates : number of Clifford PPRs - num_pi8_gates : number of non-Clifford PPRs - num_pi2_gates : number of classical PPRs - max_weight_pi4 : maximum weight of Clifford PPRs - max_weight_pi8 : maximum weight of non-Clifford PPRs - max_weight_pi2 : maximum weight of classical PPRs - num_logical_qubits : number of logical qubits - num_of_ppm : number of PPMs
    from catalyst.passes import get_ppm_specs, to_ppr, merge_ppr_ppm, commute_ppr
    
    pipe = [("pipe", ["enforce-runtime-invariants-pipeline"])]
    
    @qjit(pipelines=pipe, target="mlir", autograph=True)
    def test_convert_clifford_to_ppr_workflow():
    
        device = qml.device("lightning.qubit", wires=2)
    
        @merge_ppr_ppm
        @commute_ppr(max_pauli_size=2)
        @to_ppr
        @qml.qnode(device)
        def f():
            qml.CNOT([0, 2])
            qml.T(0)
            return measure(0), measure(1)
    
        @merge_ppr_ppm(max_pauli_size=1)
        @commute_ppr
        @to_ppr
        @qml.qnode(device)
        def g():
            qml.CNOT([0, 2])
            qml.T(0)
            qml.T(1)
            qml.CNOT([0, 1])
            for i in range(10):
              qml.Hadamard(0)
            return measure(0), measure(1)
    
        return f(), g()
    >>> ppm_specs = get_ppm_specs(test_convert_clifford_to_ppr_workflow)
    >>> print(ppm_specs)
    {
    'f_0': {'max_weight_pi8': 1, 'num_logical_qubits': 2, 'num_of_ppm': 2, 'num_pi8_gates': 1}, 
    'g_0': {'max_weight_pi4': 2, 'max_weight_pi8': 1, 'num_logical_qubits': 2, 'num_of_ppm': 2, 'num_pi4_gates': 36, 'num_pi8_gates': 2}
    }
  • Catalyst now supports qml.Snapshot, which captures quantum states at any point in a circuit. (#1741)

    For example, the code below is capturing two snapshot'd states, all within a qjit'd circuit:

    NUM_QUBITS = 2
    dev = qml.device("lightning.qubit", wires=NUM_QUBITS)
    
    @qjit
    @qml.qnode(dev)
    def circuit():
        wires = list(range(NUM_QUBITS))
        qml.Snapshot("Initial state")
    
        for wire in wires:
            qml.Hadamard(wires=wire)
    
        qml.Snapshot("After applying Hadamard gates")
    
        return qml.probs()
    
    results = circuit()
    snapshots, *results = circuit()
    
    >>> print(snapshots)
    [Array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex128), 
    Array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex128)]
    >>> print(results)
    Array([0.25, 0.25, 0.25, 0.25], dtype=float64)
    >>> print(results)
    ([Array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex128), 
    Array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex128)], 
    Array([0.25, 0.25, 0.25, 0.25], dtype=float64))
  • Catalyst now supports automatic qubit management, meaning that the number of wires does not need to be specified during device initialization. (#1788)

    @qjit
    def workflow():
        dev = qml.device("lightning.qubit") # no wires here!
        @qml.qnode(dev)
        def circuit():
            qml.PauliX(wires=2)
            return qml.probs()
        return circuit()
    
    print(workflow())
    [0. 1. 0. 0. 0. 0. 0. 0.]

    While this feature adds a lot of convenience, it may also reduce performance on devices where reallocating resources can be expensive, such as statevector simulators.

  • Two new peephole-optimization compilation passes called passes.disentangle_cnot and passes.disentangle_swap have been added. Each compilation pass replaces SWAP or CNOT instructions with other equivalent elementary gates. (#1823)

    As an example, passes.disentangle_cnot applied to the circuit below will replace the CNOT gate with an X gate.

    dev = qml.device("lightning.qubit", wires=2)
    
    @qml.qjit(keep_intermediate=True)
    @catalyst.passes.disentangle_cnot
    @qml.qnode(dev)
    def circuit():
        # first qubit in |1>
        qml.X(0)
        # second qubit in |0>
        # current state : |10>
        qml.CNOT([0,1]) # state after CNOT : |11>
        return qml.state()
    >>> from catalyst.debug import get_compilation_stage
    >>> print(get_compilation_stage(circuit, stage="QuantumCompilationPass"))
    ...
    %out_qubits = quantum.custom "PauliX"() %1 : !quantum.bit
    %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit
    %out_qubits_0 = quantum.custom "PauliX"() %2 : !quantum.bit
    ...

Improvements 🛠

  • The qml.measure operation for mid-circuit measurements can now be used in qjit-compiled circuits with program capture enabled. (#1766)

    Note that the simulation behaviour of mid-circuit measurements can differ between PennyLane and Catalyst, depending on the chosen mcm_method. Please see the
    Functionality differences from PennyLane section in the :doc:sharp bits and debugging tips page <sharp_bits> for additional information.

  • The behaviour of measurement processes executed on null.qubit with qjit is now more consistent with their behaviour on null.qubit without qjit. (#1598)

    Previously, measurement processes like qml.sample, qml.counts, qml.probs, etc., returned values from uninitialized memory when executed on null.qubit with qjit. This change ensures that measurement processes on null.qubit always return the value 0 or the result corresponding to the '0' state, depending on the context.

  • The package name of the Catalyst distribution has been updated to be consistent with PyPA standards, from PennyLane-Catalyst to pennylane_catalyst. This change is not expected to affect users as tools in the Python ecosystem (e.g. pip) already handle both versions through normalization. (#1817)

  • The passes.commute_ppr and passes.merge_ppr_ppm passes now accept an optional max_pauli_size argument, which limits the size of the Pauli strings generated by the passes through commutation or absorption rules. (#1719)

  • The passes.to_ppr pass is now more efficient by adding support for the direct conversion of...

Read more

Catalyst v0.11.0-post1

05 May 15:07
Compare
Choose a tag to compare

Users are now redirected to pennylane.ai/search with the doc content type selected and the associated project and version filters selected when using the search bar.

Catalyst v0.11.0

15 Apr 16:15
2ccf8ee
Compare
Choose a tag to compare

New features since last release

  • A novel optimization technique is implemented in Catalyst that performs quantum peephole optimizations across loop boundaries. The technique has been added to the existing optimizations cancel_inverses and merge_rotations to increase their effectiveness in structured programs. (#1476)

    A frequently occurring pattern is operations at the beginning and end of a loop that cancel each other out. With loop boundary analysis, the cancel_inverses optimization can eliminate these redundant operations and thus reduce quantum circuit depth.

    For example,

    dev = qml.device("lightning.qubit", wires=2)
    
    @qml.qjit
    @catalyst.passes.cancel_inverses
    @qml.qnode(dev)
    def circuit():
        for i in range(3):
            qml.Hadamard(0)
            qml.CNOT([0, 1])
            qml.Hadamard(0)
        return qml.expval(qml.Z(0))

    Here, the Hadamard gate pairs which are consecutive across two iterations are eliminated, leaving behind only two unpaired Hadamard gates, from the first and last iteration, without unrolling the for loop. For more details on loop-boundary optimization, see the PennyLane Compilation entry.

  • A new intermediate representation and compilation framework has been added to Catalyst to describe and manipulate programs in the Pauli product measurement (PPM) representation. As part of this framework, three new passes are now available to convert Clifford + T gates to Pauli product measurements as described in arXiv:1808.02892. (#1499) (#1551) (#1563) (#1564) (#1577)

    Note that programs in the PPM representation cannot yet be executed on available backends. The passes currently exist for analysis, but PPM programs may become executable in the future when a suitable backend is available.

    The following new compilation passes can be accessed from the catalyst.passes module or in catalyst.pipeline:

    • catalyst.passes.to_ppr: Clifford + T gates are converted into Pauli product rotations (PPRs) ($\exp{iP \theta}$, where $P$ is a tensor product of Pauli operators):

      • H gate → 3 rotations with $P_1 = Z, P_2 = X, P_3 = Z$ and $\theta = \tfrac{\pi}{4}$
      • S gate → 1 rotation with $P = Z$ and $\theta = \tfrac{\pi}{4}$
      • T gate → 1 rotation with $P = Z$ and $\theta = \tfrac{\pi}{8}$
      • CNOT gate → 3 rotations with $P_1 = (Z \otimes X), P_2 = (-Z \otimes \mathbb{1}), P_3 = (-\mathbb{1} \otimes X)$ and $\theta = \tfrac{\pi}{4}$
    • catalyst.passes.commute_ppr: Commute Clifford PPR operations (PPRs with $\theta = \tfrac{\pi}{4}$) to the end of the circuit, past non-Clifford PPRs (PPRs with $\theta = \tfrac{\pi}{8}$)

    • catalyst.passes.ppr_to_ppm: Absorb Clifford PPRs into terminal Pauli product measurements (PPMs).

    For more information on PPMs, please refer to our PPM documentation page.

  • Catalyst now supports qubit number-invariant compilation. That is, programs can be compiled without specifying the number of qubits to allocate ahead of time. Instead, the device can be supplied with a dynamic program variable as the number of wires. (#1549) (#1553) (#1565) (#1574)

    For example, the following toy workflow is now supported, where the number of qubits, n, is provided as an argument to a qjit'd function:

    import catalyst
    import pennylane as qml
    
    @catalyst.qjit(autograph=True)
    def f(n):  
        device = qml.device("lightning.qubit", wires=n, shots=10)
    
        @qml.qnode(device)
        def circuit():
    
            for i in range(n):
                qml.RX(1.5, wires=i)
    
            return qml.counts()
    
        return circuit()
    >>> f(3)
    (Array([0, 1, 2, 3, 4, 5, 6, 7], dtype=int64),
    Array([0, 0, 3, 2, 3, 1, 1, 0], dtype=int64))
    >>> f(4)
    (Array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15],      dtype=int64),
    Array([0, 0, 1, 1, 2, 0, 0, 0, 0, 0, 1, 1, 2, 1, 0, 1], dtype=int64))
  • Catalyst better integrates with PennyLane program capture, supporting PennyLane-native control flow operations and providing more efficient transform handling when both Catalyst and PennyLane support a transform. (#1468) (#1509) (#1521) (#1544) (#1561) (#1567) (#1578)

    Using PennyLane's program capture mechanism involves setting experimental_capture=True in the qjit decorator. With this present, the following control flow functions in PennyLane are now usable with qjit:

    • Support for qml.cond:

      import pennylane as qml
      from catalyst import qjit
      
      dev = qml.device("lightning.qubit", wires=1)
      
      @qjit(experimental_capture=True)
      @qml.qnode(dev)
      def circuit(x: float):
      
          def ansatz_true():
              qml.RX(x, wires=0)
              qml.Hadamard(wires=0)
      
          def ansatz_false():
              qml.RY(x, wires=0)
      
          qml.cond(x > 1.4, ansatz_true, ansatz_false)()
      
          return qml.expval(qml.Z(0))
      >>> circuit(0.1)
      Array(0.99500417, dtype=float64)
    • Support for qml.for_loop:

      dev = qml.device("lightning.qubit", wires=2)
      
      @qjit(experimental_capture=True)
      @qml.qnode(dev)
      def circuit(x: float):
      
          @qml.for_loop(10)
          def loop(i):
              qml.H(wires=1)
              qml.RX(x, wires=0)
              qml.CNOT(wires=[0, 1])
      
          loop()
          return qml.expval(qml.Z(0))
      >>> circuit(0.1)
      Array(0.97986841, dtype=float64)
    • Support for qml.while_loop:

      @qjit(experimental_capture=True)
      @qml.qnode(dev)
      def circuit(x: float):
      
          f = lambda c: c < 5
      
          @qml.while_loop(f)
          def loop(c):
              qml.H(wires=1)
              qml.RX(x, wires=0)
              qml.CNOT(wires=[0, 1])
      
              return c + 1
          
          loop(0)
          return qml.expval(qml.Z(0))
      >>> circuit(0.1)
      Array(0.97526892, dtype=float64)

    Additionally, Catalyst can now apply its own compilation passes when equivalent transforms are provided by PennyLane (e.g., cancel_inverses and merge_rotations). In cases where Catalyst does not have its own analogous implementation of a transform available in PennyLane, the transform will be expanded according to rules provided by PennyLane.

    For example, consider this workflow that contains two PennyLane transforms: cancel_inverses and single_qubit_fusion. Catalyst has its own implementation of cancel_inverses in the passes module, and will smartly invoke its implementation intead. Conversely, Catalyst does not have its own implementation of single_qubit_fusion, and will therefore resort to PennyLane's implementation of the transform.

    dev = qml.device("lightning.qubit", wires=1)
    
    @qjit(experimental_capture=True)
    def func(r1, r2):
    
        @qml.transforms.cancel_inverses
        @qml.transforms.single_qubit_fusion
        @qml.qnode(dev)
        def circuit(r1, r2):
            qml.Rot(*r1, wires=0)
            qml.Rot(*r2, wires=0)
            qml.RZ(r1[0], wires=0)
            qml.RZ(r2[0], wires=0) 
    
            qml.Hadamard(wires=0)
            qml.Hadamard(wires=0)
            
            return qml.expval(qml.PauliZ(0))  
    
        return circuit(r1, r2)
    >>> r1 = jnp.array([0.1, 0.2, 0.3])
    >>> r2 = jnp.array([0.4, 0.5, 0.6])
    >>> func(r1, r2)
    Array(0.7872403, dtype=float64)

Improvements 🛠

  • Several changes have been made to reduce compile time:

    • MLIR's verifier has been turned off. (#1513)
    • Unnecessary I/O has been removed. (#1514) (#1602)
    • Improvements have been made to reduce complexity and memory. (#1524)
    • IR canonicalization and LLVMIR textual generation is now performed lazily. (#1530)
    • Speed up how tracers are overwritten for hybrid ops. (#1622)
  • Catalyst now decomposes non-differentiable gates when differentiating through workflows. Additionally, with diff_method=parameter-shift, circuits are now verified to be fully compatible with Catalyst's parameter-shift implementation before compilation. (#1562) (#1568) (#1569) (#1604)

    Gates that are constant, such as when all parameters are Python or NumPy data types, are not decomposed when this is allow...

Read more

Catalyst v0.10.0

14 Jan 17:20
2c441bf
Compare
Choose a tag to compare

New features since last release

  • Catalyst can now load and apply local MLIR plugins from the PennyLane frontend. (#1287) (#1317) (#1361) (#1370)

    Custom compilation passes and dialects in MLIR can be specified for use in Catalyst via a shared object (*.so or *.dylib on macOS) that implements the pass. Details on creating your own plugin can be found in our compiler plugin documentation. At a high level, there are three ways to use a plugin once it's properly specified:

    • catalyst.passes.apply_pass can be used on QNodes when there is a Python entry point defined for the plugin. In that case, the plugin and pass should both be specified and separated by a period.

      @catalyst.passes.apply_pass("plugin_name.pass_name")
      @qml.qnode(qml.device("lightning.qubit", wires=1))
      def qnode():
          return qml.state()
      
      @qml.qjit
      def module():
          return qnode()
    • catalyst.passes.apply_pass_plugin can be used on QNodes when the plugin did not define an entry point. In that case the full filesystem path must be specified in addition to the pass name.

      from pathlib import Path
      
      @catalyst.passes.apply_pass_plugin(Path("path_to_plugin"), "pass_name")
      @qml.qnode(qml.device("lightning.qubit", wires=1))
      def qnode():
          return qml.state()
      
      @qml.qjit
      def module():
          return qnode()
    • Alternatively, one or more dialect and pass plugins can be specified in advance in the catalyst.qjit decorator, via the pass_plugins and dialect_plugins keyword arguments. The catalyst.passes.apply_pass function can then be used without specifying the plugin.

      from pathlib import Path
      
      plugin = Path("shared_object_file.so")
      
      @catalyst.passes.apply_pass("pass_name")
      @qml.qnode(qml.device("lightning.qubit", wires=0))
      def qnode():
        qml.Hadamard(wires=0)
        return qml.state()
      
      @qml.qjit(pass_plugins=[plugin], dialect_plugins=[plugin])
      def module():
        return qnode()

    For more information on usage, visit our compiler plugin documentation.

Improvements 🛠

  • The Catalyst CLI, a command line interface for debugging and dissecting different stages of compilation, is now available under the catalyst command after installing Catalyst with pip. Even though the tool was first introduced in v0.9, it was not yet included in binary distributions of Catalyst (wheels). The full usage instructions are available in the Catalyst CLI documentation. (#1285) (#1368) (#1405)

  • Lightning devices now support finite-shot expectation values of qml.Hermitian when used with Catalyst. (#451)

  • The PennyLane state preparation template qml.CosineWindow is now compatible with Catalyst. (#1166)

  • A development distribution of Python with dynamic linking support (libpython.so) is no longer needed in order to use catalyst.debug.compile_executable to generate standalone executables of compiled programs. (#1305)

  • In Catalyst v0.9 the output of the compiler instrumentation (catalyst.debug.instrumentation) had inadvertently been made more verbose by printing timing information for each run of each pass. This change has been reverted. Instead, the catalyst.qjit option verbose=True will now instruct the instrumentation to produce this more detailed output. (#1343)

  • Two additional circuit optimizations have been added to Catalyst: disentangle-CNOT and disentangle-SWAP. The optimizations are available via the catalyst.passes module. (#1154) (#1407)

    The optimizations use a finite state machine to propagate limited qubit state information through the circuit to turn CNOT and SWAP gates into cheaper instructions. The pass is based on the work by J. Liu, L. Bello, and H. Zhou, Relaxed Peephole Optimization: A Novel Compiler Optimization for Quantum Circuits, 2020, arXiv:2012.07711.

Breaking changes 💔

  • The minimum supported PennyLane version has been updated to v0.40; backwards compatibility in either direction is not maintained. (#1308)

  • (Device Developers Only) The way the shots parameter is initialized in C++ device backends is changing. (#1310)

    The previous method of including the shot number in the kwargs argument of the device constructor is deprecated and will be removed in the next release (v0.11). Instead, the shots value will be specified exclusively via the existing SetDeviceShots function called at the beginning of a quantum execution. Device developers are encouraged to update their device implementations between this and the next release while both methods are supported.

    Similarly, the Sample and Counts functions (and their Partial* equivalents) will no longer provide a shots argument, since they are redundant. The signature of these functions will update in the next release.

  • (Device Developers Only) The toml-based device schemas have been integrated with PennyLane and updated to a new version schema = 3. (#1275)

    Devices with existing TOML schema = 2 will not be compatible with the current release of Catalyst until updated. A summary of the most importation changes is listed here:

    • operators.gates.native renamed to operators.gates
    • operators.gates.decomp and operators.gates.matrix are removed and no longer necessary
    • condition property is renamed to conditions
    • Entries in the measurement_processes section now expect the full PennyLane class name as opposed to the deprecated mp.return_type shorthand (e.g. ExpectationMP instead of Expval).
    • The mid_circuit_measurements field has been replaced with supported_mcm_methods, which expects a list of mcm methods that the device is able to work with (or empty if unsupported).
    • A new field has been added, overlapping_observables, which indicates whether a device supports multiple measurements during one execution on overlapping wires.
    • The options section has been removed. Instead, the Python device class should define a device_kwargs field holding the name and values of C++ device constructor kwargs.

    See the Custom Devices page for the most up-to-date information on integrating your device with Catalyst and PennyLane.

Bug fixes 🐛

  • Fixed a bug introduced in Catalyst v0.8 that breaks nested invocations of qml.adjoint and qml.ctrl (e.g. qml.adjoint(qml.adjoint(qml.H(0)))). (#1301)

  • Fixed a bug in catalyst.debug.compile_executable when using non-64bit arrays as input to the compiled function, due to incorrectly computed stride information. (#1338)

Internal changes ⚙️

  • Starting with Python 3.12, Catalyst's binary distributions (wheels) will now follow Python's Stable ABI, eliminating the need for a separate wheel per minor Python version. To enable this, the following changes have made:

    • Stable ABI wheels are now generated for Python 3.12 and up. [(#1357)](#1357 (#1385)

    • Pybind11 has been replaced with nanobind for C++/Python bindings across all components. (#1173) (#1293) (#1391) (#624)

      Nanobind has been developed as a natural successor to the pybind11 library and offers a number of advantages like its ability to target Python's Stable ABI.

    • Python C-API calls have been replaced with functions from Python's Limited API. (#1354)

    • The QuantumExtension module for MLIR Python bindings, which relies on pybind11, has been removed. The module was never included in the distributed wheels and could not be converted to nanobind easily due to its dependency on upstream MLIR code. Pybind11 does not support the Python Stable ABI. (#1187)

  • Catalyst no longer depends on or pins the scipy package. Instead, OpenBLAS is sourced directly from scipy-openblas32 or Accelerate is used. (#1322) [(#1328)](https://github.com/PennyLaneAI/catalys...

Read more

Catalyst v0.9.0

05 Nov 16:58
d809155
Compare
Choose a tag to compare

New features

  • Catalyst now supports the specification of shot-vectors when used with qml.sample measurements on the lightning.qubit device. (#1051)

    Shot-vectors allow shots to be specified as a list of shots, [20, 1, 100], or as a tuple of the form ((num_shots, repetitions), ...) such that ((20, 3), (1, 100)) is equivalent to shots=[20, 20, 20, 1, 1, ..., 1].

    This can result in more efficient quantum execution, as a single job representing the total number of shots is executed on the quantum device, with the measurement post-processing then coarse-grained with respect to the shot-vector.

    For example,

    dev = qml.device("lightning.qubit", wires=1, shots=((5, 2), 7))
    
    @qjit
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(0)
        return qml.sample()
    >>> circuit()
    (Array([[0], [1], [0], [1], [1]], dtype=int64),
    Array([[0], [1], [1], [0], [1]], dtype=int64),
    Array([[1], [0], [1], [1], [0], [1], [0]], dtype=int64))

    Note that other measurement types, such as expval and probs, currently do not support shot-vectors.

  • A new function catalyst.pipeline allows the quantum-circuit-transformation pass pipeline for QNodes within a qjit-compiled workflow to be configured. (#1131) (#1240)

    import pennylane as qml
    from catalyst import pipeline, qjit
    
    my_passes = {
        "cancel_inverses": {},
        "my_circuit_transformation_pass": {"my-option" : "my-option-value"},
    }
    
    dev = qml.device("lightning.qubit", wires=2)
    
    @pipeline(my_passes)
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        return qml.expval(qml.PauliZ(0))
    
    @qjit
    def fn(x):
        return jnp.sin(circuit(x ** 2))

    pipeline can also be used to specify different pass pipelines for different parts of the same qjit-compiled workflow:

    my_pipeline = {
        "cancel_inverses": {},
        "my_circuit_transformation_pass": {"my-option" : "my-option-value"},
    }
    
    my_other_pipeline = {"cancel_inverses": {}}
    
    @qjit
    def fn(x):
        circuit_pipeline = pipeline(my_pipeline)(circuit)
        circuit_other = pipeline(my_other_pipeline)(circuit)
        return jnp.abs(circuit_pipeline(x) - circuit_other(x))

    The pass pipeline order and options can be configured globally for a qjit-compiled function, by using the circuit_transform_pipeline argument of the qjit decorator.

    my_passes = {
        "cancel_inverses": {},
        "my_circuit_transformation_pass": {"my-option" : "my-option-value"},
    }
    
    @qjit(circuit_transform_pipeline=my_passes)
    def fn(x):
        return jnp.sin(circuit(x ** 2))

    Global and local (via @pipeline) configurations can coexist, however local pass pipelines will always take precedence over global pass pipelines.

    The available MLIR passes are listed and documented in the passes module documentation.

  • A peephole merge rotations pass, which acts similarly to the Python-based PennyLane merge rotations transform, is now available in MLIR and can be applied to QNodes within a qjit-compiled function. (#1162) (#1205) (#1206)

    The merge_rotations pass can be provided to the catalyst.pipeline decorator:

    from catalyst import pipeline, qjit
    
    my_passes = {
        "merge_rotations": {}
    }
    
    dev = qml.device("lightning.qubit", wires=1)
    
    @qjit
    @pipeline(my_passes)
    @qml.qnode(dev)
    def g(x: float):
        qml.RX(x, wires=0)
        qml.RX(x, wires=0)
        qml.Hadamard(wires=0)
        return qml.expval(qml.PauliX(0))

    It can also be applied directly to qjit-compiled QNodes via the catalyst.passes.merge_rotations Python decorator:

    from catalyst.passes import merge_rotations
    
    @qjit
    @merge_rotations
    @qml.qnode(dev)
    def g(x: float):
        qml.RX(x, wires=0)
        qml.RX(x, wires=0)
        qml.Hadamard(wires=0)
        return qml.expval(qml.PauliX(0))
  • Static arguments of a qjit-compiled function can now be indicated by name via a static_argnames argument to the qjit decorator. (#1158)

    Specified static argument names will be treated as compile-time static values, allowing any hashable Python object to be passed to this function argument during compilation.

    >>> @qjit(static_argnames="y")
    ... def f(x, y):
    ...     print(f"Compiling with y={y}")
    ...     return x + y
    >>> f(0.5, 0.3)
    Compiling with y=0.3

    The function will only be re-compiled if the hash values of the static arguments change. Otherwise, re-using previous static argument values will result in no re-compilation:

    Array(0.8, dtype=float64)
    >>> f(0.1, 0.3)  # no re-compilation occurs
    Array(0.4, dtype=float64)
    >>> f(0.1, 0.4)  # y changes, re-compilation
    Compiling with y=0.4
    Array(0.5, dtype=float64)
  • Catalyst Autograph now supports updating a single index or a slice of JAX arrays using Python's array assignment operator syntax. (#769) (#1143)

    Using operator assignment syntax in favor of at...op expressions is now possible for the following operations:

    • x[i] += y in favor of x.at[i].add(y)
    • x[i] -= y in favor of x.at[i].add(-y)
    • x[i] *= y in favor of x.at[i].multiply(y)
    • x[i] /= y in favor of x.at[i].divide(y)
    • x[i] **= y in favor of x.at[i].power(y)
    @qjit(autograph=True)
    def f(x):
        first_dim = x.shape[0]
        result = jnp.copy(x)
    
        for i in range(first_dim):
          result[i] *= 2  # This is now supported
    
        return result
    >>> f(jnp.array([1, 2, 3]))
    Array([2, 4, 6], dtype=int64)
  • Catalyst now has a standalone compiler tool called catalyst-cli that quantum-compiles MLIR input files into an object file independent of the Python frontend. (#1208) (#1255)

    This compiler tool combines three stages of compilation:

    1. quantum-opt: Performs the MLIR-level optimizations and lowers the input dialect to the LLVM dialect.
    2. mlir-translate: Translates the input in the LLVM dialect into LLVM IR.
    3. llc: Performs lower-level optimizations and creates the object file.

    catalyst-cli runs all three stages under the hood by default, but it also has the ability to run each stage individually. For example:

    # Creates both the optimized IR and an object file
    catalyst-cli input.mlir -o output.o
    
    # Only performs MLIR optimizations
    catalyst-cli --tool=opt input.mlir -o llvm-dialect.mlir
    
    # Only lowers LLVM dialect MLIR input to LLVM IR
    catalyst-cli --tool=translate llvm-dialect.mlir -o llvm-ir.ll
    
    # Only performs lower-level optimizations and creates object file
    catalyst-cli --tool=llc llvm-ir.ll -o output.o

    Note that catalyst-cli is only available when Catalyst is built from source, and is not included when installing Catalyst via pip or from wheels.

  • Experimental integration of the PennyLane capture module is available. It currently only supports quantum gates, without control flow. (#1109)

    To trigger the PennyLane pipeline for capturing the program as a Jaxpr, simply set experimental_capture=True in the qjit decorator.

    import pennylane as qml
    from catalyst import qjit
    
    dev = qml.device("lightning.qubit", wires=1)
    
    @qjit(experimental_capture=True)
    @qml.qnode(dev)
    def circuit():
        qml.Hadamard(0)
        qml.CNOT([0, 1])
        return qml.expval(qml.Z(0))

Improvements

  • Multiple qml.sample calls can now be returned from the same program, and can be structured using Python containers. For example, a program can return a dictionary of the form return {"first": qml.sample(), "second": qml.sample()}. (#1051)

  • Catalyst now ships with null.qubit, a Catalyst runtime plugin that mocks out all functions in the QuantumDevice interface. This device is provided as a convenience for testing and benchmarking purposes. (#1179)

    qml.device("null.qubit", wires=1)
    
    @qml.qjit
    @qml.qnode(dev)
    def g(x):
        qml.RX(x, wires=0)
        return qml.probs(wires=[0])
  • Setting the seed argument in the qjit decorator will now seed sampled results, in addition to mid-circuit measurement results. (#1164)

    dev = qml.device("lightning.qubit", wires=1, shots=10)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        m = catalyst.measure(0)
    
        if m:
            qml.Hadamard(0)
    
        return qml.sample()
    
    @qml.qjit(seed=37, autograph=True)
    def workflow(x):
        return jnp.squeeze(jnp.stack([circuit(x) for i in range(4)]))
    >>> workflow(1.8)
    Array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
           [1, 1, 0, 0, 1, 1, 0, 0, 1, 0],
           [0, 0, 1, 0, 1, 1, 0, 0, 1, 1],
           [1, 1, 1, 0, 0, 1, 1, 0, 1, 1]], dtype=int64)
    >>> workf...
Read more

Catalyst v0.8.1

12 Sep 02:11
2c45243
Compare
Choose a tag to compare

New features

  • The catalyst.mitigate_with_zne error mitigation compilation pass now supports the option to fold gates locally as well as the existing method of globally. (#1006) (#1129)

    While global folding applies the scale factor by forming the inverse of the entire quantum circuit (without measurements) and repeating the circuit with its inverse, local folding instead inserts per-gate folding sequences directly in place of each gate in the original circuit.

    For example,

    import jax
    import pennylane as qml
    from catalyst import qjit, mitigate_with_zne
    from pennylane.transforms import exponential_extrapolate
    
    dev = qml.device("lightning.qubit", wires=4, shots=5)
    
    @qml.qnode(dev)
    def circuit():
      qml.Hadamard(wires=0)
      qml.CNOT(wires=[0, 1])
      return qml.expval(qml.PauliY(wires=0))
    
    @qjit(keep_intermediate=True)
    def mitigated_circuit():
      s = jax.numpy.array([1, 2, 3])
      return mitigate_with_zne(
        circuit,
        scale_factors=s,
        extrapolate=exponential_extrapolate,
        folding="local-all" # "local-all" for local on all gates or "global" for the original method (default being "global")
      )()
    >>> circuit()
    >>> mitigated_circuit()

Improvements

Breaking changes

  • The argument scale_factors of mitigate_with_zne function now follows the proper literature definition. It now needs to be a list of positive odd integers, as we don't support the fractional part. (#1120)

Bug fixes

  • Those functions calling the gather_p primitive (like jax.scipy.linalg.expm) can now be used in multiple qjits in a single program. (#1096)

Contributors

This release contains contributions from (in alphabetical order):

Joey Carter,
Alessandro Cosentino,
Paul Haochen Wang,
David Ittah,
Romain Moyard,
Daniel Strano,
Raul Torres.

Catalyst v0.8.0

03 Sep 21:36
2cb2fc8
Compare
Choose a tag to compare

New features

  • JAX-compatible functions that run on classical accelerators, such as GPUs, via catalyst.accelerate now support autodifferentiation. (#920)

    For example,

    from catalyst import qjit, grad
    
    @qjit
    @grad
    def f(x):
        expm = catalyst.accelerate(jax.scipy.linalg.expm)
        return jnp.sum(expm(jnp.sin(x)) ** 2)
    >>> x = jnp.array([[0.1, 0.2], [0.3, 0.4]])
    >>> f(x)
    Array([[2.80120452, 1.67518663],
           [1.61605839, 4.42856163]], dtype=float64)
  • Assertions can now be raised at runtime via the catalyst.debug_assert function. (#925)

    Python-based exceptions (via raise) and assertions (via assert) will always be evaluated at program capture time, before certain runtime information may be available.

    Use debug_assert to instead raise assertions at runtime, including assertions that depend on values of dynamic variables.

    For example,

    from catalyst import debug_assert
    
    @qjit
    def f(x):
        debug_assert(x < 5, "x was greater than 5")
        return x * 8
    >>> f(4)
    Array(32, dtype=int64)
    >>> f(6)
    RuntimeError: x was greater than 5

    Assertions can be disabled globally for a qjit-compiled function via the disable_assertions keyword argument:

    @qjit(disable_assertions=True)
    def g(x):
        debug_assert(x < 5, "x was greater than 5")
        return x * 8
    >>> g(6)
    Array(48, dtype=int64)
  • Mid-circuit measurement results when using lightning.qubit and lightning.kokkos can now be seeded via the new seed argument of the qjit decorator. (#936)

    The seed argument accepts an unsigned 32-bit integer, which is used to initialize the pseudo-random state at the beginning of each execution of the compiled function. Therefor, different qjit objects with the same seed (including repeated calls to the same qjit) will always return the same sequence of mid-circuit measurement results.

    dev = qml.device("lightning.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        m = measure(0)
    
        if m:
            qml.Hadamard(0)
    
        return qml.probs()
    
    @qjit(seed=37, autograph=True)
    def workflow(x):
        return jnp.stack([circuit(x) for i in range(4)])

    Repeatedly calling the workflow function above will always result in the same values:

    >>> workflow(1.8)
    Array([[1. , 0. ],
         [1. , 0. ],
         [1. , 0. ],
         [0.5, 0.5]], dtype=float64)
    >>> workflow(1.8)
    Array([[1. , 0. ],
         [1. , 0. ],
         [1. , 0. ],
         [0.5, 0.5]], dtype=float64)

    Note that setting the seed will not avoid shot-noise stochasticity in terminal measurement statistics such as sample or expval:

    dev = qml.device("lightning.qubit", wires=1, shots=10)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        m = measure(0)
    
        if m:
            qml.Hadamard(0)
    
        return qml.expval(qml.PauliZ(0))
    
    @qjit(seed=37, autograph=True)
    def workflow(x):
        return jnp.stack([circuit(x) for i in range(4)])
    >>> workflow(1.8)
    Array([1. , 1. , 1. , 0.4], dtype=float64)
    >>> workflow(1.8)
    Array([ 1. ,  1. ,  1. , -0.2], dtype=float64)
  • Exponential fitting is now a supported method of zero-noise extrapolation when performing error mitigation in Catalyst using mitigate_with_zne. (#953)

    This new functionality fits the data from noise-scaled circuits with an exponential function, and returns the zero-noise value:

    from pennylane.transforms import exponential_extrapolate
    from catalyst import mitigate_with_zne
    
    dev = qml.device("lightning.qubit", wires=2, shots=100000)
    
    @qml.qnode(dev)
    def circuit(weights):
        qml.StronglyEntanglingLayers(weights, wires=[0, 1])
        return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    
    @qjit
    def workflow(weights, s):
        zne_circuit = mitigate_with_zne(circuit, scale_factors=s, extrapolate=exponential_extrapolate)
        return zne_circuit(weights)
    >>> weights = jnp.ones([3, 2, 3])
    >>> scale_factors = jnp.array([1, 2, 3])
    >>> workflow(weights, scale_factors)
    Array(-0.19946598, dtype=float64)
  • A new module is available, catalyst.passes, which provides Python decorators for enabling and configuring Catalyst MLIR compiler passes. (#911) (#1037)

    The first pass available is catalyst.passes.cancel_inverses, which enables the -removed-chained-self-inverse MLIR pass that cancels two neighbouring Hadamard gates.

    from catalyst.debug import get_compilation_stage
    from catalyst.passes import cancel_inverses
    
    dev = qml.device("lightning.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(x: float):
        qml.RX(x, wires=0)
        qml.Hadamard(wires=0)
        qml.Hadamard(wires=0)
        return qml.expval(qml.PauliZ(0))
    
    @qjit(keep_intermediate=True)
    def workflow(x):
        optimized_circuit = cancel_inverses(circuit)
        return circuit(x), optimized_circuit(x)
  • Catalyst now has debug functions get_compilation_stage and replace_ir to acquire and recompile the IR from a given pipeline pass for functions compiled with keep_intermediate=True. (#981)

    For example, consider the following function:

    @qjit(keep_intermediate=True)
    def f(x):
        return x**2
    >>> f(2.0)
    4.0

    Here we use get_compilation_stage to acquire the IR, and then modify %2 = arith.mulf %in, %in_0 : f64 to turn the square function into a cubic one via replace_ir:

    from catalyst.debug import get_compilation_stage, replace_ir
    
    old_ir = get_compilation_stage(f, "HLOLoweringPass")
    new_ir = old_ir.replace(
        "%2 = arith.mulf %in, %in_0 : f64\n",
        "%t = arith.mulf %in, %in_0 : f64\n    %2 = arith.mulf %t, %in_0 : f64\n"
    )
    replace_ir(f, "HLOLoweringPass", new_ir)

    The recompilation starts after the given checkpoint stage:

    >>> f(2.0)
    8.0

    Either function can also be used independently of each other. Note that get_compilation_stage replaces the print_compilation_stage function; please see the Breaking Changes section for more details.

  • Catalyst now supports generating executables from compiled functions for the native host architecture using catalyst.debug.compile_executable. (#1003)

    >>> @qjit
    ... def f(x):
    ...     y = x * x
    ...     catalyst.debug.print_memref(y)
    ...     return y
    >>> f(5)
    MemRef: base@ = 0x31ac22580 rank = 0 offset = 0 sizes = [] strides = [] data =
    25
    Array(25, dtype=int64)

    We can use compile_executable to compile this function to a binary:

    >>> from catalyst.debug import compile_executable
    >>> binary = compile_executable(f, 5)
    >>> print(binary)
    /path/to/executable

    Executing this function from a shell environment:

    $ /path/to/executable
    MemRef: base@ = 0x64fc9dd5ffc0 rank = 0 offset = 0 sizes = [] strides = [] data =
    25

Improvements

  • Catalyst has been updated to work with JAX v0.4.28 (exact version match required). (#931) (#995)

  • Catalyst now supports keyword arguments for qjit-compiled functions. (#1004)

    >>> @qjit
    ... @grad
    ... def f(x, y):
    ...     return x * y
    >>> f(3., y=2.)
    Array(2., dtype=float64)

    Note that the static_argnums argument to the qjit decorator is not supported when passing argument values as keyword arguments.

  • Support has been added for the jax.numpy.argsort function within qjit-compiled functions. (#901)

  • Autograph now supports in-place array assignments with static slices. (#843)

    For example,

    @qjit(autograph=True)
    def f(x, y):
        y[1:10:2] = x
        return y
    >>> f(jnp.ones(5), jnp.zeros(10))
    Array([0., 1., 0., 1., 0., 1., 0., 1., 0., 1.], dtype=float64)
  • Autograph now works when qjit is applied to a function decorated with vmap, cond, for_loop or while_loop. Previously, stacking the autograph-enabled qjit decorator directly on top of other Catalyst decorators would lead to errors. (#835) (#938) (#942)

    from catalyst import vmap, qjit
    
    dev = qml.device("lightning.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        return qml.expval(qml.PauliZ(0))
    >>> x = jnp.array([0.1, 0.2, 0.3])
    >>> qjit(vmap(circuit), autograph=True)(x)
    Array([0.99500417, 0.98006658, 0.95533649], dtype=float64)
  • Runtime memory usage, and compilation complexity, has been reduced by eliminating some scalar tensors from the IR. This has been done by adding a linalg-detensorize pass at the end of the HLO lowering pipeline. (#1010)

  • Program verification is exte...

Read more

Catalyst v0.7.0

08 Jul 19:45
0b8213d
Compare
Choose a tag to compare

New features

  • Add support for accelerating classical processing via JAX with catalyst.accelerate. (#805)

    Classical code that can be just-in-time compiled with JAX can now be seamlessly executed on GPUs or other accelerators with catalyst.accelerate, right inside of QJIT-compiled functions.

    @accelerate(dev=jax.devices("gpu")[0])
    def classical_fn(x):
        return jnp.sin(x) ** 2
    
    @qjit
    def hybrid_fn(x):
        y = classical_fn(jnp.sqrt(x)) # will be executed on a GPU
        return jnp.cos(y)

    Available devices can be retrieved via jax.devices(). If not provided, the default value of jax.devices()[0] as determined by JAX will be used.

  • Catalyst callback functions, such as pure_callback, debug.callback, and debug.print, now all support auto-differentiation. (#706) (#782) (#822) (#834) (#882) (#907)

    • When using callbacks that do not return any values, such as catalyst.debug.callback and catalyst.debug.print, these functions are marked as 'inactive' and do not contribute to or affect the derivative of the function:

      import logging
      
      log = logging.getLogger(__name__)
      log.setLevel(logging.INFO)
      
      @qml.qjit
      @catalyst.grad
      def f(x):
          y = jnp.cos(x)
          catalyst.debug.print("Debug print: y = {0:.4f}", y)
          catalyst.debug.callback(lambda _: log.info("Value of y = %s", _))(y)
          return y ** 2
      >>> f(0.54)
      INFO:__main__:Value of y = 0.8577086813638242
      Debug print: y = 0.8577
      array(-0.88195781)
    • Callbacks that do return values and may affect the qjit-compiled functions computation, such as pure_callback, may have custom derivatives manually registered with the Catalyst compiler in order to support differentiation.

      This can be done via the pure_callback.fwd and pure_callback.bwd methods, to specify how the forwards and backwards pass (the vector-Jacobian product) of the callback should be computed:

      @catalyst.pure_callback
      def callback_fn(x) -> float:
          return np.sin(x[0]) * x[1]
      
      @callback_fn.fwd
      def callback_fn_fwd(x):
          # returns the evaluated function as well as residual
          # values that may be useful for the backwards pass
          return callback_fn(x), x
      
      @callback_fn.bwd
      def callback_fn_vjp(res, dy):
          # Accepts residuals from the forward pass, as well
          # as (one or more) cotangent vectors dy, and returns
          # a tuple of VJPs corresponding to each input parameter.
      
          def vjp(x, dy) -> (jax.ShapeDtypeStruct((2,), jnp.float64),):
              return (np.array([np.cos(x[0]) * dy * x[1], np.sin(x[0]) * dy]),)
      
          # The VJP function can also be a pure callback
          return catalyst.pure_callback(vjp)(res, dy)
      
      @qml.qjit
      @catalyst.grad
      def f(x):
          y = jnp.array([jnp.cos(x[0]), x[1]])
          return jnp.sin(callback_fn(y))
      >>> x = jnp.array([0.1, 0.2])
      >>> f(x)
      array([-0.01071923,  0.82698717])
  • Catalyst now supports the 'dynamic one shot' method for simulating circuits with mid-circuit measurements, which compared to other methods, may be advantageous for circuits with many mid-circuit measurements executed for few shots. (#5617) (#798)

    The dynamic one shot method evaluates dynamic circuits by executing them one shot at a time via catalyst.vmap, sampling a dynamic execution path for each shot. This method only works for a QNode executing with finite shots, and it requires the device to support mid-circuit measurements natively.

    This new mode can be specified by using the mcm_method argument of the QNode:

    dev = qml.device("lightning.qubit", wires=5, shots=20)
    
    @qml.qjit(autograph=True)
    @qml.qnode(dev, mcm_method="one-shot")
    def circuit(x):
    
        for i in range(10):
            qml.RX(x, 0)
            m = catalyst.measure(0)
    
            if m:
                qml.RY(x ** 2, 1)
    
            x = jnp.sin(x)
    
        return qml.expval(qml.Z(1))

    Catalyst's existing method for simulating mid-circuit measurements remains available via mcm_method="single-branch-statistics".

    When using mcm_method="one-shot", the postselect_mode keyword argument can also be used to specify whether the returned result should include shots-number of postselected measurements ("fill-shots"), or whether results should include all results, including invalid postselections ("hw_like"):

    @qml.qjit
    @qml.qnode(dev, mcm_method="one-shot", postselect_mode="hw-like")
    def func(x):
        qml.RX(x, wires=0)
        m_0 = catalyst.measure(0, postselect=1)
        return qml.sample(wires=0)
    >>> res = func(0.9)
    >>> res
    array([-2147483648, -2147483648,           1, -2147483648, -2147483648,
           -2147483648, -2147483648,           1, -2147483648, -2147483648,
           -2147483648, -2147483648,           1, -2147483648, -2147483648,
           -2147483648, -2147483648, -2147483648, -2147483648, -2147483648])
    >>> jnp.delete(res, jnp.where(res == np.iinfo(np.int32).min)[0])
    Array([1, 1, 1], dtype=int64)

    Note that invalid shots will not be discarded, but will be replaced by np.iinfo(np.int32).min They will not be used for processing final results (like expectation values), but they will appear in the output of QNodes that return samples directly.

    For more details, see the dynamic quantum circuit documentation.

  • Catalyst now has support for returning qml.sample(m) where m is the result of a mid-circuit measurement. (#731)

    When used with mcm_method="one-shot", this will return an array with one measurement result for each shot:

    dev = qml.device("lightning.qubit", wires=2, shots=10)
    
    @qml.qjit
    @qml.qnode(dev, mcm_method="one-shot")
    def func(x):
        qml.RX(x, wires=0)
        m = catalyst.measure(0)
        qml.RX(x ** 2, wires=0)
        return qml.sample(m), qml.expval(qml.PauliZ(0))
    >>> func(0.9)
    (array([0, 1, 0, 0, 0, 0, 1, 0, 0, 0]), array(0.4))

    In mcm_method="single-branch-statistics" mode, it will be equivalent to returning m directly from the quantum function --- that is, it will return a single boolean corresponding to the measurement in the branch selected:

    @qml.qjit
    @qml.qnode(dev, mcm_method="single-branch-statistics")
    def func(x):
        qml.RX(x, wires=0)
        m = catalyst.measure(0)
        qml.RX(x ** 2, wires=0)
        return qml.sample(m), qml.expval(qml.PauliZ(0))
    >>> func(0.9)
    (array(False), array(0.8))
  • A new function, catalyst.value_and_grad, returns both the result of a function and its gradient with a single forward and backwards pass. (#804) (#859)

    This can be more efficient, and reduce overall quantum executions, compared to separately executing the function and then computing its gradient.

    For example:

    dev = qml.device("lightning.qubit", wires=3)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        qml.CNOT(wires=[0, 1])
        qml.RX(x, wires=2)
        return qml.probs()
    
    @qml.qjit
    @catalyst.value_and_grad
    def cost(x):
        return jnp.sum(jnp.cos(circuit(x)))
    >>> cost(0.543)
    (array(7.64695856), array(0.33413963))
  • Autograph now supports single index JAX array assignment (#717)

    When using Autograph, syntax of the form x[i] = y where i is a single integer will now be automatically converted to the JAX equivalent of x = x.at(i).set(y):

    @qml.qjit(autograph=True)
    def f(array):
        result = jnp.ones(array.shape, dtype=array.dtype)
    
        for i, x in enumerate(array):
            result[i] = result[i] + x * 3
    
        return result
    >>> f(jnp.array([-0.1, 0.12, 0.43, 0.54]))
    array([0.7 , 1.36, 2.29, 2.62])
  • Catalyst now supports dynamically-shaped arrays in control-flow primitives. Arrays with dynamic shapes can now be used with for_loop, while_loop, and cond primitives. (#775) (#777) (#830)

    @qjit
    def f(shape):
        a = jnp.ones([shape], dtype=float)
    
        @for_loop(0, 10, 2)
        def loop(i, a):
            return a + i
    
        return loop(a)
    >>> f(3)
    array([21., 21., 21.])
  • Support has been added for disabling Autograph for specific functions. (#705) (#710)

    The decorator catalyst.disable_autograph allows one to disable Autograph from auto-converting specific external functions when called within a qjit-compiled function with autograph=True:

    def approximate_e(n):
        num = 1.
        fac = 1.
        for i in range(1, n + 1):
            fac *= i
            num += 1. / fac
     ...
Read more

Catalyst v0.6.0

06 May 19:27
73bd0bc
Compare
Choose a tag to compare

New features

  • Catalyst now supports externally hosted callbacks with parameters and return values within qjit-compiled code. This provides the ability to insert native Python code into any qjit-compiled function, allowing for the capability to include subroutines that do not yet support qjit-compilation and enhancing the debugging experience. (#540) (#596) (#610) (#650) (#649) (#661) (#686) (#689)

    The following two callback functions are available:

    • catalyst.pure_callback supports callbacks of pure functions. That is, functions with no side-effects that accept parameters and return values. However, the return type and shape of the function must be known in advance, and is provided as a type signature.

      @pure_callback
      def callback_fn(x) -> float:
          # here we call non-JAX compatible code, such
          # as standard NumPy
          return np.sin(x)
      
      @qjit
      def fn(x):
          return jnp.cos(callback_fn(x ** 2))
      >>> fn(0.654)
      array(0.9151995)
    • catalyst.debug.callback supports callbacks of functions with no return values. This makes it an easy entry point for debugging, for example via printing or logging at runtime.

      @catalyst.debug.callback
      def callback_fn(y):
          print("Value of y =", y)
      
      @qjit
      def fn(x):
          y = jnp.sin(x)
          callback_fn(y)
          return y ** 2
      >>> fn(0.54)
      Value of y = 0.5141359916531132
      array(0.26433582)
      >>> fn(1.52)
      Value of y = 0.998710143975583
      array(0.99742195)

    Note that callbacks do not currently support differentiation, and cannot be used inside functions that catalyst.grad is applied to.

  • More flexible runtime printing through support for format strings. (#621)

    The catalyst.debug.print function has been updated to support Python-like format strings:

    @qjit
    def cir(a, b, c):
        debug.print("{c} {b} {a}", a=a, b=b, c=c)
    >>> cir(1, 2, 3)
    3 2 1

    Note that previous functionality of the print function to print out memory reference information of variables has been moved to catalyst.debug.print_memref.

  • Catalyst now supports QNodes that execute on Oxford Quantum Circuits (OQC) superconducting hardware, via OQC Cloud. (#578) (#579) (#691)

    To use OQC Cloud with Catalyst, simply ensure your credentials are set as environment variables, and load the oqc.cloud device to be used within your qjit-compiled workflows.

    import os
    os.environ["OQC_EMAIL"] = "your_email"
    os.environ["OQC_PASSWORD"] = "your_password"
    os.environ["OQC_URL"] = "oqc_url"
    
    dev = qml.device("oqc.cloud", backend="lucy", shots=2012, wires=2)
    
    @qjit
    @qml.qnode(dev)
    def circuit(a: float):
        qml.Hadamard(0)
        qml.CNOT(wires=[0, 1])
        qml.RX(wires=0)
        return qml.counts(wires=[0, 1])
    
    print(circuit(0.2))
  • Catalyst now ships with an instrumentation feature allowing to explore what steps are run during compilation and execution, and for how long. (#528) (#597)

    Instrumentation can be enabled from the frontend with the catalyst.debug.instrumentation context manager:

    >>> @qjit
    ... def expensive_function(a, b):
    ...     return a + b
    >>> with debug.instrumentation("session_name", detailed=False):
    ...     expensive_function(1, 2)
    [DIAGNOSTICS] Running capture                   walltime: 3.299 ms      cputime: 3.294 ms       programsize: 0 lines
    [DIAGNOSTICS] Running generate_ir               walltime: 4.228 ms      cputime: 4.225 ms       programsize: 14 lines
    [DIAGNOSTICS] Running compile                   walltime: 57.182 ms     cputime: 12.109 ms      programsize: 121 lines
    [DIAGNOSTICS] Running run                       walltime: 1.075 ms      cputime: 1.072 ms  

    The results will be appended to the provided file if the filename attribute is set, and printed to the console otherwise. The flag detailed determines whether individual steps in the compiler and runtime are instrumented, or whether only high-level steps like "program capture" and "compilation" are reported.

    Measurements currently include wall time, CPU time, and (intermediate) program size.

Improvements

  • AutoGraph now supports return statements inside conditionals in qjit-compiled functions. (#583)

    For example, the following pattern is now supported, as long as all return values have the same type:

    @qjit(autograph=True)
    def fn(x):
        if x > 0:
            return jnp.sin(x)
        return jnp.cos(x)
    >>> fn(0.1)
    array(0.09983342)
    >>> fn(-0.1)
    array(0.99500417)

    This support extends to quantum circuits:

    dev = qml.device("lightning.qubit", wires=1)
    
    @qjit(autograph=True)
    @qml.qnode(dev)
    def f(x: float):
      qml.RX(x, wires=0)
    
      m = catalyst.measure(0)
    
      if not m:
          return m, qml.expval(qml.PauliZ(0))
    
      qml.RX(x ** 2, wires=0)
    
      return m, qml.expval(qml.PauliZ(0))
    >>> f(1.4)
    (array(False), array(1.))
    >>> f(1.4)
    (array(True), array(0.37945176))

    Note that returning results with different types or shapes within the same function, such as different observables or differently shaped arrays, is not possible.

  • Errors are now raised at compile time if the gradient of an unsupported function is requested. (#204)

    At the moment, CompileError exceptions will be raised if at compile time it is found that code reachable from the gradient operation contains either a mid-circuit measurement, a callback, or a JAX-style custom call (which happens through the mitigation operation as well as certain JAX operations).

  • Catalyst now supports devices built from the new PennyLane device API. (#565) (#598) (#599) (#636) (#638) (#664) (#687)

    When using the new device API, Catalyst will discard the preprocessing from the original device, replacing it with Catalyst-specific preprocessing based on the TOML file provided by the device. Catalyst also requires that provided devices specify their wires upfront.

  • A new compiler optimization that removes redundant chains of self inverse operations has been added. This is done within a new MLIR pass called remove-chained-self-inverse. Currently we only match redundant Hadamard operations, but the list of supported operations can be expanded. (#630)

  • The catalyst.measure operation is now more lenient in the accepted type for the wires parameter. In addition to a scalar, a 1D array is also accepted as long as it only contains one element. (#623)

    For example, the following is now supported:

    catalyst.measure(wires=jnp.array([0]))
  • The compilation & execution of @qjit compiled functions can now be aborted using an interrupt signal (SIGINT). This includes using CTRL-C from a command line and the Interrupt button in a Jupyter Notebook. (#642)

  • The Catalyst Amazon Braket support has been updated to work with the latest version of the Amazon Braket PennyLane plugin (v1.25.0) and Amazon Braket Python SDK (v1.73.3) (#620) (#672) (#673)

    Note that with this update, all declared qubits in a submitted program will always be measured, even if specific qubits were never used.

  • An updated quantum device specification format, TOML schema v2, is now supported by Catalyst. This allows device authors to specify properties such as native quantum control support, gate invertibility, and differentiability on a per-operation level. (#554)

    For more details on the new TOML schema, please refer to the custom devices documentation.

  • An exception is now raised when OpenBLAS cannot be found by Catalyst during compilation. (#643)

Breaking changes

  • qml.sample and qml.counts now produce integer arrays for the sample array and basis state array when used without obser...
Read more

Catalyst v0.5.0

04 Mar 22:56
e941350
Compare
Choose a tag to compare

New features

  • Catalyst now provides a QJIT compatible catalyst.vmap function, which makes it even easier to modify functions to map over inputs with additional batch dimensions. (#497) (#569)

    When working with tensor/array frameworks in Python, it can be important to ensure that code is written to minimize usage of Python for loops (which can be slow and inefficient), and instead push as much of the computation through to the array manipulation library, by taking advantage of extra batch dimensions.

    For example, consider the following QNode:

    dev = qml.device("lightning.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(x, y):
        qml.RX(jnp.pi * x[0] + y, wires=0)
        qml.RY(x[1] ** 2, wires=0)
        qml.RX(x[1] * x[2], wires=0)
        return qml.expval(qml.PauliZ(0))
    >>> circuit(jnp.array([0.1, 0.2, 0.3]), jnp.pi)
    Array(-0.93005586, dtype=float64)

    We can use catalyst.vmap to introduce additional batch dimensions to our input arguments, without needing to use a Python for loop:

    >>> x = jnp.array([[0.1, 0.2, 0.3],
    ...                [0.4, 0.5, 0.6],
    ...                [0.7, 0.8, 0.9]])
    >>> y = jnp.array([jnp.pi, jnp.pi / 2, jnp.pi / 4])
    >>> qjit(vmap(cost))(x, y)
    array([-0.93005586, -0.97165424, -0.6987465 ])

    catalyst.vmap() has been implemented to match the same behaviour of jax.vmap, so should be a drop-in replacement in most cases. Under-the-hood, it is automatically inserting Catalyst-compatible for loops, which will be compiled and executed outside of Python for increased performance.

  • Catalyst now supports compiling and executing QJIT-compiled QNodes using the CUDA Quantum compiler toolchain. (#477) (#536) (#547)

    Simply import the CUDA Quantum @cudaqjit decorator to use this functionality:

    from catalyst.cuda import cudaqjit

    Or, if using Catalyst from PennyLane, simply specify @qml.qjit(compiler="cuda_quantum").

    The following devices are available when compiling with CUDA Quantum:

    • softwareq.qpp: a modern C++ statevector simulator
    • nvidia.custatevec: The NVIDIA CuStateVec GPU simulator (with support for multi-gpu)
    • nvidia.cutensornet: The NVIDIA CuTensorNet GPU simulator (with support for matrix product state)

    For example:

    dev = qml.device("softwareq.qpp", wires=2)
    
    @cudaqjit
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x[0], wires=0)
        qml.RY(x[1], wires=1)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliY(0))
    >>> circuit(jnp.array([0.5, 1.4]))
    -0.47244976756708373

    Note that CUDA Quantum compilation currently does not have feature parity with Catalyst compilation; in particular, AutoGraph, control flow, differentiation, and various measurement statistics (such as probabilities and variance) are not yet supported. Classical code support is also limited.

  • Catalyst now supports just-in-time compilation of static (compile-time constant) arguments. (#476) (#550)

    The @qjit decorator takes a new argument static_argnums, which specifies positional arguments of the decorated function should be treated as compile-time static arguments.

    This allows any hashable Python object to be passed to the function during compilation; the function will only be re-compiled if the hash value of the static arguments change. Otherwise, re-using previous static argument values will result in no re-compilation.

    @qjit(static_argnums=(1,))
    def f(x, y):
        print(f"Compiling with y={y}")
        return x + y
    >>> f(0.5, 0.3)
    Compiling with y=0.3
    array(0.8)
    >>> f(0.1, 0.3)  # no re-compilation occurs
    array(0.4)
    >>> f(0.1, 0.4)  # y changes, re-compilation
    Compiling with y=0.4
    array(0.5)

    This functionality can be used to support passing arbitrary Python objects to QJIT-compiled functions, as long as they are hashable:

    from dataclasses import dataclass
    
    @dataclass
    class MyClass:
        val: int
    
        def __hash__(self):
            return hash(str(self))
    
    @qjit(static_argnums=(1,))
    def f(x: int, y: MyClass):
        return x + y.val
    >>> f(1, MyClass(5))
    array(6)
    >>> f(1, MyClass(6))  # re-compilation
    array(7)
    >>> f(2, MyClass(5))  # no re-compilation
    array(7)
  • Mid-circuit measurements now support post-selection and qubit reset when used with the Lightning simulators. (#491) (#507)

    To specify post-selection, simply pass the postselect argument to the catalyst.measure function:

    dev = qml.device("lightning.qubit", wires=1)
    
    @qjit
    @qml.qnode(dev)
    def f():
        qml.Hadamard(0)
        m = measure(0, postselect=1)
        return qml.expval(qml.PauliZ(0))

    Likewise, to reset a wire after mid-circuit measurement, simply specify reset=True:

    dev = qml.device("lightning.qubit", wires=1)
    
    @qjit
    @qml.qnode(dev)
    def f():
        qml.Hadamard(0)
        m = measure(0, reset=True)
        return qml.expval(qml.PauliZ(0))

Improvements

  • Catalyst now supports Python 3.12 (#532)

  • The JAX version used by Catalyst has been updated to v0.4.23. (#428)

  • Catalyst now supports the qml.GlobalPhase operation. (#563)

  • Native support for qml.PSWAP and qml.ISWAP gates on Amazon Braket devices has been added. (#458)

    Specifically, a circuit like

    dev = qml.device("braket.local.qubit", wires=2, shots=100)
    
    @qjit
    @qml.qnode(dev)
    def f(x: float):
        qml.Hadamard(0)
        qml.PSWAP(x, wires=[0, 1])
        qml.ISWAP(wires=[1, 0])
        return qml.probs()

    would no longer decompose the PSWAP and ISWAP gates.

  • The qml.BlockEncode operator is now supported with Catalyst. (#483)

  • Catalyst no longer relies on a TensorFlow installation for its AutoGraph functionality. Instead, the standalone diastatic-malt package is used and automatically installed as a dependency. (#401)

  • The @qjit decorator will remember previously compiled functions when the PyTree metadata of arguments changes, in addition to also remembering compiled functions when static arguments change. (#522)

    The following example will no longer trigger a third compilation:

    @qjit
    def func(x):
        print("compiling")
        return x
    >>> func([1,]);             # list
    compiling
    >>> func((2,));             # tuple
    compiling
    >>> func([3,]);             # list

    Note however that in order to keep overheads low, changing the argument type or shape (in a promotion incompatible way) may override a previously stored function (with identical PyTree metadata and static argument values):

    @qjit
    def func(x):
        print("compiling")
        return x
    >>> func(jnp.array(1));     # scalar
    compiling
    >>> func(jnp.array([2.]));  # 1-D array
    compiling
    >>> func(jnp.array(3));     # scalar
    compiling
  • Catalyst gradient functions (grad, jacobian, vjp, and jvp) now support being applied to functions that use (nested) container types as inputs and outputs. This includes lists and dictionaries, as well as any data structure implementing the PyTree protocol. (#500) (#501) (#508) (#549)

    dev = qml.device("lightning.qubit", wires=1)
    
    @qml.qnode(dev)
    def circuit(phi, psi):
        qml.RY(phi, wires=0)
        qml.RX(psi, wires=0)
        return [{"expval0": qml.expval(qml.PauliZ(0))}, qml.expval(qml.PauliZ(0))]
    
    psi = 0.1
    phi = 0.2
    >>> qjit(jacobian(circuit, argnum=[0, 1]))(psi, phi)
    [{'expval0': (array(-0.0978434), array(-0.19767681))}, (array(-0.0978434), array(-0.19767681))]
  • Support has been added for linear algebra functions which depend on computing the eigenvalues of symmetric matrices, such as np.sqrt_matrix(). (#488)

    For example, you can compile qml.math.sqrt_matrix:

    @qml.qjit
    def workflow(A):
        B = qml.math.sqrt_matrix(A)
        return B @ A

    Internally, this involves support for lowering the eigenvectors/values computation lapack method lapack_dsyevd via stablehlo.custom_call.

  • Additional debugging functions are now available in the catalyst.debug directory. (#529) (#522)

    This includes:

    • filter_static_args(args, static_argnums) to remove static values from arguments using the
      provided index list.

    • get_cmain(fn, *args) to return a C program that calls a jitted function w...

Read more