Skip to content

Commit

Permalink
Use qsharp package to generate QIR
Browse files Browse the repository at this point in the history
  • Loading branch information
idavis committed Sep 27, 2024
1 parent a391158 commit 0a3d926
Show file tree
Hide file tree
Showing 26 changed files with 665 additions and 533 deletions.
184 changes: 120 additions & 64 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,139 @@ 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)
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

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
84 changes: 57 additions & 27 deletions azure-quantum/azure/quantum/qiskit/backends/microsoft.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,17 @@
# Licensed under the MIT License.
##

from typing import TYPE_CHECKING, Any, Dict, List
from typing import TYPE_CHECKING, Dict, List
from azure.quantum.version import __version__
from qiskit import QuantumCircuit
from abc import abstractmethod
from .backend import AzureQirBackend
from .backend import AzureQirBackend, QIR_BASIS_GATES

from qiskit.providers.models import BackendConfiguration
from qiskit.providers import Options, Provider

QIR_BASIS_GATES = [
"measure",
"m",
"ccx",
"cx",
"cz",
"h",
"reset",
"rx",
"ry",
"rz",
"s",
"sdg",
"swap",
"t",
"tdg",
"x",
"y",
"z",
"id",
]
from qsharp import TargetProfile
from qsharp.interop.qiskit import ResourceEstimatorBackend
import pyqir as pyqir

if TYPE_CHECKING:
from azure.quantum.qiskit import AzureQuantumProvider
Expand All @@ -55,19 +36,68 @@ def __init__(

@classmethod
def _default_options(cls):
return Options(targetCapability="AdaptiveExecution")
return Options(target_profile=TargetProfile.Adaptive_RI)

def _azure_config(self) -> Dict[str, str]:
config = super()._azure_config()
config.update(
{
"provider_id": "microsoft-qc",
"output_data_format": "microsoft.resource-estimates.v1",
"to_qir_kwargs": {"record_output": False},
}
)
return config

def _generate_qir(
self, circuits: List[QuantumCircuit], target_profile: TargetProfile, **kwargs
) -> pyqir.Module:
if len(circuits) == 0:
raise ValueError("No QuantumCircuits provided")

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

skip_transpilation = kwargs.pop("skip_transpilation", False)
backend = ResourceEstimatorBackend(
skip_transpilation=skip_transpilation, **kwargs
)
context = pyqir.Context()
llvm_module = pyqir.qir_module(context, name)
for circuit in circuits:
qir_str = backend.qir(circuit, target_profile=target_profile)
module = pyqir.Module.from_ir(context, qir_str)
llvm_module.link(module)

# Add NOOP for recording output tuples
# the service isn't set up to handle any output recording calls
# and the Q# compiler will always emit them.
noop_tuple_record_output = """; NOOP the extern calls to recording output tuples
define void @__quantum__rt__tuple_record_output(i64, i8*) {
ret void
}"""
noop_tuple_record_output_module = pyqir.Module.from_ir(
context, noop_tuple_record_output
)
llvm_module.link(noop_tuple_record_output_module)

err = llvm_module.verify()
if err is not None:
raise Exception(err)

return llvm_module


class MicrosoftResourceEstimationBackend(MicrosoftBackend):
"""Backend class for interfacing with the resource estimator target"""
Expand All @@ -77,7 +107,7 @@ class MicrosoftResourceEstimationBackend(MicrosoftBackend):
@classmethod
def _default_options(cls):
return Options(
targetCapability="AdaptiveExecution",
target_profile=TargetProfile.Adaptive_RI,
errorBudget=1e-3,
qubitParams={"name": "qubit_gate_ns_e3"},
qecScheme={"name": "surface_code"}
Expand Down
Loading

0 comments on commit 0a3d926

Please sign in to comment.