Skip to content

Commit

Permalink
Use qsharp package to generate QIR (#641)
Browse files Browse the repository at this point in the history
* Use qsharp package to generate QIR
  • Loading branch information
idavis authored Oct 22, 2024
1 parent 508042c commit 46c47f8
Show file tree
Hide file tree
Showing 27 changed files with 1,132 additions and 1,003 deletions.
188 changes: 123 additions & 65 deletions azure-quantum/azure/quantum/qiskit/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,53 @@
from azure.quantum.job.session import SessionHost

try:
from qiskit import QuantumCircuit, transpile
from qiskit import QuantumCircuit
from qiskit.providers import BackendV1 as Backend
from qiskit.providers import Options
from qiskit.providers import Provider
from qiskit.providers.models import BackendConfiguration
from qiskit.qobj import QasmQobj, PulseQobj
from pyqir import Module
from qiskit_qir import to_qir_module
import pyqir as pyqir
from qsharp.interop.qiskit import QSharpBackend
from qsharp import TargetProfile

except ImportError:
raise ImportError(
"Missing optional 'qiskit' dependencies. \
To install run: pip install azure-quantum[qiskit]"
)

# barrier is handled by an extra flag which will transpile
# them away if the backend doesn't support them. This has
# to be done as a special pass as the transpiler will not
# remove barriers by default.
QIR_BASIS_GATES = [
"x",
"y",
"z",
"measure",
"reset",
"ccx",
"cx",
"cy",
"cz",
"rx",
"rxx",
"crx",
"ry",
"ryy",
"cry",
"rz",
"rzz",
"crz",
"h",
"swap",
"cx",
"cz",
"reset",
"s",
"sdg",
"swap",
"t",
"tdg",
"measure",
"x",
"y",
"z",
"id",
"ch",
]


Expand Down Expand Up @@ -391,98 +406,141 @@ def _prepare_job_metadata(self, circuits: List[QuantumCircuit]) -> Dict[str, str
return {}

def _generate_qir(
self, circuits, targetCapability, **to_qir_kwargs
) -> Tuple[Module, List[str]]:
self, circuits: List[QuantumCircuit], target_profile: TargetProfile, **kwargs
) -> pyqir.Module:

if len(circuits) == 0:
raise ValueError("No QuantumCircuits provided")

config = self.configuration()
# Barriers aren't removed by transpilation and must be explicitly removed in the Qiskit to QIR translation.
emit_barrier_calls = "barrier" in config.basis_gates
return to_qir_module(
circuits,
targetCapability,
emit_barrier_calls=emit_barrier_calls,
**to_qir_kwargs,
supports_barrier = "barrier" in config.basis_gates
skip_transpilation = kwargs.pop("skip_transpilation", False)

backend = QSharpBackend(
qiskit_pass_options={"supports_barrier": supports_barrier},
target_profile=target_profile,
skip_transpilation=skip_transpilation,
**kwargs,
)

def _get_qir_str(self, circuits, targetCapability, **to_qir_kwargs) -> str:
module, _ = self._generate_qir(circuits, targetCapability, **to_qir_kwargs)
name = "batch"
if len(circuits) == 1:
name = circuits[0].name

if isinstance(circuits, list):
for value in circuits:
if not isinstance(value, QuantumCircuit):
raise ValueError("Input must be List[QuantumCircuit]")
else:
raise ValueError("Input must be List[QuantumCircuit]")

context = pyqir.Context()
llvm_module = pyqir.qir_module(context, name)
for circuit in circuits:
qir_str = backend.qir(circuit)
module = pyqir.Module.from_ir(context, qir_str)
entry_point = next(filter(pyqir.is_entry_point, module.functions))
entry_point.name = circuit.name
llvm_module.link(module)
err = llvm_module.verify()
if err is not None:
raise Exception(err)

return llvm_module

def _get_qir_str(
self,
circuits: List[QuantumCircuit],
target_profile: TargetProfile,
**to_qir_kwargs,
) -> str:
module = self._generate_qir(circuits, target_profile, **to_qir_kwargs)
return str(module)

def _translate_input(
self, circuits: List[QuantumCircuit], input_params: Dict[str, Any]
self, circuits: Union[QuantumCircuit, List[QuantumCircuit]], input_params: Dict[str, Any]
) -> bytes:
"""Translates the input values to the QIR expected by the Backend."""
logger.info(f"Using QIR as the job's payload format.")
config = self.configuration()
if not (isinstance(circuits, list)):
circuits = [circuits]

# Override QIR translation parameters
# We will record the output by default, but allow the backend to override this, and allow the user to override the backend.
to_qir_kwargs = input_params.pop(
"to_qir_kwargs", config.azure.get("to_qir_kwargs", {"record_output": True})
)
targetCapability = input_params.pop(
"targetCapability",
self.options.get("targetCapability", "AdaptiveExecution"),
)
target_profile = self._get_target_profile(input_params)

if logger.isEnabledFor(logging.DEBUG):
qir = self._get_qir_str(circuits, targetCapability, **to_qir_kwargs)
qir = self._get_qir_str(circuits, target_profile, skip_transpilation=True)
logger.debug(f"QIR:\n{qir}")

# We'll transpile automatically to the supported gates in QIR unless explicitly skipped.
if not input_params.pop("skipTranspile", False):
# Set of gates supported by QIR targets.
circuits = transpile(
circuits, basis_gates=config.basis_gates, optimization_level=0
)
skip_transpilation = input_params.pop("skipTranspile", False)

module = self._generate_qir(
circuits, target_profile, skip_transpilation=skip_transpilation
)

def get_func_name(func: pyqir.Function) -> str:
return func.name

entry_points = list(
map(get_func_name, filter(pyqir.is_entry_point, module.functions))
)

if not skip_transpilation:
# We'll only log the QIR again if we performed a transpilation.
if logger.isEnabledFor(logging.DEBUG):
qir = self._get_qir_str(circuits, targetCapability, **to_qir_kwargs)
qir = str(module)
logger.debug(f"QIR (Post-transpilation):\n{qir}")

(module, entry_points) = self._generate_qir(
circuits, targetCapability, **to_qir_kwargs
)

if not "items" in input_params:
if "items" not in input_params:
arguments = input_params.pop("arguments", [])
input_params["items"] = [
{"entryPoint": name, "arguments": arguments} for name in entry_points
]

return module.bitcode
return str(module).encode("utf-8")

def _estimate_cost_qir(self, circuits, shots, options={}):
def _estimate_cost_qir(
self, circuits: Union[QuantumCircuit, List[QuantumCircuit]], shots, options={}
):
"""Estimate the cost for the given circuit."""
config = self.configuration()
input_params = self._get_input_params(options, shots=shots)

if not (isinstance(circuits, list)):
circuits = [circuits]

to_qir_kwargs = input_params.pop(
"to_qir_kwargs", config.azure.get("to_qir_kwargs", {"record_output": True})
)
targetCapability = input_params.pop(
"targetCapability",
self.options.get("targetCapability", "AdaptiveExecution"),
)

if not input_params.pop("skipTranspile", False):
# Set of gates supported by QIR targets.
circuits = transpile(
circuits, basis_gates=config.basis_gates, optimization_level=0
)


(module, _) = self._generate_qir(
circuits, targetCapability, **to_qir_kwargs
skip_transpilation = input_params.pop("skipTranspile", False)
target_profile = self._get_target_profile(input_params)
module = self._generate_qir(
circuits, target_profile, skip_transpilation=skip_transpilation
)

workspace = self.provider().get_workspace()
target = workspace.get_targets(self.name())
return target.estimate_cost(module, shots=shots)

def _get_target_profile(self, input_params) -> TargetProfile:
# Default to Adaptive_RI if not specified on the backend
# this is really just a safeguard in case the backend doesn't have a default
default_profile = self.options.get("target_profile", TargetProfile.Adaptive_RI)

# If the user is using the old targetCapability parameter, we'll warn them
# and use that value for now. This will be removed in the future.
if "targetCapability" in input_params:
warnings.warn(
"The 'targetCapability' parameter is deprecated and will be ignored in the future. "
"Please, use 'target_profile' parameter instead.",
category=DeprecationWarning,
)
cap = input_params.pop("targetCapability")
if cap == "AdaptiveExecution":
default_profile = TargetProfile.Adaptive_RI
else:
default_profile = TargetProfile.Base
# If the user specifies a target profile, use that.
# Otherwise, use the profile we got from the backend/targetCapability.
return input_params.pop("target_profile", default_profile)


class AzureBackend(AzureBackendBase):
"""Base class for interfacing with a backend in Azure Quantum"""
Expand Down
4 changes: 2 additions & 2 deletions azure-quantum/azure/quantum/qiskit/backends/ionq.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from azure.quantum.qiskit.job import AzureQuantumJob
from azure.quantum.target.ionq import IonQ
from abc import abstractmethod

from qsharp import TargetProfile
from qiskit import QuantumCircuit

from .backend import (
Expand Down Expand Up @@ -65,7 +65,7 @@ def _default_options(cls) -> Options:
**{
cls._SHOTS_PARAM_NAME: _DEFAULT_SHOTS_COUNT,
},
targetCapability="BasicExecution",
target_profile=TargetProfile.Base,
)

def _azure_config(self) -> Dict[str, str]:
Expand Down
Loading

0 comments on commit 46c47f8

Please sign in to comment.