Releases: PennyLaneAI/catalyst
Catalyst v0.9.0
New features
-
Catalyst now supports the specification of shot-vectors when used with
qml.sample
measurements on thelightning.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 toshots=[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
andprobs
, 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 theqjit
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 thecatalyst.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 theqjit
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 ofx.at[i].add(y)
x[i] -= y
in favor ofx.at[i].add(-y)
x[i] *= y
in favor ofx.at[i].multiply(y)
x[i] /= y
in favor ofx.at[i].divide(y)
x[i] **= y
in favor ofx.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:
quantum-opt
: Performs the MLIR-level optimizations and lowers the input dialect to the LLVM dialect.mlir-translate
: Translates the input in the LLVM dialect into LLVM IR.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 formreturn {"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 theqjit
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...
Catalyst v0.8.1
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
-
Fixes an issue where certain JAX linear algebra functions from
jax.scipy.linalg
gave incorrect results when invoked from within a qjit block, and adds full support for otherjax.scipy.linalg
functions. (#1097)The supported linear algebra functions include, but are not limited to:
Breaking changes
- The argument
scale_factors
ofmitigate_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 (likejax.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
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 (viaassert
) 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
andlightning.kokkos
can now be seeded via the newseed
argument of theqjit
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 sameqjit
) 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
orexpval
: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
andreplace_ir
to acquire and recompile the IR from a given pipeline pass for functions compiled withkeep_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 viareplace_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 theprint_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 theqjit
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 withvmap
,cond
,for_loop
orwhile_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...
Catalyst v0.7.0
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 ofjax.devices()[0]
as determined by JAX will be used. -
Catalyst callback functions, such as
pure_callback
,debug.callback
, anddebug.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
andcatalyst.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
andpure_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"
, thepostselect_mode
keyword argument can also be used to specify whether the returned result should includeshots
-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)
wherem
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 returningm
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
wherei
is a single integer will now be automatically converted to the JAX equivalent ofx = 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
, andcond
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 withautograph=True
:def approximate_e(n): num = 1. fac = 1. for i in range(1, n + 1): fac *= i num += 1. / fac ...
Catalyst v0.6.0
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 flagdetailed
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 thewires
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 usingCTRL-C
from a command line and theInterrupt
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
andqml.counts
now produce integer arrays for the sample array and basis state array when used without obser...
Catalyst v0.5.0
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 ofjax.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 simulatornvidia.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 argumentstatic_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 thecatalyst.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
andqml.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
andISWAP
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
, andjvp
) 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
viastablehlo.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...
-
Catalyst v0.4.1
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
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'scatalyst.qjit
.In addition to the above
qml.qjit
integration, the following native PennyLane functions can now be used with theqjit
decorator:qml.adjoint
,qml.ctrl
,qml.grad
,qml.jacobian
,qml.vjp
,qml.jvp
, andqml.adjoint
,qml.while_loop
,qml.for_loop
,qml.cond
. These will alias to the corresponding Catalyst functions when used within aqjit
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 bylightning.qubit
andlightning.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
, andjnp.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 followingsum
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
, andbraket.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
, andprobs
) are now supported for thelightning.qubit
andlightning.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
, andvjp
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 theqml.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...
Catalyst v0.3.2-post1
This post-release updates the docs with up-to-date information & additional sections for the installation guide.
Catalyst v0.3.2
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 andif
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
andnot
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 theget_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 namepennylane.compilers
must be added, with the following try points:-
context
: Path to the compilation evaluation context manager. This context manager should have the methodcontext.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 signatureqjit(fn, *args, **kwargs)
, wherefn
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 matchjax.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.