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

Optimize is_measurement protocol #4140

Merged
merged 11 commits into from
Jun 8, 2021
Merged

Conversation

smitsanghavi
Copy link
Collaborator

@smitsanghavi smitsanghavi commented May 27, 2021

Current is_measurement implementation is slow because:

  • It delegates to getting all the measurement keys first and then checks for non-emptyness
  • Measurement key protocol iterates through each operation in order and decomposes each until it finds _measurement_key_ or _measurement_keys_ implemented.
    • Decomposing can be costly and most of the time would be spent on non-measurement operations.
  • There is no early exit. Even if we find a measurement, we aren't declaring victory, we wait to gather all the keys and create the full set. This is disastrous for big circuits with terminal measurements.

Made a few optimizations to move this faster:

  • Added _is_measurement_ magic methods on some of the objects for slightly faster evaluation
    • This would give even more benefit if some expensive-to-decompose gates can explicitly set it to False, enabling us to skip it entirely regardless of whether it is decomposable or not.
  • Instead of a depth-first-search approach as above (here depth=each level of decompose), we use breadth-first-search to make sure we do all the cheap _is_measurement_ checks before attempting any decompose
  • Early escape. As soon as we find a measurement, we declare victory and return.
  • Reverse the decomposed. At every "level" we iterate in the reverse order of moments since measurements are typically terminal (or terminal-ish)

This implementation may perform worse than the existing one on circuits that have many many levels of nested CircuitOperations or other composite gates at the start of the circuit and only these ones have measurements. This seems like a pretty non-typical case.

@google-cla google-cla bot added the cla: yes Makes googlebot stop complaining. label May 27, 2021
@smitsanghavi smitsanghavi marked this pull request as ready for review May 27, 2021 07:26
@smitsanghavi smitsanghavi requested review from cduck, vtomole and a team as code owners May 27, 2021 07:26
assert c_op._is_measurement_() is NotImplemented
# Memoize `has_measurements` in the circuit.
assert circuit.has_measurements()
assert c_op._is_measurement_() is True
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a red flag to me - memoization should not change the public properties of an object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True, but _is_measurement_ isn't really a public property. cirq.is_measurement still gives the same answer (line 88) before memoization.
_is_measurement_ is just the first (assumed to be efficient) strategy in calculating the protocol result. It should be allowed to return NotImplemented and let the protocol fallback to alternate strategies (i.e. decomposition).

Copy link
Collaborator

Choose a reason for hiding this comment

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

See my other comment - not memoizing in _is_measurement_ prevents it from becoming efficient unless the user calls has_measurement, which is inconvenient behavior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense. See my other reply for the caveat in this approach.

cirq-core/cirq/ops/raw_types.py Outdated Show resolved Hide resolved
cirq-core/cirq/protocols/measurement_key_protocol.py Outdated Show resolved Hide resolved
@@ -136,6 +136,11 @@ def are_all_measurements_terminal(self) -> bool:

# End of memoized methods.

def _is_measurement_(self) -> bool:
if self._has_measurements is None:
return NotImplemented
Copy link
Collaborator

Choose a reason for hiding this comment

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

As noted above - if _has_measurements is not set, this should set it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Calculating has_measurement from scratch can be very costly. _is_measurement_ cannot be expected to do the full circuit decomposition inside it, the protocol should handle it in the case where _is_measurement_ does not have an efficient answer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is why I'm concerned: for fc = FrozenCircuit(...) if someone invokes cirq.is_measurement(fc), then later invokes fc.has_measurement(), the result will not be memoized for the has_measurement call.

Ideally, something like this would work:

def _is_measurement_(self) -> bool:
    if self._has_measurements is None:
        self._has_measurements = cirq.is_measurement(self.unfreeze())
    return self._has_measurements

Deferring is_measurement handling to the Circuit class prevents infinite recursion in the _is_measurement_from_magic_method step of the protocol.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated to enable memoization. This comes with the downside that a CircuitOperation, once encountered, will now be immediately fully evaluated regardless of how much decomposing it needs. I was aesthetically hesitant to go this route since it deviates from a pure breadth-first search for a measurement. But memoizing the expensive is_measurement call is definitely a net win and worth it.

The is_measurement protocol might slow down a bit for circuits with many non-measurement terminal CircuitOperations, but these will probably be rare.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Loss of BFS is unfortunate...I have a nagging feeling that there's a clean solution that allows BFS to coexist with proper memoization, but the existing tangle of methods and expected behavior make implementing that solution infeasible :/

@@ -136,6 +136,11 @@ def are_all_measurements_terminal(self) -> bool:

# End of memoized methods.

def _is_measurement_(self) -> bool:
if self._has_measurements is None:
return NotImplemented
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is why I'm concerned: for fc = FrozenCircuit(...) if someone invokes cirq.is_measurement(fc), then later invokes fc.has_measurement(), the result will not be memoized for the has_measurement call.

Ideally, something like this would work:

def _is_measurement_(self) -> bool:
    if self._has_measurements is None:
        self._has_measurements = cirq.is_measurement(self.unfreeze())
    return self._has_measurements

Deferring is_measurement handling to the Circuit class prevents infinite recursion in the _is_measurement_from_magic_method step of the protocol.

assert c_op._is_measurement_() is NotImplemented
# Memoize `has_measurements` in the circuit.
assert circuit.has_measurements()
assert c_op._is_measurement_() is True
Copy link
Collaborator

Choose a reason for hiding this comment

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

See my other comment - not memoizing in _is_measurement_ prevents it from becoming efficient unless the user calls has_measurement, which is inconvenient behavior.

Copy link
Collaborator

@95-martin-orion 95-martin-orion left a comment

Choose a reason for hiding this comment

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

Approval granted, but we should either leave #4141 open or create a follow-up issue to disentangle BFS from memoization.

@smitsanghavi smitsanghavi added the automerge Tells CirqBot to sync and merge this PR. (If it's running.) label Jun 8, 2021
@CirqBot
Copy link
Collaborator

CirqBot commented Jun 8, 2021

Automerge cancelled: Only collaborators with write permission can use automerge.

@CirqBot CirqBot removed the automerge Tells CirqBot to sync and merge this PR. (If it's running.) label Jun 8, 2021
@95-martin-orion 95-martin-orion merged commit 3b315cd into quantumlib:master Jun 8, 2021
rht pushed a commit to rht/Cirq that referenced this pull request May 1, 2023
* Optimize is_measurement protocol

* Optimize is_measurement protocol

* No shortcut in circuit

* Change from recursive to iterative

* add test and remove vals_to_decompose arg

* FrozenCircuit uses evaluates fully for memoization

* Coverage for tagged non-GateOperation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Makes googlebot stop complaining.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants