[WIP] New algorithm for RB using transpiled Cliffords#851
[WIP] New algorithm for RB using transpiled Cliffords#851merav-aharoni wants to merge 94 commits into
Conversation
…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
… the previous version of rb_experiment.
New algorithm for generating Clifford circuits for single qubit.
…cuits, generate_1q_transpiled_clifford_circuits
…ling_single_qubit
… 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
…s change regarding _format_data
…eters for test_full_sampling_single_qubit
…lse, so that all tests will pass
…ding support for interleave
…cuits. Transformed interleaved element into a transpiled clifford circuit. Added relevant tests
|
@ShellyGarion , I believe I addressed all your comments, except for one where I asked a question. Please review again, in particular the new tests. |
nkanazawa1989
left a comment
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(...)| (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"] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
- Do you think it would be better if the user defined the path to the required files?
- What do you suggest instead of text data?
There was a problem hiding this comment.
- 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).
- Depends on data structure.
| 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 |
There was a problem hiding this comment.
This is also problematic because in principle we should be able to run 3Q+ RB though its outcome might not be statistically confident.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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_1Qeach row and each column is a permutation of [0,...,23] CLIFF_INVERSE_DATA_1Qis a permutation of [0,...,23]- In
CLIFF_COMPOSE_DATA_2Qeach row is a permutation CLIFF_INVERSE_DATA_2Qis a permutation
|
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 |
|
|
||
| # basis_gates must be set for randomized benchmarking | ||
| transpiler_options = { | ||
| "basis_gates": ["rz", "sx", "cx"], |
There was a problem hiding this comment.
Why not use this set as a default if the user does not pass this option?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 = ( |
There was a problem hiding this comment.
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]
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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": |
There was a problem hiding this comment.
You can do this check once in the beginning of the method
| 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 |
There was a problem hiding this comment.
Using os.path.join() is considered more robust than using +
| 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) + "]" |
There was a problem hiding this comment.
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 = [") |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
|
Based on @nkanazawa1989 's comments regarding usage of files, I suggest the following: we add two parameters to |
…_single_clifford and use transpile() directly instead
|
I still don't like the idea of using local files. I think current issues are
I would switch to the 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. |
|
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. |
|
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
Other places where we've recently encountered the same topic:
|
|
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. |
|
To my previous comment, add: |
|
Superseded by |
Summary
We implement a new algorithm for RB, 1 and 2 qubits.
Details and comments
Here are the main ideas behind the implementation:
qiskit_experiments/library/randomized_benchmarking/generate_transpiled_circuits.py. The transpiled Cliffords are stored in the same directory, in a file namedtranspiled_circs_[1|2]_<basis_gates>.qpy.Clifford1.compose(Clifford2) == Clifford3, then we conceptually, we store{(1, 2) : 3}.qiskit_experiments/library/randomized_benchmarking/clifford_data.py. The data is generated by the file, in the same directory,create_clifford_map.py.all-cliffords X all-cliffords. Instead, we define an array ofsingle-gate-cliffords. This comprises all Cliffords that consist of a single gate. These arrays are stored inclifford_data.pyas well. There are 8 such Cliffords for 1-qubit, and 20 such Cliffords for 2-qubits. It is sufficient to store the compose table ofall-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)).This PR supersedes PR #825. Therefore I will close that PR.