Skip to content

[WIP] New algorithm for RB using transpiled Cliffords#851

Closed
merav-aharoni wants to merge 94 commits into
qiskit-community:mainfrom
merav-aharoni:2_qubits_rb
Closed

[WIP] New algorithm for RB using transpiled Cliffords#851
merav-aharoni wants to merge 94 commits into
qiskit-community:mainfrom
merav-aharoni:2_qubits_rb

Conversation

@merav-aharoni
Copy link
Copy Markdown
Contributor

@merav-aharoni merav-aharoni commented Jul 19, 2022

Summary

We implement a new algorithm for RB, 1 and 2 qubits.

Details and comments

Here are the main ideas behind the implementation:

  1. We transpile all the Cliffords in advance and store them in a file. The file that generates the transpiled Cliffords is qiskit_experiments/library/randomized_benchmarking/generate_transpiled_circuits.py. The transpiled Cliffords are stored in the same directory, in a file named transpiled_circs_[1|2]_<basis_gates>.qpy.
  2. Every Clifford is represented by a number. We store a list of the compositions of Cliffords represented as numbers. For example, if Clifford1.compose(Clifford2) == Clifford3, then we conceptually, we store {(1, 2) : 3}.
  3. Similarly, we store for each number representing a Cliffords, the number representing the inverse Clifford. This data, along with the data in (2) is stored in the file qiskit_experiments/library/randomized_benchmarking/clifford_data.py. The data is generated by the file, in the same directory, create_clifford_map.py.
  4. For (2) above, we don't actually store the map, but only the results of the compose in an array. This is more efficient in performance. The result is found using the indices of the input Cliffords. Similarly, for (3) we also only store the array of inverse numbers.
  5. For the compose, we don't actually store the full compose table of all-cliffords X all-cliffords. Instead, we define an array of single-gate-cliffords. This comprises all Cliffords that consist of a single gate. These arrays are stored in clifford_data.py as well. There are 8 such Cliffords for 1-qubit, and 20 such Cliffords for 2-qubits. It is sufficient to store the compose table of all-cliffords X single-gate-cliffords, since for every clifford on the right hand side, we can break it down into single gate Cliffords, and do the composition one at a time. This greatly reduces the storage space for the array of composition results (from O(n^2) to O(n)).
  6. We currently support two sets of basis gates: {rz, sx, cx} and {s, h, x, cx}. To support a new set of basis gates, one must first generate the two files mentioned above.

This PR supersedes PR #825. Therefore I will close that PR.

merav-aharoni and others added 30 commits May 25, 2022 20:50
…enerate once all 24 transpiled Cliffords. Then, for every rb_circuit, select Cliffords at random and compose them to a circuit
…. Added parameter to all calls to compose to use inplace=True. Removed redundant method generate_all_transpiled_clifford_circuits
…use front=True, because I assume front=False when creating the circuits
New algorithm for generating Clifford circuits for single qubit.
…cuits, generate_1q_transpiled_clifford_circuits
… otherwise randomization is not identical in the two experiments
…ing called by the child class CurveAnalaysis. Added parameter to rb_experiment to determine whether to use the old algorithm or new one
…cuits. Transformed interleaved element into a transpiled clifford circuit. Added relevant tests
@merav-aharoni
Copy link
Copy Markdown
Contributor Author

@ShellyGarion , I believe I addressed all your comments, except for one where I asked a question. Please review again, in particular the new tests.

Copy link
Copy Markdown
Collaborator

@nkanazawa1989 nkanazawa1989 left a comment

Choose a reason for hiding this comment

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

Thanks for proposing new framework of performant RB circuit generation. Seems like the speedup is very promising, but we should improve the implementation from software design viewpoint.

First of all, new framework introduce tight coupling to the file system, and it generates cache files inside the software (this must be avoided). You should at least use a temp location provided by the operating system, and also should be able to create multiple cache files for different basis gates -- if we really want to and decide to rely on such non in-memory cache mechanism. Apart from this, you should be careful not to break conventional workflow (requiring pre cache generation), and not to restrict capability of experiment (i.e. <2Q).

Perhaps I would employ python descriptor to implement such mechanism and let the descriptor generate cache data when an element is called for the first time. Anyways, I suggest you to start from writing a design doc so that we can be on the same page before reviewing a huge PR. Here is the public Qiskit RFCs and I think this can be discussed publicly.


def clifford_1_qubit(self, num):
@classmethod
def clifford_1_qubit(cls, num):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I was planning to deprecate CliffordUtils because it uses a class just as a namespace and doesn't make any difference from defining a set of functions in a module unless you need to define a subclass for a particular RB experiment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We could remove CliffordUtils as a class, and just keep the methods. I think it is nicer and more expressive to have all these methods grouped in a class, to indicate the common functionality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When the method does not access any class-relevant data (i.e. doesn't use the cls parameter) you can make is a @staticmethod instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Right, but still these are just a collection of functions. I wonder why these must be class methods, i.e.

from qiskit_experiments.library.randomized_benchmarking import clifford_utils

clifford_utils.clifford_1_qubits(...)

v.s.

from qiskit_experiments.library.randomized_benchmarking.clifford_utils import CliffordUtils

CliffordUtils.clifford_1_qubits(...)

Comment thread qiskit_experiments/library/randomized_benchmarking/clifford_utils.py Outdated
(2, 2, 3, 3, 4, 4),
]
GENERAL_CLIFF_LIST = ["id", "h", "sdg", "s", "x", "sx", "sxdg", "y", "z", "cx"]
TRANSPILED_CLIFF_LIST = ["sx", "rz", "cx"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This assumes a particular IBM-ish backend. Other providers may use different basis set depending on their hardware architecture, or, even us may want to use different set, e.g. "ecr" rather than "cx" which are locally equivalent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is difficult to make the code efficient without optimizing it for a specific set of basis gates.
For non-IBM backend, one can change the basis gates, and pre-generate the data files (there is a code for this in generate_transpiled_circuits.py).
As for an ecr gate, it is currently not one of the gates that the Clifford class can handle:
https://qiskit.org/documentation/stubs/qiskit.quantum_info.Clifford.html

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Right, but Qiskit is backend agnostic. This violates very important policy of Qiskit.

clifford_single_gate_to_num[(gate.name, qubit_as_str)] = num
else:
print("not found")
file.write(f"CLIFF_SINGLE_GATE_MAP_1Q = {clifford_single_gate_to_num}\n")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like file is not defined within current scope. Also you should NOT induce strong coupling to file system. This makes code unreadable, and also hardly guarantees multiple platform support.

By the way is there any special reason to generate text data? This is expensive to load because of the deserialization overhead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  1. Do you think it would be better if the user defined the path to the required files?
  2. What do you suggest instead of text data?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  1. Some tmp file dir, or qiskit experiments data dir (e.g. in app data in Mac) -- at least somewhere not in the package. Note that qiskit provider code is kind of doing this, i.e. it saves token. However this should be usually avoided (local file system).
  2. Depends on data structure.

Comment thread qiskit_experiments/library/randomized_benchmarking/rb_experiment.py Outdated
self._transpiled_cliff_circuits = {}
# transpiled clifford circuits for 1 and 2 qubits respectively
self._transpiled_cliff_circuits[1] = None
self._transpiled_cliff_circuits[2] = None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is also problematic because in principle we should be able to run 3Q+ RB though its outcome might not be statistically confident.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I could of course define this as an array. But I preferred to write it this way to stress that for now only 1 and 2 qubit -rb are supported. Many changes will be needed to support more than 2 qubits. One option is to keep the legacy code for 3 or more qubits, but I am not sure if that code worked for more than 2 qubits. @ShellyGarion - do you know if the previous version worked in this case?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Then this should be implemented as a subclass of StandardRB, i.e. StandardRB1Q and StandardRB2Q (because constructor argument is qubits: Sequence[int]). However, such subclasses still make the RB experiment different from standard execution model.

)
n = self.num_qubits
if self._transpiled_cliff_circuits[n] is None:
if os.path.isfile(transpiled_circs_file):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is no longer our standard experiment workflow. This seems like a research code, i.e. framework is performant but dedicated to very limited use case. We should be able to create circuit without caching. For example, we may run this experiment with multiple backends with different basis set.

@ShellyGarion ShellyGarion self-requested a review July 27, 2022 04:11
Copy link
Copy Markdown
Contributor

@ShellyGarion ShellyGarion left a comment

Choose a reason for hiding this comment

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

The main idea behind this improvement seems to be correct.
I would still suggest to add some more tests the data in clifford_data.py has been generated correctly and satisfies the group properties.

  • In CLIFF_COMPOSE_DATA_1Q each row and each column is a permutation of [0,...,23]
  • CLIFF_INVERSE_DATA_1Q is a permutation of [0,...,23]
  • In CLIFF_COMPOSE_DATA_2Q each row is a permutation
  • CLIFF_INVERSE_DATA_2Q is a permutation

@merav-aharoni
Copy link
Copy Markdown
Contributor Author

Hi @nkanazawa1989 , thank you for your careful comments. As you suggested, I will start by writing a design doc, along with some of the questions you raised, which are also questions I had, in particular with regard to storing data in files and regarding the generality of the code. This way we can discuss these issue in a wider forum. I don't think https://github.com/Qiskit/rfcs is the right place for this document, as the specification there is for " 'substantial' changes to the Qiskit meta-package". I recall we once had a place (in Box?) for such documents. Does anyone recall where that is?


# basis_gates must be set for randomized benchmarking
transpiler_options = {
"basis_gates": ["rz", "sx", "cx"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not use this set as a default if the user does not pass this option?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is possible. The question is whether we think it is better to have a default, or to make sure the user specifies their needs. @nkanazawa1989 , @ShellyGarion ? what do you think?

Copy link
Copy Markdown
Collaborator

@nkanazawa1989 nkanazawa1989 Jul 27, 2022

Choose a reason for hiding this comment

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

RB experiment itself should be general. Experiment must define a protocol, not executable (target) code.


def clifford_1_qubit(self, num):
@classmethod
def clifford_1_qubit(cls, num):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When the method does not access any class-relevant data (i.e. doesn't use the cls parameter) you can make is a @staticmethod instead.

name = inst.name
gates_with_delay = basis_gates.copy()
gates_with_delay.append("delay")
single_gate_map = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A slightly more elegant way of doing this:

cliff_single_gate_maps = {1: CLIFF_SINGLE_GATE_MAP_1Q, 2: CLIFF_SINGLE_GATE_MAP_2Q}
single_gate_map = cliff_single_gate_maps[rb_num_qubits]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Answering to the comment above regarding @staticmethod: this method actually calls another class method: cls.clifford_1_qubit_circuit(num) so I think it must be a classmethod. Or am I missing something? I did change a couple of other methods to static, as you suggested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Regarding your second comment - I agree. Nicer!

if set(basis_gates).issubset(set(cls.TRANSPILED_CLIFF_LIST)):
if name in {"sx", "cx"}:
map_index = name
elif name == "delay":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can do this check once in the beginning of the method

Comment thread qiskit_experiments/library/randomized_benchmarking/clifford_utils.py Outdated
suffix += "_" + basis_gates[-1]
circs_file_name = "/transpiled_circs_" + str(num_qubits) + "q" + suffix + ".qpy"
root_dir = os.path.dirname(os.path.abspath(__file__))
transpiled_circs_file = root_dir + circs_file_name
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using os.path.join() is considered more robust than using +

Comment thread qiskit_experiments/library/randomized_benchmarking/create_clifford_map.py Outdated
num = cliff_to_num_2q[cliff.__repr__()]
# qubit_as_str is not really necessary. It is only added to be consistent
# with the representation for 2 qubits
qubit_as_str = "[" + str(qubit) + "]"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can also do qubit_as_str = f"[{qubit}]"

cliff = cliff1.adjoint()
invs[i] = cliff_to_num_1q[cliff.__repr__()]

file.write("CLIFF_COMPOSE_DATA_1Q = [")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

even if storing text and not bytes (bytes might be better) you can use json.dump instead of manually writing what is essentially a json data file.

max_qubit = max(self.physical_qubits) + 1
all_rb_circuits = []

if is_interleaved:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This breaks the current structure where StandardRB did not know about InterleavedRB. Can't say if it's good or bad, but I usually prefer ensuring classes know as little about each other as possible (so if someone wants to understand interleaved RB, all the relevant interleaved logic will be in the interleaved rb file).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree with this comment. I also was not sure if this was best. The other option would be to copy the _build methods to InterleavedRB. There would be a lot of code duplication, because I didn't see any reasonable way to break these down into smaller methods.
But if you think this is better, I will make this change.

@merav-aharoni
Copy link
Copy Markdown
Contributor Author

Based on @nkanazawa1989 's comments regarding usage of files, I suggest the following: we add two parameters to StandardRB (and to interleavedRB): transpiled_cliffords_file and clifford_data_file. The user will specify the path for the relevant files in these parameters. For each of these files, if it exists, the code will use it, and if it doesn't exist, the code will invoke the relevant script to create the file using the basis_gates. These parameters will be mandatory. We will provide the default versions for these files as they are now.
I would appreciate your input, @nkanazawa1989 , @ShellyGarion , @gadial .

@nkanazawa1989
Copy link
Copy Markdown
Collaborator

nkanazawa1989 commented Jul 27, 2022

I still don't like the idea of using local files. I think current issues are

  • Partly transpiled circuit data set must be pre-generated for performance, i.e. this is virtual extension of basis gates. This is no longer the standard experiment workflow and this turns the RB experiment into a special case. This means a user needs extra learning for this particular experiment, and one may hesitate migrating from Ignis to QE.
  • Generated data must be stored for next execution, preferably the data must live after program is terminated. For example, you may run the experiment multiple times with automated system, e.g. crontab/shell script in linux. However, this file system coupling increases test complexity and also maintenance overhead.
  • New mechanism must assume a particular basis gates to pre-generate the virtual basis gates. This tightly couples experiment to a particular target backend.

I would switch to the StagedPassManager and implement the backend specific transpile routine in the qiskit ibm provider. This allows you to assume IBM-specific basis gates, and storing pre generated circuit QPY as a part of the qiskit ibm provider package. Then, QE SRB can call IBM provider's RB-transpiler if the backend is provided by the IBM provider. Otherwise, it will call legacy transpile routine.

I feel this discussion is no longer a part of code review. I hope you will write a design doc (before continue the code cleanup), so that we can first get concrete design to implement.

(EDIT)

I don't complain the general idea of how it works. I think this is reasonable and scalable approach. The point is how we "standardize" the workflow of all built-in experiments.

@itoko
Copy link
Copy Markdown
Contributor

itoko commented Jul 28, 2022

I don't fully understand the background of this PR, but it seems to me that this PR address a performance issue on RB experiments. Is there any profile data that shows which part is the bottleneck in the current code? I think we need to discuss what approach we should take based on that.

@yaelbh
Copy link
Copy Markdown
Collaborator

yaelbh commented Jul 28, 2022

As we've already discussed in various contexts, an experiment has several different functionalities: build (transpiled or non-transpiled) circuits, run them, analyze, save to the database. We've already noticed that the circuit building should become more independent (beyond override of the _transpiled_circuits method). By "more indpendent", I mean the ability of a user to specify the (usually transpiled) circuits as an input to the run. Then I'd imagine some procedure that looks roughly like this:

  • Outside of the experiment flow, outside of BaseExperiment.run and everything - a user saves whatever is required for her to generate transpiled RB circuits (can be e.g. the circuits themselves, or Clifford decompostions). She does it in her file system, in the location that she chooses. She can write her own code for this, and also there can be code in qiskit-experiments to help her do it, but probably as some utility and not as part of BaseExperiment or even of StandardRB. The qiskit-experiments code can refrain from writing to files; instead it returns Python objects, which the user can dump by herself.
  • The user generates the transpiled circuits. This again happens outside of the main flow, and can even be done by code that the user writes.
  • The user runs the experiment, with the transpiled circuits specified in the input.

Other places where we've recently encountered the same topic:

  1. The request for modularity came up, to my understanding, in conversations of @chriseclectic with users.
  2. PR [WIP] Add caching of transpiled circuit generation to BaseExperiment #815 that @chriseclectic started and @ItamarGoldman is about to continue - about circuit caching. Note by the way that Haggai and I would like a follow-up PR to save the cache to a file, which can be loaded later. So we see that not only circuit caching - also file handling is a recurring issue.
  3. @ItamarGoldman has suggested to pre-transpile say 1000 RB circuits, then in every execution sample one of them.
  4. In the whole-backend PR [WIP] Functions to facilitate experiments on entire backends #859, I build something I've named BasicExperiment, which is an experiment whose only purpose is to store transpiled circuits.

@merav-aharoni
Copy link
Copy Markdown
Contributor Author

Hi @itoko , thanks for looking at this issue. Profiling results for the existing rb can be seen in https://github.ibm.com/MERAV/prof_qiskit_exps/tree/main/profiling/output.
I presented these results in one of the experiments squad meeting. The main bottleneck was identified as transpile. This solution was also discussed in one of the squad meetings. I can try to find the recording, if you are interested.

@yaelbh
Copy link
Copy Markdown
Collaborator

yaelbh commented Jul 28, 2022

To my previous comment, add:
5. @coruscating has asked, in the context of QV: "Would be nice to have standalone functions in experiments to help with playing around with the data (for example method to allow loading custom circuits while keeping analysis the same)".

@merav-aharoni merav-aharoni changed the title New algorithm for RB using transpiled Cliffords [WIP] New algorithm for RB using transpiled Cliffords Aug 17, 2022
@merav-aharoni
Copy link
Copy Markdown
Contributor Author

Superseded by https://github.com/Qiskit/qiskit-experiments/tree/feature/rb_speedup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants