Skip to content

Commit 46c47f8

Browse files
authored
Use qsharp package to generate QIR (#641)
* Use qsharp package to generate QIR
1 parent 508042c commit 46c47f8

27 files changed

+1132
-1003
lines changed

azure-quantum/azure/quantum/qiskit/backends/backend.py

+123-65
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,53 @@
2121
from azure.quantum.job.session import SessionHost
2222

2323
try:
24-
from qiskit import QuantumCircuit, transpile
24+
from qiskit import QuantumCircuit
2525
from qiskit.providers import BackendV1 as Backend
2626
from qiskit.providers import Options
2727
from qiskit.providers import Provider
2828
from qiskit.providers.models import BackendConfiguration
2929
from qiskit.qobj import QasmQobj, PulseQobj
30-
from pyqir import Module
31-
from qiskit_qir import to_qir_module
30+
import pyqir as pyqir
31+
from qsharp.interop.qiskit import QSharpBackend
32+
from qsharp import TargetProfile
3233

3334
except ImportError:
3435
raise ImportError(
3536
"Missing optional 'qiskit' dependencies. \
3637
To install run: pip install azure-quantum[qiskit]"
3738
)
3839

40+
# barrier is handled by an extra flag which will transpile
41+
# them away if the backend doesn't support them. This has
42+
# to be done as a special pass as the transpiler will not
43+
# remove barriers by default.
3944
QIR_BASIS_GATES = [
40-
"x",
41-
"y",
42-
"z",
45+
"measure",
46+
"reset",
47+
"ccx",
48+
"cx",
49+
"cy",
50+
"cz",
4351
"rx",
52+
"rxx",
53+
"crx",
4454
"ry",
55+
"ryy",
56+
"cry",
4557
"rz",
58+
"rzz",
59+
"crz",
4660
"h",
47-
"swap",
48-
"cx",
49-
"cz",
50-
"reset",
5161
"s",
5262
"sdg",
63+
"swap",
5364
"t",
5465
"tdg",
55-
"measure",
66+
"x",
67+
"y",
68+
"z",
69+
"id",
70+
"ch",
5671
]
5772

5873

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

393408
def _generate_qir(
394-
self, circuits, targetCapability, **to_qir_kwargs
395-
) -> Tuple[Module, List[str]]:
409+
self, circuits: List[QuantumCircuit], target_profile: TargetProfile, **kwargs
410+
) -> pyqir.Module:
411+
412+
if len(circuits) == 0:
413+
raise ValueError("No QuantumCircuits provided")
396414

397415
config = self.configuration()
398416
# Barriers aren't removed by transpilation and must be explicitly removed in the Qiskit to QIR translation.
399-
emit_barrier_calls = "barrier" in config.basis_gates
400-
return to_qir_module(
401-
circuits,
402-
targetCapability,
403-
emit_barrier_calls=emit_barrier_calls,
404-
**to_qir_kwargs,
417+
supports_barrier = "barrier" in config.basis_gates
418+
skip_transpilation = kwargs.pop("skip_transpilation", False)
419+
420+
backend = QSharpBackend(
421+
qiskit_pass_options={"supports_barrier": supports_barrier},
422+
target_profile=target_profile,
423+
skip_transpilation=skip_transpilation,
424+
**kwargs,
405425
)
406426

407-
def _get_qir_str(self, circuits, targetCapability, **to_qir_kwargs) -> str:
408-
module, _ = self._generate_qir(circuits, targetCapability, **to_qir_kwargs)
427+
name = "batch"
428+
if len(circuits) == 1:
429+
name = circuits[0].name
430+
431+
if isinstance(circuits, list):
432+
for value in circuits:
433+
if not isinstance(value, QuantumCircuit):
434+
raise ValueError("Input must be List[QuantumCircuit]")
435+
else:
436+
raise ValueError("Input must be List[QuantumCircuit]")
437+
438+
context = pyqir.Context()
439+
llvm_module = pyqir.qir_module(context, name)
440+
for circuit in circuits:
441+
qir_str = backend.qir(circuit)
442+
module = pyqir.Module.from_ir(context, qir_str)
443+
entry_point = next(filter(pyqir.is_entry_point, module.functions))
444+
entry_point.name = circuit.name
445+
llvm_module.link(module)
446+
err = llvm_module.verify()
447+
if err is not None:
448+
raise Exception(err)
449+
450+
return llvm_module
451+
452+
def _get_qir_str(
453+
self,
454+
circuits: List[QuantumCircuit],
455+
target_profile: TargetProfile,
456+
**to_qir_kwargs,
457+
) -> str:
458+
module = self._generate_qir(circuits, target_profile, **to_qir_kwargs)
409459
return str(module)
410460

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

418-
# Override QIR translation parameters
419-
# We will record the output by default, but allow the backend to override this, and allow the user to override the backend.
420-
to_qir_kwargs = input_params.pop(
421-
"to_qir_kwargs", config.azure.get("to_qir_kwargs", {"record_output": True})
422-
)
423-
targetCapability = input_params.pop(
424-
"targetCapability",
425-
self.options.get("targetCapability", "AdaptiveExecution"),
426-
)
469+
target_profile = self._get_target_profile(input_params)
427470

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

432475
# We'll transpile automatically to the supported gates in QIR unless explicitly skipped.
433-
if not input_params.pop("skipTranspile", False):
434-
# Set of gates supported by QIR targets.
435-
circuits = transpile(
436-
circuits, basis_gates=config.basis_gates, optimization_level=0
437-
)
476+
skip_transpilation = input_params.pop("skipTranspile", False)
477+
478+
module = self._generate_qir(
479+
circuits, target_profile, skip_transpilation=skip_transpilation
480+
)
481+
482+
def get_func_name(func: pyqir.Function) -> str:
483+
return func.name
484+
485+
entry_points = list(
486+
map(get_func_name, filter(pyqir.is_entry_point, module.functions))
487+
)
488+
489+
if not skip_transpilation:
438490
# We'll only log the QIR again if we performed a transpilation.
439491
if logger.isEnabledFor(logging.DEBUG):
440-
qir = self._get_qir_str(circuits, targetCapability, **to_qir_kwargs)
492+
qir = str(module)
441493
logger.debug(f"QIR (Post-transpilation):\n{qir}")
442494

443-
(module, entry_points) = self._generate_qir(
444-
circuits, targetCapability, **to_qir_kwargs
445-
)
446-
447-
if not "items" in input_params:
495+
if "items" not in input_params:
448496
arguments = input_params.pop("arguments", [])
449497
input_params["items"] = [
450498
{"entryPoint": name, "arguments": arguments} for name in entry_points
451499
]
452500

453-
return module.bitcode
501+
return str(module).encode("utf-8")
454502

455-
def _estimate_cost_qir(self, circuits, shots, options={}):
503+
def _estimate_cost_qir(
504+
self, circuits: Union[QuantumCircuit, List[QuantumCircuit]], shots, options={}
505+
):
456506
"""Estimate the cost for the given circuit."""
457-
config = self.configuration()
458507
input_params = self._get_input_params(options, shots=shots)
459508

460509
if not (isinstance(circuits, list)):
461510
circuits = [circuits]
462-
463-
to_qir_kwargs = input_params.pop(
464-
"to_qir_kwargs", config.azure.get("to_qir_kwargs", {"record_output": True})
465-
)
466-
targetCapability = input_params.pop(
467-
"targetCapability",
468-
self.options.get("targetCapability", "AdaptiveExecution"),
469-
)
470-
471-
if not input_params.pop("skipTranspile", False):
472-
# Set of gates supported by QIR targets.
473-
circuits = transpile(
474-
circuits, basis_gates=config.basis_gates, optimization_level=0
475-
)
476-
477511

478-
(module, _) = self._generate_qir(
479-
circuits, targetCapability, **to_qir_kwargs
512+
skip_transpilation = input_params.pop("skipTranspile", False)
513+
target_profile = self._get_target_profile(input_params)
514+
module = self._generate_qir(
515+
circuits, target_profile, skip_transpilation=skip_transpilation
480516
)
481-
517+
482518
workspace = self.provider().get_workspace()
483519
target = workspace.get_targets(self.name())
484520
return target.estimate_cost(module, shots=shots)
485521

522+
def _get_target_profile(self, input_params) -> TargetProfile:
523+
# Default to Adaptive_RI if not specified on the backend
524+
# this is really just a safeguard in case the backend doesn't have a default
525+
default_profile = self.options.get("target_profile", TargetProfile.Adaptive_RI)
526+
527+
# If the user is using the old targetCapability parameter, we'll warn them
528+
# and use that value for now. This will be removed in the future.
529+
if "targetCapability" in input_params:
530+
warnings.warn(
531+
"The 'targetCapability' parameter is deprecated and will be ignored in the future. "
532+
"Please, use 'target_profile' parameter instead.",
533+
category=DeprecationWarning,
534+
)
535+
cap = input_params.pop("targetCapability")
536+
if cap == "AdaptiveExecution":
537+
default_profile = TargetProfile.Adaptive_RI
538+
else:
539+
default_profile = TargetProfile.Base
540+
# If the user specifies a target profile, use that.
541+
# Otherwise, use the profile we got from the backend/targetCapability.
542+
return input_params.pop("target_profile", default_profile)
543+
486544

487545
class AzureBackend(AzureBackendBase):
488546
"""Base class for interfacing with a backend in Azure Quantum"""

azure-quantum/azure/quantum/qiskit/backends/ionq.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from azure.quantum.qiskit.job import AzureQuantumJob
88
from azure.quantum.target.ionq import IonQ
99
from abc import abstractmethod
10-
10+
from qsharp import TargetProfile
1111
from qiskit import QuantumCircuit
1212

1313
from .backend import (
@@ -65,7 +65,7 @@ def _default_options(cls) -> Options:
6565
**{
6666
cls._SHOTS_PARAM_NAME: _DEFAULT_SHOTS_COUNT,
6767
},
68-
targetCapability="BasicExecution",
68+
target_profile=TargetProfile.Base,
6969
)
7070

7171
def _azure_config(self) -> Dict[str, str]:

0 commit comments

Comments
 (0)