Skip to content
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

[runtime] QubitPlacer 2 - RandomDevicePlacer #4719

Merged
merged 9 commits into from
Jan 19, 2022
Merged
2 changes: 2 additions & 0 deletions cirq-google/cirq_google/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@
QuantumRuntimeConfiguration,
execute,
QubitPlacer,
CouldNotPlaceError,
NaiveQubitPlacer,
RandomDevicePlacer,
)

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 @@ -56,4 +56,5 @@ def _class_resolver_dictionary() -> Dict[str, ObjectFactory]:
# pylint: enable=line-too-long
'cirq.google.QuantumRuntimeConfiguration': cirq_google.QuantumRuntimeConfiguration,
'cirq.google.NaiveQubitPlacer': cirq_google.NaiveQubitPlacer,
'cirq.google.RandomDevicePlacer': cirq_google.RandomDevicePlacer,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cirq_type": "cirq.google.RandomDevicePlacer"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cirq_google.RandomDevicePlacer()
2 changes: 2 additions & 0 deletions cirq-google/cirq_google/json_test_data/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'THETA_ZETA_GAMMA_FLOQUET_PHASED_FSIM_CHARACTERIZATION',
'QuantumEngineSampler',
'ValidatingSampler',
'CouldNotPlaceError',
# Abstract:
'ExecutableSpec',
],
Expand All @@ -67,6 +68,7 @@
'SharedRuntimeInfo',
'ExecutableGroupResultFilesystemRecord',
'NaiveQubitPlacer',
'RandomDevicePlacer',
]
},
tested_elsewhere=[
Expand Down
2 changes: 2 additions & 0 deletions cirq-google/cirq_google/workflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@

from cirq_google.workflow.qubit_placement import (
QubitPlacer,
CouldNotPlaceError,
NaiveQubitPlacer,
RandomDevicePlacer,
)
30 changes: 30 additions & 0 deletions cirq-google/cirq_google/workflow/_device_shim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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 itertools
from typing import Iterable, cast

import cirq
import networkx as nx


def _gridqubits_to_graph_device(qubits: Iterable[cirq.GridQubit]):
return nx.Graph(
pair for pair in itertools.combinations(qubits, 2) if pair[0].is_adjacent(pair[1])
)


def _Device_dot_get_nx_graph(device: 'cirq.Device') -> nx.Graph:
tanujkhattar marked this conversation as resolved.
Show resolved Hide resolved
"""Shim over future `cirq.Device` method to get a NetworkX graph."""
return _gridqubits_to_graph_device(cast(Iterable[cirq.GridQubit], device.qubit_set()))
136 changes: 135 additions & 1 deletion cirq-google/cirq_google/workflow/qubit_placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@

import abc
import dataclasses
from typing import Dict, Any, Tuple, TYPE_CHECKING
from functools import lru_cache
from typing import Dict, Any, Tuple, List, Callable, TYPE_CHECKING

import numpy as np

import cirq
from cirq import _compat
from cirq.devices.named_topologies import get_placements
from cirq_google.workflow._device_shim import _Device_dot_get_nx_graph

if TYPE_CHECKING:
import cirq_google as cg


class CouldNotPlaceError(RuntimeError):
"""Raised if a problem topology could not be placed on a device graph."""


class QubitPlacer(metaclass=abc.ABCMeta):
@abc.abstractmethod
def place_circuit(
Expand Down Expand Up @@ -73,3 +80,130 @@ def _json_dict_(self) -> Dict[str, Any]:

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


def default_topo_node_to_qubit(node: Any) -> cirq.Qid:
"""The default mapping from `cirq.NamedTopology` nodes and `cirq.Qid`.

There is a correspondence between nodes and the "abstract" Qids
used to construct un-placed circuit. `cirq.get_placements` returns a dictionary
mapping from node to Qid. We use this function to transform it into a mapping
from "abstract" Qid to device Qid. This function encodes the default behavior used by
`RandomDevicePlacer`.

If nodes are tuples of integers, map to `cirq.GridQubit`. Otherwise, try
to map to `cirq.LineQubit` and rely on its validation.

Args:
node: A node from a `cirq.NamedTopology` graph.

Returns:
A `cirq.Qid` appropriate for the node type.
"""

try:
return cirq.GridQubit(*node)
except TypeError:
return cirq.LineQubit(node)
Comment on lines +85 to +107
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd propose to add a method nodes_to_qubits in the NamedTopology class and use it here instead of this hack, especially because the lambda argument in the qubit placer class below is also not serializable.

Note that we already have nodes_to_gridqubits method on TiltedSquareLattice and have an open issue to add a similar nodes_to_linequbits method for the LineTopology.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hmm I don't consider this a hack.

NamedTopology.nodes_to_qubits may seem obvious for LineTopology and TiltedSquareLattice but (a) maybe that isn't so and (b) it would be non-obvious for other topology types.

(a) what if you happened to use a different qubit type, especially for LineTopology. You could have written your circuit using GridQubit(x, 0) or even GridQubit(x, y) for fixed y where you know that that defines an actual list on the device. What if you used NamedQubit's a, b, c, ...,

(b) Imagine additional named topologies that don't have a corresponding qubit type. Consider a hexagonal topology. what would be the nodes_to_qubits function? there are multiple ways to handle this https://www.redblobgames.com/grids/hexagons/ including mapping to gridqubits or defining a new coordinate system.

nodes_to_gridqubits and nodes_to_linequbits as non-abstract, non-interface, topology-specific methods makes sense. you can even imagine having LineTopology.nodes_to_gridqubits. But I think it would be too tricky to have as part of the NamedTopology interface

Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. I imagine there would be more places where we'll end up needing the topo_node_to_qubit_func parameter, i.e. all qubit placement / routing related utilities. It would be nice if we can avoid passing this additional parameter around by putting the mapping in the named topology itself.
  2. Do we imagine a use case of NamedTopology's where the topology itself should exist but the mapping routines need not exist because they are hard to write? IIUC, whenever someone creates a new NamedToplogy (eg: the hexagonal topology), they'd also have to figure out a way to map the topology to qubits for it to be useful. If that's the case, then it's just a matter of whether we should do it outside the class or inside.
  3. Assuming 2 above is true, we can deal with the problem of different mappings for a specific named topology by either deriving from the topology class and overriding the nodes_to_qubits method, or by parameterising the topology class with flags to control the placement behavior.
  4. Moving the placement logic to named topology has an added benefit that it makes it serializable. I'm curious to know your perspective on why that is not so important?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The true solution would be to let you define Circuits with arbitrary qubit identifiers, and then you could use integers or tuples of integers or whatever is appropriate for your problem. The current construct is a shim around that. It's like if in classical programming you couldn't use variable names and had to use hardcoded memory addresses. Most of what follows expands on this idea.

  1. any qubit mapping worth its salt shouldn't care about the form of the input "qubit" (logical qubit, node, whatever). The shim is that the input qubits must be cirq.Qid even if that doesn't make complete sense.
  2. No; it's not straightforward like that. There are qubit mapping strategies that are completely agnostic to the input and output topologies; some that make make some assumptions (e.g. maybe specialize for a linear or planar target topo); and some basic strategies that make very strict assumptions (like the Naive + offset placer). That's why the mapping should happen in the mapping strategy.
  3. Philosophically, this would be a step in the wrong direction. I want to be able to describe ~~problem~~ topologies independent of their implementation.
  4. The placement logic is serializable. If I tell you that I used NaiveQubitPlacer to run my problem, you'd know what strategy I used. If I gave you a json document with RandomQubitPlacer and the target topology, you'd know what strategy I used.

There's just this annoying thing you need to "integrate out" where I'd like to describe my problem in terms of namedtopology nodes but have to use gridqubit or linequbit

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

some semi-relevant hearsay #4713



@lru_cache()
def _cached_get_placements(
problem_topo: 'cirq.NamedTopology', device: 'cirq.Device'
) -> List[Dict[Any, 'cirq.Qid']]:
"""Cache `cirq.get_placements` onto the specific device."""
return get_placements(
big_graph=_Device_dot_get_nx_graph(device), small_graph=problem_topo.graph
)


def _get_random_placement(
problem_topology: 'cirq.NamedTopology',
device: 'cirq.Device',
rs: np.random.RandomState,
topo_node_to_qubit_func: Callable[[Any], 'cirq.Qid'] = default_topo_node_to_qubit,
) -> Dict['cirq.Qid', 'cirq.Qid']:
"""Place `problem_topology` randomly onto a device.

This is a helper function used by `RandomDevicePlacer.place_circuit`.
"""
placements = _cached_get_placements(problem_topology, device)
if len(placements) == 0:
raise CouldNotPlaceError
random_i = rs.randint(len(placements))
placement = placements[random_i]
placement_gq = {topo_node_to_qubit_func(k): v for k, v in placement.items()}
return placement_gq


class RandomDevicePlacer(QubitPlacer):
def __init__(
self,
topo_node_to_qubit_func: Callable[[Any], cirq.Qid] = default_topo_node_to_qubit,
):
"""A placement strategy that randomly places circuits onto devices.

Args:
topo_node_to_qubit_func: A function that maps from `cirq.NamedTopology` nodes
to `cirq.Qid`. There is a correspondence between nodes and the "abstract" Qids
used to construct the un-placed circuit. `cirq.get_placements` returns a dictionary
mapping from node to Qid. We use this function to transform it into a mapping
from "abstract" Qid to device Qid. By default: nodes which are tuples correspond
to `cirq.GridQubit`s; otherwise `cirq.LineQubit`.

Note:
The attribute `topo_node_to_qubit_func` is not preserved in JSON serialization. This
bit of plumbing does not affect the placement behavior.
"""
self.topo_node_to_qubit_func = topo_node_to_qubit_func

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 onto a device via `cirq.get_placements` with
randomized selection of the placement each time.

This requires device information to be present in `shared_rt_info`.

Args:
circuit: The circuit.
problem_topology: The topologies (i.e. connectivity) of the circuit.
shared_rt_info: A `cg.SharedRuntimeInfo` object that contains a `device` attribute
of type `cirq.Device` to enable placement.
rs: A `RandomState` as a source of randomness for random placements.

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

Raises:
ValueError: If `shared_rt_info` does not have a device field.
"""
device = shared_rt_info.device
if device is None:
raise ValueError(
"RandomDevicePlacer requires shared_rt_info.device to be a `cirq.Device`. "
"This should have been set during the initialization phase of `cg.execute`."
)
placement = _get_random_placement(
problem_topology, device, rs=rs, topo_node_to_qubit_func=self.topo_node_to_qubit_func
)
return circuit.unfreeze().transform_qubits(placement).freeze(), placement

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

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

def __repr__(self) -> str:
return "cirq_google.RandomDevicePlacer()"

def __eq__(self, other):
if isinstance(other, RandomDevicePlacer):
return True
78 changes: 77 additions & 1 deletion cirq-google/cirq_google/workflow/qubit_placement_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# 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 pytest

import cirq
import cirq_google as cg
Expand All @@ -36,6 +37,81 @@ def test_naive_qubit_placer():
)
assert circuit is not circuit2
assert circuit == circuit2
assert all(q in cg.Sycamore23.qubit_set() for q in circuit.all_qubits())
assert all(q in cg.Sycamore23.qubit_set() for q in circuit2.all_qubits())
for k, v in mapping.items():
assert k == v


def test_random_device_placer_tilted_square_lattice():
topo = cirq.TiltedSquareLattice(4, 2)
qubits = sorted(topo.nodes_to_gridqubits().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 not all(q in cg.Sycamore23.qubit_set() for q in circuit.all_qubits())

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


def test_random_device_placer_line():
topo = cirq.LineTopology(8)
qubits = cirq.LineQubit.range(8)
circuit = cirq.testing.random_circuit(qubits, n_moments=8, op_density=1.0, random_state=52)

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


def test_random_device_placer_repr():
cirq.testing.assert_equivalent_repr(cg.RandomDevicePlacer(), global_vals={'cirq_google': cg})


def test_random_device_placer_bad_device():
topo = cirq.LineTopology(8)
qubits = cirq.LineQubit.range(8)
circuit = cirq.testing.random_circuit(qubits, n_moments=8, op_density=1.0, random_state=52)
qp = cg.RandomDevicePlacer()
with pytest.raises(ValueError, match=r'.*shared_rt_info\.device.*'):
qp.place_circuit(
circuit,
problem_topology=topo,
shared_rt_info=cg.SharedRuntimeInfo(run_id='1'),
rs=np.random.RandomState(1),
)


def test_random_device_placer_small_device():
topo = cirq.TiltedSquareLattice(3, 3)
qubits = sorted(topo.nodes_to_gridqubits().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)
)
qp = cg.RandomDevicePlacer()
with pytest.raises(cg.CouldNotPlaceError):
qp.place_circuit(
circuit,
problem_topology=topo,
shared_rt_info=cg.SharedRuntimeInfo(run_id='1', device=cg.Foxtail),
rs=np.random.RandomState(1),
)