Skip to content

Releases: PennyLaneAI/catalyst

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

Catalyst v0.4.1

29 Jan 23:22
2d164aa
Compare
Choose a tag to compare

Improvements

  • Catalyst wheels are now packaged with OpenMP and ZStd, which avoids installing additional requirements separately in order to use pre-packaged Catalyst binaries. (#457) (#478)

    Note that OpenMP support for the lightning.kokkos backend has been disabled on macOS x86_64, due to memory issues in the computation of Lightning's adjoint-jacobian in the presence of multiple OMP threads.

Bug fixes

  • Resolve an infinite recursion in the decomposition of the Controlled operator whenever computing a Unitary matrix for the operator fails. (#468)

  • Resolve a failure to generate gradient code for specific input circuits. (#439)

    In this case, jnp.mod was used to compute wire values in a for loop, which prevented the gradient architecture from fully separating quantum and classical code. The following program is now supported:

    @qjit
    @grad
    @qml.qnode(dev)
    def f(x):
        def cnot_loop(j):
            qml.CNOT(wires=[j, jnp.mod((j + 1), 4)])
    
        for_loop(0, 4, 1)(cnot_loop)()
    
        return qml.expval(qml.PauliZ(0))
  • Resolve unpredictable behaviour when importing libraries that share Catalyst's LLVM dependency (e.g. TensorFlow). In some cases, both packages exporting the same symbols from their shared libraries can lead to process crashes and other unpredictable behaviour, since the wrong functions can be called if both libraries are loaded in the current process. The fix involves building shared libraries with hidden (macOS) or protected (linux) symbol visibility by default, exporting only what is necessary. (#465)

  • Resolve a failure to find the SciPy OpenBLAS library when running Catalyst, due to a different SciPy version being used to build Catalyst than to run it. (#471)

  • Resolve a memory leak in the runtime stemming from missing calls to device destructors at the end of programs. (#446)

Contributors

This release contains contributions from (in alphabetical order):

Ali Asadi, David Ittah.

Catalyst v0.4.0

08 Jan 17:45
fae58ae
Compare
Choose a tag to compare

New features

  • Catalyst is now accessible directly within the PennyLane user interface, once Catalyst is installed, allowing easy access to Catalyst just-in-time functionality.

    Through the use of the qml.qjit decorator, entire workflows can be JIT compiled down to a machine binary on first-function execution, including both quantum and classical processing. Subsequent calls to the compiled function will execute the previously-compiled binary, resulting in significant performance improvements.

    import pennylane as qml
    
    dev = qml.device("lightning.qubit", wires=2)
    
    @qml.qjit
    @qml.qnode(dev)
    def circuit(theta):
        qml.Hadamard(wires=0)
        qml.RX(theta, wires=1)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(wires=1))
    >>> circuit(0.5)  # the first call, compilation occurs here
    array(0.)
    >>> circuit(0.5)  # the precompiled quantum function is called
    array(0.)

    Currently, PennyLane supports the Catalyst hybrid compiler with the qml.qjit decorator, which directly aliases Catalyst's catalyst.qjit.

    In addition to the above qml.qjit integration, the following native PennyLane functions can now be used with the qjit decorator: qml.adjoint, qml.ctrl, qml.grad, qml.jacobian, qml.vjp, qml.jvp, and qml.adjoint, qml.while_loop, qml.for_loop, qml.cond. These will alias to the corresponding Catalyst functions when used within a qjit context.

    For more details on these functions, please refer to the PennyLane compiler documentation and compiler module documentation.

  • Just-in-time compiled functions now support asynchronous execution of QNodes. (#374) (#381) (#420) (#424) (#433)

    Simply specify async_qnodes=True when using the @qjit decorator to enable the async execution of QNodes. Currently, asynchronous execution is only supported by lightning.qubit and lightning.kokkos.

    Asynchronous execution will be most beneficial for just-in-time compiled functions that contain --- or generate --- multiple QNodes.

    For example,

    dev = qml.device("lightning.qubit", wires=2)
    
    @qml.qnode(device=dev)
    def circuit(params):
        qml.RX(params[0], wires=0)
        qml.RY(params[1], wires=1)
        qml.CNOT(wires=[0, 1])
        return qml.expval(qml.PauliZ(wires=0))
    
    @qjit(async_qnodes=True)
    def multiple_qnodes(params):
        x = jnp.sin(params)
        y = jnp.cos(params)
        z = jnp.array([circuit(x), circuit(y)]) # will be executed in parallel
        return circuit(z)
    >>> func(jnp.array([1.0, 2.0]))
    1.0
    

    Here, the first two circuit executions will occur in parallel across multiple threads, as their execution can occur independently.

  • Preliminary support for PennyLane transforms has been added. (#280)

    @qjit
    @qml.transforms.split_non_commuting
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x,wires=0)
        return [qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))]
    >>> circuit(0.4)
    [array(-0.51413599), array(0.85770868)]

    Currently, most PennyLane transforms will work with Catalyst as long as:

    • The circuit does not include any Catalyst-specific features, such
      as Catalyst control flow or measurement,

    • The QNode returns only lists of measurement processes,

    • AutoGraph is disabled, and

    • The transformation does not require or depend on the numeric value of
      dynamic variables.

  • Catalyst now supports just-in-time compilation of dynamically-shaped arrays. (#366) (#386) (#390) (#411)

    The @qjit decorator can now be used to compile functions that accepts or contain tensors whose dimensions are not known at compile time; runtime execution with different shapes is supported without recompilation.

    In addition, standard tensor initialization functions jax.numpy.ones, jnp.zeros, and jnp.empty now accept dynamic variables (where the value is only known at runtime).

    @qjit
    def func(size: int):
        return jax.numpy.ones([size, size], dtype=float)
    >>> func(3)
    [[1. 1. 1.]
     [1. 1. 1.]
     [1. 1. 1.]]

    When passing tensors as arguments to compiled functions, the abstracted_axes keyword argument to the @qjit decorator can be used to specify which axes of the input arguments should be treated as abstract (and thus avoid recompilation).

    For example, without specifying abstracted_axes, the following sum function would recompile each time an array of different size is passed as an argument:

    >>> @qjit
    >>> def sum_fn(x):
    >>>     return jnp.sum(x)
    >>> sum_fn(jnp.array([1]))     # Compilation happens here.
    >>> sum_fn(jnp.array([1, 1]))  # And here!

    By passing abstracted_axes, we can specify that the first axes of the first argument is to be treated as dynamic during initial compilation:

    >>> @qjit(abstracted_axes={0: "n"})
    >>> def sum_fn(x):
    >>>     return jnp.sum(x)
    >>> sum_fn(jnp.array([1]))     # Compilation happens here.
    >>> sum_fn(jnp.array([1, 1]))  # No need to recompile.

    Note that support for dynamic arrays in control-flow primitives (such as loops), is not yet supported.

  • Error mitigation using the zero-noise extrapolation method is now available through the catalyst.mitigate_with_zne transform. (#324) (#414)

    For example, given a noisy device (such as noisy hardware available through Amazon Braket):

    dev = qml.device("noisy.device", wires=2)
    
    @qml.qnode(device=dev)
    def circuit(x, n):
    
        @for_loop(0, n, 1)
        def loop_rx(i):
            qml.RX(x, wires=0)
    
        loop_rx()
    
        qml.Hadamard(wires=0)
        qml.RZ(x, wires=0)
        loop_rx()
        qml.RZ(x, wires=0)
        qml.CNOT(wires=[1, 0])
        qml.Hadamard(wires=1)
        return qml.expval(qml.PauliY(wires=0))
    
    @qjit
    def mitigated_circuit(args, n):
        s = jax.numpy.array([1, 2, 3])
        return mitigate_with_zne(circuit, scale_factors=s)(args, n)
    >>> mitigated_circuit(0.2, 5)
    0.5655341100116512

    In addition, a mitigation dialect has been added to the MLIR layer of Catalyst. It contains a Zero Noise Extrapolation (ZNE) operation, with a lowering to a global folded circuit.

Improvements

  • The three backend devices provided with Catalyst, lightning.qubit, lightning.kokkos, and braket.aws, are now dynamically loaded at runtime. (#343) (#400)

    This takes advantage of the new backend plugin system provided in Catalyst v0.3.2, and allows the devices to be packaged separately from the runtime CAPI. Provided backend devices are now loaded at runtime, instead of being linked at compile time.

    For more details on the backend plugin system, see the custom devices documentation.

  • Finite-shot measurement statistics (expval, var, and probs) are now supported for the lightning.qubit and lightning.kokkos devices. Previously, exact statistics were returned even when finite shots were specified. (#392) (#410)

    >>> dev = qml.device("lightning.qubit", wires=2, shots=100)
    >>> @qjit
    >>> @qml.qnode(dev)
    >>> def circuit(x):
    >>>     qml.RX(x, wires=0)
    >>>     return qml.probs(wires=0)
    >>> circuit(0.54)
    array([0.94, 0.06])
    >>> circuit(0.54)
    array([0.93, 0.07])
  • Catalyst gradient functions grad, jacobian, jvp, and vjp can now be invoked from outside a @qjit context. (#375)

    This simplifies the process of writing functions where compilation can be turned on and off easily by adding or removing the decorator. The functions dispatch to their JAX equivalents when the compilation is turned off.

    dev = qml.device("lightning.qubit", wires=2)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        return qml.expval(qml.PauliZ(0))
    >>> grad(circuit)(0.54)  # dispatches to jax.grad
    Array(-0.51413599, dtype=float64, weak_type=True)
    >>> qjit(grad(circuit))(0.54). # differentiates using Catalyst
    array(-0.51413599)
  • New lightning.qubit configuration options are now supported via the qml.device loader, including Markov Chain Monte Carlo sampling support. (#369)

    dev = qml.device("lightning.qubit", wires=2, shots=1000, mcmc=True)
    
    @qml.qnode(dev)
    def circuit(x):
        qml.RX(x, wires=0)
        return qml.expval(qml.PauliZ(0))
    >>> circuit(0.54)
    array(0.856)
  • Improvements have been made to the runtime and quantum MLIR dialect in order to support asynchronous execution.

    • The runtime now su...
Read more

Catalyst v0.3.2-post1

15 Nov 19:41
Compare
Choose a tag to compare

This post-release updates the docs with up-to-date information & additional sections for the installation guide.

Catalyst v0.3.2

08 Nov 17:21
eac6d11
Compare
Choose a tag to compare

New features

  • The experimental AutoGraph feature now supports Python while loops, allowing native Python loops to be captured and compiled with Catalyst. (#318)

    dev = qml.device("lightning.qubit", wires=4)
    
    @qjit(autograph=True)
    @qml.qnode(dev)
    def circuit(n: int, x: float):
        i = 0
    
        while i < n:
            qml.RX(x, wires=i)
            i += 1
    
        return qml.expval(qml.PauliZ(0))
    >>> circuit(4, 0.32)
    array(0.94923542)

    This feature extends the existing AutoGraph support for Python for loops and if statements introduced in v0.3. Note that TensorFlow must be installed for AutoGraph support.

    For more details, please see the AutoGraph guide.

  • In addition to loops and conditional branches, AutoGraph now supports native Python and, or and not operators in Boolean expressions. (#325)

    dev = qml.device("lightning.qubit", wires=1)
    
    @qjit(autograph=True)
    @qml.qnode(dev)
    def circuit(x: float):
    
        if x >= 0 and x < jnp.pi:
            qml.RX(x, wires=0)
    
        return qml.probs()
    >>> circuit(0.43)
    array([0.95448287, 0.04551713])
    >>> circuit(4.54)
    array([1., 0.])

    Note that logical Boolean operators will only be captured by AutoGraph if all operands are dynamic variables (that is, a value known only at runtime, such as a measurement result or function argument). For other use cases, it is recommended to use the jax.numpy.logical_* set of functions where appropriate.

  • Debug compiled programs and print dynamic values at runtime with debug.print (#279) (#356)

    You can now print arbitrary values from your running program, whether they are arrays, constants, strings, or abitrary Python objects. Note that while non-array Python objects will be printed at runtime, their string representation is captured at compile time, and thus will always be the same regardless of program inputs. The output for arrays optionally includes a descriptor for how the data is stored in memory ("memref").

    @qjit
    def func(x: float):
        debug.print(x, memref=True)
        debug.print("exit")
    >>> func(jnp.array(0.43))
    MemRef: base@ = 0x5629ff2b6680 rank = 0 offset = 0 sizes = [] strides = [] data =
    0.43
    exit
  • Catalyst now officially supports macOS X86_64 devices, with macOS binary wheels available for both AARCH64 and X86_64. (#347) (#313)

  • It is now possible to dynamically load third-party Catalyst compatible devices directly into a pre-installed Catalyst runtime on Linux. (#327)

    To take advantage of this, third-party devices must implement the Catalyst::Runtime::QuantumDevice interface, in addition to defining the following method:

    extern "C" Catalyst::Runtime::QuantumDevice*
    getCustomDevice() { return new CustomDevice(); }

    This support can also be integrated into existing PennyLane Python devices that inherit from the QuantumDevice class, by defining the get_c_interface static method.

    For more details, see the custom devices documentation.

Improvements

  • Return values of conditional functions no longer need to be of exactly the same type. Type promotion is automatically applied to branch return values if their types don't match. (#333)

    @qjit
    def func(i: int, f: float):
    
        @cond(i < 3)
        def cond_fn():
            return i
    
        @cond_fn.otherwise
        def otherwise():
            return f
    
        return cond_fn()
    >>> func(1, 4.0)
    array(1.0)

    Automatic type promotion across conditional branches also works with AutoGraph:

    @qjit(autograph=True)
    def func(i: int, f: float):
    
        if i < 3:
            i = i
        else:
            i = f
    
        return i
    >>> func(1, 4.0)
    array(1.0)
  • AutoGraph now supports converting functions even when they are invoked through functional wrappers such as adjoint, ctrl, grad, jacobian, etc. (#336)

    For example, the following should now succeed:

    def inner(n):
      for i in range(n):
        qml.T(i)
    
    @qjit(autograph=True)
    @qml.qnode(dev)
    def f(n: int):
        adjoint(inner)(n)
        return qml.state()
  • To prepare for Catalyst's frontend being integrated with PennyLane, the appropriate plugin entry point interface has been added to Catalyst. (#331)

    For any compiler packages seeking to be registered in PennyLane, the entry_points metadata under the the group name pennylane.compilers must be added, with the following try points:

    • context: Path to the compilation evaluation context manager. This context manager should have the method context.is_tracing(), which returns True if called within a program that is being traced or captured.

    • ops: Path to the compiler operations module. This operations module may contain compiler specific versions of PennyLane operations. Within a JIT context, PennyLane operations may dispatch to these.

    • qjit: Path to the JIT compiler decorator provided by the compiler. This decorator should have the signature qjit(fn, *args, **kwargs), where fn is the function to be compiled.

  • The compiler driver diagnostic output has been improved, and now includes failing IR as well as the names of failing passes. (#349)

  • The scatter operation in the Catalyst dialect now uses an SCF for loop to avoid ballooning the compiled code. (#307)

  • The CopyGlobalMemRefPass pass of our MLIR processing pipeline now supports dynamically shaped arrays. (#348)

  • The Catalyst utility dialect is now included in the Catalyst MLIR C-API. (#345)

  • Fix an issue with the AutoGraph conversion system that would prevent the fallback to Python from working correctly in certain instances. (#352)

    The following type of code is now supported:

    @qjit(autograph=True)
    def f():
      l = jnp.array([1, 2])
      for _ in range(2):
          l = jnp.kron(l, l)
      return l

Breaking changes

  • The axis ordering for catalyst.jacobian is updated to match jax.jacobian. Assuming we have parameters of shape [a,b] and results of shape [c,d], the returned Jacobian will now have shape [c, d, a, b] instead of [a, b, c, d]. (#283)

Bug fixes

  • An upstream change in the PennyLane-Lightning project was addressed to prevent compilation issues in the StateVectorLQubitDynamic class in the runtime. The issue was introduced in #499. (#322)

  • The requirements.txt file to build Catalyst from source has been updated with a minimum pip version, >=22.3. Previous versions of pip are unable to perform editable installs when the system-wide site-packages are read-only, even when the --user flag is provided. (#311)

  • The frontend has been updated to make it compatible with PennyLane MeasurementProcess objects now being PyTrees in PennyLane version 0.33. (#315)

Contributors

This release contains contributions from (in alphabetical order):

Ali Asadi,
David Ittah,
Sergei Mironov,
Romain Moyard,
Erick Ochoa Lopez.