Skip to content

[runtime] QubitPlacer part 1 #4700

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cirq-google/cirq_google/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
ExecutableGroupResultFilesystemRecord,
QuantumRuntimeConfiguration,
execute,
QubitPlacer,
NaiveQubitPlacer,
)

from cirq_google import experimental
Expand Down
1 change: 1 addition & 0 deletions cirq-google/cirq_google/json_resolver_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ def _class_resolver_dictionary() -> Dict[str, ObjectFactory]:
'cirq.google.ExecutableGroupResultFilesystemRecord': cirq_google.ExecutableGroupResultFilesystemRecord,
# pylint: enable=line-too-long
'cirq.google.QuantumRuntimeConfiguration': cirq_google.QuantumRuntimeConfiguration,
'cirq.google.NaiveQubitPlacer': cirq_google.NaiveQubitPlacer,
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,31 @@
},
"runtime_info": {
"cirq_type": "cirq.google.RuntimeInfo",
"execution_index": 5
"execution_index": 5,
"qubit_placement": [
[
[
0,
0
],
{
"cirq_type": "GridQubit",
"row": 5,
"col": 5
}
],
[
[
1,
1
],
{
"cirq_type": "GridQubit",
"row": 6,
"col": 6
}
]
]
},
"raw_data": {
"cirq_type": "Result",
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cirq_type": "cirq.google.NaiveQubitPlacer"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cirq_google.NaiveQubitPlacer()
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"cirq_type": "cirq.google.RuntimeInfo",
"execution_index": 5
"execution_index": 5,
"qubit_placement": null
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
cirq_google.RuntimeInfo(execution_index=5)
cirq_google.RuntimeInfo(execution_index=5, qubit_placement=None)
Original file line number Diff line number Diff line change
@@ -1 +1 @@
cirq_google.SharedRuntimeInfo(run_id='my run')
cirq_google.SharedRuntimeInfo(run_id='my run', device=None)
1 change: 1 addition & 0 deletions cirq-google/cirq_google/json_test_data/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
'RuntimeInfo',
'SharedRuntimeInfo',
'ExecutableGroupResultFilesystemRecord',
'NaiveQubitPlacer',
]
},
tested_elsewhere=[
Expand Down
5 changes: 5 additions & 0 deletions cirq-google/cirq_google/workflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@
from cirq_google.workflow.io import (
ExecutableGroupResultFilesystemRecord,
)

from cirq_google.workflow.qubit_placement import (
QubitPlacer,
NaiveQubitPlacer,
)
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def _get_quantum_executables():
return [
QuantumExecutable(
spec=_get_example_spec(name=f'example-program-{i}'),
problem_topology=cirq.LineTopology(10),
circuit=_get_random_circuit(qubits, random_state=i),
measurement=BitstringsMeasurement(n_repetitions=10),
)
Expand Down
60 changes: 52 additions & 8 deletions cirq-google/cirq_google/workflow/quantum_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@
from typing import Any, Dict, Optional, List

import cirq
import numpy as np
from cirq import _compat
from cirq.protocols import dataclass_json_dict
from cirq.protocols import dataclass_json_dict, obj_to_dict_helper
from cirq_google.workflow._abstract_engine_processor_shim import AbstractEngineProcessorShim
from cirq_google.workflow.io import _FilesystemSaver
from cirq_google.workflow.progress import _PrintLogger
from cirq_google.workflow.quantum_executable import (
QuantumExecutable,
ExecutableSpec,
QuantumExecutableGroup,
)
from cirq_google.workflow.qubit_placement import QubitPlacer, NaiveQubitPlacer


@dataclasses.dataclass
Expand All @@ -39,21 +42,31 @@ class SharedRuntimeInfo:

Args:
run_id: A unique `str` identifier for this run.
device: The actual device used during execution, not just its processor_id
"""

run_id: str
device: Optional[cirq.Device] = None

@classmethod
def _json_namespace_(cls) -> str:
return 'cirq.google'

def _json_dict_(self) -> Dict[str, Any]:
return dataclass_json_dict(self)
# TODO (gh-4699): serialize `device` as well once SerializableDevice is serializable.
return obj_to_dict_helper(self, attribute_names=['run_id'])

def __repr__(self) -> str:
return _compat.dataclass_repr(self, namespace='cirq_google')


def _try_tuple(k: Any) -> Any:
"""If we serialize a dictionary that had tuple keys, they get turned to json lists."""
if isinstance(k, list):
return tuple(k)
return k # coverage: ignore


@dataclasses.dataclass
class RuntimeInfo:
"""Runtime information relevant to a particular `cg.QuantumExecutable`.
Expand All @@ -63,16 +76,29 @@ class RuntimeInfo:
Args:
execution_index: What order (in its `cg.QuantumExecutableGroup`) this
`cg.QuantumExecutable` was executed.
qubit_placement: If a QubitPlacer was used, a record of the mapping
from problem-qubits to device-qubits.
"""

execution_index: int
qubit_placement: Optional[Dict[Any, cirq.Qid]] = None

@classmethod
def _json_namespace_(cls) -> str:
return 'cirq.google'

def _json_dict_(self) -> Dict[str, Any]:
return dataclass_json_dict(self)
d = dataclass_json_dict(self)
if d['qubit_placement']:
d['qubit_placement'] = list(d['qubit_placement'].items())
return d

@classmethod
def _from_json_dict_(cls, **kwargs) -> 'RuntimeInfo':
kwargs.pop('cirq_type')
if kwargs.get('qubit_placement', None):
kwargs['qubit_placement'] = {_try_tuple(k): v for k, v in kwargs['qubit_placement']}
return cls(**kwargs)

def __repr__(self) -> str:
return _compat.dataclass_repr(self, namespace='cirq_google')
Expand Down Expand Up @@ -143,10 +169,16 @@ class QuantumRuntimeConfiguration:
run_id: A unique `str` identifier for a run. If data already exists for the specified
`run_id`, an exception will be raised. If not specified, we will generate a UUID4
run identifier.
random_seed: An initial seed to make the run deterministic. Otherwise, the default numpy
seed will be used.
qubit_placer: A `cg.QubitPlacer` implementation to map executable qubits to device qubits.
The placer is only called if a given `cg.QuantumExecutable` has a `problem_topology`.
"""

processor: AbstractEngineProcessorShim
run_id: Optional[str] = None
random_seed: Optional[int] = None
qubit_placer: QubitPlacer = NaiveQubitPlacer()

@classmethod
def _json_namespace_(cls) -> str:
Expand Down Expand Up @@ -200,26 +232,38 @@ def execute(
# coverage: ignore
raise ValueError("Please provide a non-empty `base_data_dir`.")

shared_rt_info = SharedRuntimeInfo(run_id=run_id)
sampler = rt_config.processor.get_sampler()
device = rt_config.processor.get_device()

shared_rt_info = SharedRuntimeInfo(
run_id=run_id,
device=device,
)
executable_results = []

saver = _FilesystemSaver(base_data_dir=base_data_dir, run_id=run_id)
saver.initialize(rt_config, shared_rt_info)

sampler = rt_config.processor.get_sampler()
logger = _PrintLogger(n_total=len(executable_group))
logger.initialize()

rs = np.random.RandomState(rt_config.random_seed)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should we give the seed to the rt_config or the randomstate generator itself ? It seems like down the line if we kept creating randomStates off of this seed we'd get a lot of duplicate random numbers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two part answer

  1. rt_config needs to be serializable and put in a database so we can groupby and agg the results. Seed is (much!) easier than the internal state of a random state
  2. We don't create >1 randomstate based on this seed. We create one at the top of the function (here!) and it gets passed to any subroutines that require randomness.

exe: QuantumExecutable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this declaration mess with the below loop ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is how you do type annotations for loop variables

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this annotation? Wouldn't the type of exe get inferred automatically based on the type of executable_group which we iterating on?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pycharm wasn't picking it up

for i, exe in enumerate(executable_group):
runtime_info = RuntimeInfo(execution_index=i)

if exe.params != tuple():
raise NotImplementedError("Circuit params are not yet supported.")

circuit = exe.circuit

if not hasattr(exe.measurement, 'n_repetitions'):
raise NotImplementedError("Only `BitstringsMeasurement` are supported.")

circuit = exe.circuit
if exe.problem_topology is not None:
circuit, mapping = rt_config.qubit_placer.place_circuit(
circuit, problem_topology=exe.problem_topology, shared_rt_info=shared_rt_info, rs=rs
)
runtime_info.qubit_placement = mapping

sampler_run_result = sampler.run(circuit, repetitions=exe.measurement.n_repetitions)

exe_result = ExecutableResult(
Expand Down
10 changes: 9 additions & 1 deletion cirq-google/cirq_google/workflow/quantum_runtime_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ def _load_result_by_hand(tmpdir: str, run_id: str) -> cg.ExecutableGroupResult:
@pytest.mark.parametrize('run_id_in', ['unit_test_runid', None])
def test_execute(tmpdir, run_id_in, patch_cirq_default_resolvers):
assert patch_cirq_default_resolvers
rt_config = cg.QuantumRuntimeConfiguration(processor=_MockEngineProcessor(), run_id=run_id_in)
rt_config = cg.QuantumRuntimeConfiguration(
processor=_MockEngineProcessor(),
run_id=run_id_in,
qubit_placer=cg.NaiveQubitPlacer(),
)
executable_group = cg.QuantumExecutableGroup(_get_quantum_executables())
returned_exegroup_result = cg.execute(
rt_config=rt_config, executable_group=executable_group, base_data_dir=tmpdir
Expand All @@ -179,5 +183,9 @@ def test_execute(tmpdir, run_id_in, patch_cirq_default_resolvers):
)
exegroup_result: cg.ExecutableGroupResult = egr_record.load(base_data_dir=tmpdir)

# TODO(gh-4699): Don't null-out device once it's serializable.
assert isinstance(returned_exegroup_result.shared_runtime_info.device, cg.SerializableDevice)
returned_exegroup_result.shared_runtime_info.device = None

assert returned_exegroup_result == exegroup_result
assert manual_exegroup_result == exegroup_result
75 changes: 75 additions & 0 deletions cirq-google/cirq_google/workflow/qubit_placement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright 2021 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Features for placing qubits onto devices."""

import abc
import dataclasses
from typing import Dict, Any, Tuple, TYPE_CHECKING

import numpy as np

import cirq
from cirq import _compat

if TYPE_CHECKING:
import cirq_google as cg


class QubitPlacer(metaclass=abc.ABCMeta):
@abc.abstractmethod
def place_circuit(
self,
circuit: cirq.AbstractCircuit,
problem_topology: 'cirq.NamedTopology',
shared_rt_info: 'cg.SharedRuntimeInfo',
rs: np.random.RandomState,
) -> Tuple['cirq.FrozenCircuit', Dict[Any, 'cirq.Qid']]:
"""Place a circuit with a given topology.

Args:
circuit: The circuit.
problem_topology: The topologies (i.e. connectivity) of the circuit.
shared_rt_info: A `cg.SharedRuntimeInfo` object that may contain additional info
to inform placement.
rs: A `RandomState` to enable pseudo-random placement strategies.

Returns:
A tuple of a new frozen circuit with the qubits placed and a mapping from input
qubits or nodes to output qubits.
"""


@dataclasses.dataclass(frozen=True)
class NaiveQubitPlacer(QubitPlacer):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename the class to a more descriptive name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have a suggestion

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe NoOpQubitPlacer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not really a "no op", it's more like an identity

"""Don't do any qubit placement, use circuit qubits."""

def place_circuit(
self,
circuit: 'cirq.AbstractCircuit',
problem_topology: 'cirq.NamedTopology',
shared_rt_info: 'cg.SharedRuntimeInfo',
rs: np.random.RandomState,
) -> Tuple['cirq.FrozenCircuit', Dict[Any, 'cirq.Qid']]:
return circuit.freeze(), {q: q for q in circuit.all_qubits()}

@classmethod
def _json_namespace_(cls) -> str:
return 'cirq.google'

def _json_dict_(self) -> Dict[str, Any]:
return cirq.dataclass_json_dict(self)

def __repr__(self) -> str:
return _compat.dataclass_repr(self, namespace='cirq_google')
41 changes: 41 additions & 0 deletions cirq-google/cirq_google/workflow/qubit_placement_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2021 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cirq
import cirq_google as cg

import numpy as np


def test_naive_qubit_placer():
topo = cirq.TiltedSquareLattice(4, 2)
qubits = sorted(topo.nodes_to_gridqubits(offset=(5, 3)).values())
circuit = cirq.experiments.random_rotations_between_grid_interaction_layers_circuit(
qubits, depth=8, two_qubit_op_factory=lambda a, b, _: cirq.SQRT_ISWAP(a, b)
)

assert all(q in cg.Sycamore23.qubit_set() for q in circuit.all_qubits())

qp = cg.NaiveQubitPlacer()
circuit2, mapping = qp.place_circuit(
circuit,
problem_topology=topo,
shared_rt_info=cg.SharedRuntimeInfo(run_id='1'),
rs=np.random.RandomState(1),
)
assert circuit is not circuit2
assert circuit == circuit2
assert all(q in cg.Sycamore23.qubit_set() for q in circuit.all_qubits())
for k, v in mapping.items():
assert k == v