forked from NVIDIA/cuda-quantum
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPasses.td
More file actions
1066 lines (924 loc) · 42.9 KB
/
Passes.td
File metadata and controls
1066 lines (924 loc) · 42.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/********************************************************** -*- tablegen -*- ***
* Copyright (c) 2022 - 2025 NVIDIA Corporation & Affiliates. *
* All rights reserved. *
* *
* This source code and the accompanying materials are made available under *
* the terms of the Apache License 2.0 which accompanies this distribution. *
******************************************************************************/
#ifndef CUDAQ_OPT_OPTIMIZER_TRANSFORMS_PASSES
#define CUDAQ_OPT_OPTIMIZER_TRANSFORMS_PASSES
include "mlir/Pass/PassBase.td"
def AddWireset : Pass<"add-wireset", "mlir::ModuleOp"> {
let summary = "Adds a topology-less `quake.wire_set` to the module.";
let description = [{
Adds a `quake.wire_set` operation without any topological information to the
ModuleOp.
}];
}
def AssignWireIndices : Pass<"assign-wire-indices", "mlir::func::FuncOp"> {
let summary = "Replaces `null_wire` with `borrow_wire` from a `wire_set`.";
let description = [{
Replaces all instances of `quake.null_wire` with `quake.borrow_wire` from a
common `quake.wire_set` without any topological information. Each wire is
assigned a unique identifier (the index into the `quake.wire_set`) through
this process.
}];
}
def ApplyControlNegations :
Pass<"apply-control-negations", "mlir::func::FuncOp"> {
let summary =
"Replace all `Ops` with negated controls with normal controls and `XOp`s.";
let description = [{
For every quantum operation with a negative control, replace that operation
with an X operation on the control qubit, the controlled operation with
positive polarity, and a final X operation on the control qubit.
}];
}
// ApplyOpSpecialization is a module pass because it may modify the ModuleOp
// and add new FuncOps.
def ApplySpecialization : Pass<"apply-op-specialization", "mlir::ModuleOp"> {
let summary =
"Replace quake.apply with conventional calls to specialized functions.";
let description = [{
The quake.apply op allows quake kernels to be called with implicit
specialization of the function itself. For example, a user-defined kernel
can be called with an optional veq of control qubits. These extra control
qubits implicitly create a new and distinct function that threads these
control qubits to each quantum op in the function.
The compute-action-opt command-line option is for debug and demonstration
purposes only.
}];
let constructor = "cudaq::opt::createApplyOpSpecializationPass()";
let options = [
Option<"computeActionOptimization", "compute-action-opt", "bool",
/*default=*/"true", "Enable the compute-action control optimization.">
];
}
def ArgumentSynthesis : Pass<"argument-synthesis", "mlir::func::FuncOp"> {
let summary = "Specialize a function by replacing arguments with constants";
let description = [{
This pass takes a list of functions and argument substitutions. For each
function in the list, the arguments to the function in the substitutions
list will be erased and replaced with a computed value (e.g., a constant)
provided in the substitution list. All arguments or some subset of arguments
may be substituted in this way.
To facilitate command-line testing, this pass can be run with the functions
suboption using filenames containing the argument substitutions. For
example, one might run
```console
cudaq-opt input.qke \
--argument-synthesis=functions="kernel1:subst1.qke,kernel2:subst2.qke"
```
where `kernel1`, `kernel2` are the names of functions and `subst1.qke` and
`subst2.qke` are quake source files contains arg_subst operations.
For running this pass from code, one can build the substitution code in a
std::string and use a prefix character '*' to indicate the text is inline
and not in a file.
```
kernel1:*"cc.arg_subst [0] { ... }"
```
}];
let options = [
ListOption<"funcList", "functions", "std::string",
"Function name and substitutions pairs (<func>:<filename>)">,
];
let dependentDialects = ["cudaq::cc::CCDialect", "mlir::LLVM::LLVMDialect",
"mlir::cf::ControlFlowDialect"];
}
def BasisConversionPass : Pass<"basis-conversion", "mlir::ModuleOp"> {
let summary = "Converts kernels to a set of basis operations.";
let description = [{
This pass takes as input a list of target (allowed) quantum operations.
It builds a conversion target, and uses it, together with the dialect
conversion driver, to try converting all operations into the defined basis.
The `basis` option takes a comma-separated list of elements identifying
specific quantum operations as input. Each element must have the
following format:
```
<op-name>(`(` [<number-of-controls> | `n`] `)` )?
```
Examples:
- `x` means targeting pauli-x operations without controls (aka, `not`)
- `x(1)` means targeting pauli-x operations with one control (aka, `cx`)
- `x(n)` means targeting pauli-x operation with unbounded number of controls
- `x,x(1)` means targeting both `not` and `cx` operations
}];
let options = [
ListOption<"basis", "basis", "std::string", "Set of basis operations">,
ListOption<"disabledPatterns", "disable-patterns", "std::string",
"Labels of decomposition patterns that should be filtered out">,
ListOption<"enabledPatterns", "enable-patterns", "std::string",
"Labels of decomposition patterns that should be used, all "
"other patterns are filtered out">,
];
}
def CheckKernelCalls : Pass<"check-kernel-calls", "mlir::func::FuncOp"> {
let summary = "Check calls between quantum kernels have been inlined.";
let description = [{
The aggressive inlining pass should fully inline any calls between quantum
kernels. Any residual calls will be because the call graph contains cycles.
This pass checks that there are no residual calls and print a diagnostic if
any are found.
}];
}
def CombineMeasurements :
Pass<"combine-measurements", "mlir::func::FuncOp"> {
let summary = "Extends mesurements on subveqs adds output names";
let description = [{
Replace a pattern such as:
```
func.func @kernel() attributes {"cudaq-entrypoint"} {
%1 = ... : !quake.veq<4>
%2 = quake.subveq %1, %c2, %c3 : (!quake.veq<4>, i32, i32) ->
!quake.veq<2>
%measOut = quake.mz %2 : (!quake.veq<2>) -> !cc.stdvec<!quake.measure>
}
```
with:
```
func.func @kernel() attributes {"cudaq-entrypoint", ["output_names",
"[[[0,[1,\22q0\22]],[1,[2,\22q1\22]]]]"]} {
%1 = ... : !quake.veq<4>
%measOut = quake.mz %1 : (!quake.veq<4>) -> !cc.stdvec<!quake.measure>
}
```
}];
let dependentDialects = ["cudaq::cc::CCDialect", "quake::QuakeDialect"];
}
def CombineQuantumAllocations :
Pass<"combine-quantum-alloc", "mlir::func::FuncOp"> {
let summary = "Combines quake alloca operations.";
let description = [{
Quake code may contain several distinct `quake.alloca` operations prior to
final code gen. This pass will combine them into a single allocation of
type `!quake.veq` with appropriate `quake.extract_ref` operations. The
combined allocation will be placed in the entry block and thus dominate all
potential uses.
This pass will only process `quake.alloca` operations at the top-level of
a function. It assumes all calls have been inlined, loops unrolled, etc.
If the function contains deallocations, these are combined as well. The
combined deallocation will be added to each exit block.
}];
let dependentDialects = ["cudaq::cc::CCDialect", "quake::QuakeDialect"];
}
def ConstPropComplex : Pass<"const-prop-complex", "mlir::func::FuncOp"> {
let summary = "Create and propagate complex constants.";
let description = [{
Rewrite the complex.CreateOp to complex.ConstantOp when possible.
Replace array pointer casts with element pointer casts.
}];
}
def ConvertToDirectCalls : Pass<"indirect-to-direct-calls", "mlir::ModuleOp"> {
let summary = "Convert calls to direct calls to Quake routines.";
let description = [{
Rewrite the calls in the IR so that they point to the generated code and not
to the entry point thunks that call back to the runtime. After this pass
quantum code will call other quantum code directly and without going
indirectly through the launch kernel runtime.
}];
}
def DelayMeasurements : Pass<"delay-measurements", "mlir::func::FuncOp"> {
let summary =
"Move measurements as late as possible";
let description = [{
Move measurements as late as possible. This is useful for a Base Profile
QIR program.
}];
let constructor = "cudaq::opt::createDelayMeasurementsPass()";
}
def DecompositionPass: Pass<"decomposition", "mlir::ModuleOp"> {
let summary = "Break down quantum operations.";
let description = [{
This pass performs decomposition over a set of operations by iteratively
applying decomposition patterns until either a fixpoint is reached or the
maximum number of iterations/rewrites is exhausted. Decomposition is
best-effort and does not guarantee that the entire IR is decomposed after
running this pass.
NOTE: The current implementation is conservative w.r.t global phase, which
means no decomposition will take place under the presence of controlled
`quake.apply` operations in the module.
}];
let options = [
ListOption<"disabledPatterns", "disable-patterns", "std::string",
"Labels of decomposition patterns that should be filtered out">,
ListOption<"enabledPatterns", "enable-patterns", "std::string",
"Labels of decomposition patterns that should be used, all "
"other patterns are filtered out">,
Option<"testConvergence", "test-convergence", "bool", /*default=*/"false",
"Test only: Fail pass on non-convergence to detect cyclic patterns">,
];
}
def DependencyAnalysis : Pass<"dep-analysis", "mlir::ModuleOp"> {
let summary = "Maps qubits and reorders operations based on dependency graph.";
let description = [{
A dependency graph is a Directed Acyclic Graph (DAG) where each node
represents an operation, and each edge represents a "depends on" relation
between that operation and another operation. For example, in the following
snippet the `x` operation depends on the `h` operation because it is applied
to the same qubit (`q`), so the `h` operation must happen before the `x`
operation:
```c++
cudaq::qubit q;
x(q);
h(q);
```
Once a dependency graph is created, it is then scheduled, assigning each
operation a virtual cycle. Operations that don't depend on each other may
be scheduled at the same cycle. However, an operation that depends on a
second operation must be scheduled after the second operation. The
scheduling algorithm tries to pack operations as densely as possible,
minimizing the number of cycles between operations.
From this dependency graph, we can calculate the lifetime of a qubit: from
the cycle in which it is first used through the cycle in which it is last
used. If two virtual qubits have non-overlapping lifetimes, they can be
assigned to the same physical qubit, as every virtual qubit is assumed to be
fully reset before release. Failure to fully reset virtual qubits before
release is undefinied behavior, and will likely lead to incorrect output
when running DependencyAnalysis.
}];
let dependentDialects = ["quake::QuakeDialect"];
let statistics = [
Statistic<"numVirtualQubits", "num-virtual-qubits",
"Number of virtual qubits used">,
Statistic<"numPhysicalQubits", "num-physical-qubits",
"Number of phyiscal qubits used">,
Statistic<"numCycles", "num-cycles",
"Length of kernel in cycles">,
];
}
def EraseNopCalls : Pass<"erase-nop-calls", "mlir::func::FuncOp"> {
let summary = "Erase calls to any builtin intrinsics that are NOPs.";
let description = [{
The code may contain marker function calls that do not generate any actual
code. These calls are NOPs that will be erased by this pass.
}];
}
def ExpandControlVeqs: Pass<"expand-control-veqs", "mlir::func::FuncOp"> {
let summary = "Expands veqs used as controls into individual qubits.";
let description = [{
Given an operation of the form
```mlir
quake.any [%veq] %r : (!quake.veq<n>, !quake.ref) -> ()
```
this pass will extract each qubit from `%veq%` and explicitly provide them
as qubits:
```mlir
%arg0 = quake.extract_ref %veq[0] : (!quake.veq<n>) -> !quake.ref
...
%argn = quake.extract_ref %veq[n] : (!quake.veq<n>) -> !quake.ref
quake.any [%arg0, ..., %argn] %0 : (!quake.ref, ..., !quake.ref, !quake.ref) -> ()
```
}];
}
def ExpandMeasurements : Pass<"expand-measurements"> {
let summary = "Expand multi-ref measurements to series on single refs.";
let description = [{
The `mx`, `my`, `mz` ops can take a list of qubits and/or veq arguments.
The target may only support measuring a single qubit however. This pass
expands these ops in list format into a series of measurements (including
loops) on individual qubits and into a single `std::vector<bool>` result.
The `reset` op can also take a veq argument and this pass will also expand
that to a series of `reset` operations on single qubits.
}];
let dependentDialects = ["cudaq::cc::CCDialect", "mlir::LLVM::LLVMDialect"];
let constructor = "cudaq::opt::createExpandMeasurementsPass()";
}
def FactorQuantumAllocations :
Pass<"factor-quantum-alloc", "mlir::func::FuncOp"> {
let summary = "Factors quake alloca operations.";
let description = [{
Quake code may contain `quake.alloca` operations that allocate vectors of
qubits (`!quake.veq`). This pass will factor these single allocations of
type `!quake.veq` and the associated `quake.extract_ref` operations into a
series of single qubit allocations, thereby eliminating the
`quake.extract_ref` operations.
If the function contains deallocations of quantum vectors (`veq`), these
will be replaced with a series of deallocations.
}];
let dependentDialects = ["quake::QuakeDialect"];
}
def GenerateDeviceCodeLoader : Pass<"device-code-loader", "mlir::ModuleOp"> {
let summary = "Generate device code loader stubs.";
let description = [{
Generate device code loader stubs which are used for code introspection
by the runtime.
}];
let dependentDialects = ["mlir::LLVM::LLVMDialect"];
let options = [
Option<"outputFilename", "output-filename", "std::string",
/*default=*/"\"-\"", "Name of output file.">,
Option<"generateAsQuake", "use-quake", "bool",
/*default=*/"true", "Output should be module in Quake dialect.">,
Option<"jitTime", "jit-compile", "bool",
/*default=*/"false", "Running pass at JIT compile time (default=false).">
];
}
def GenerateKernelExecution : Pass<"kernel-execution", "mlir::ModuleOp"> {
let summary = "Generate kernel execution code.";
let description = [{
Generate the kernel execution thunks. The kernel execution thunks allow
the control side (C++ code) to launch quantum kernels. This pass
generates the required glue code.
Specifying the alt-launch=2 option will generate different code that makes
use of library side argument conversion and the argument synthesis pass.
More generally, this option can be used when JIT compiling kernels on the
client/host/local processor.
There are multiple code generation kinds that are supported for flexibility
and streamlining the kernel launch process. These tend to be related to the
target and runtime environment the compiler is being run in and can involve
some technical issues that require deeper understanding of the entire
process. In general, it is not recommended for user's to change this value.
```
codegen kind description
0 Hybrid. A combination of 1 and 2 that allowed early and
streamlined JIT compilation but also supports return values
and dynamic parameters.
1 Client-server interchange format. Supports kernels that
return results and dynamic parameters.
2 Streamlined for JIT. The kernel will be converted to a
nullary function with no results. Return values from the
kernel are ignored, if present. All parameter values are to
be inlined by the JIT compiler, so this codegen kind does not
support any dynamic parameters.
```
}];
let dependentDialects = ["cudaq::cc::CCDialect", "mlir::LLVM::LLVMDialect"];
let options = [
Option<"outputFilename", "output-filename", "std::string",
/*default=*/"\"-\"", "Name of output file.">,
Option<"startingArgIdx", "starting-arg-idx", "std::size_t", /*default=*/"0",
"The starting argument index for the argsCreator.">,
Option<"codegenKind", "codegen", "std::size_t", /*default=*/"0",
"Set the kind of code to generate for the launches.">
];
}
def GetConcreteMatrix : Pass<"get-concrete-matrix", "mlir::func::FuncOp"> {
let summary =
"Replace the unitary matrix generator function with a constant matrix.";
let description = [{
Given a custom operation whose generator attribute is another function
within the module, such that if `LiftArrayAlloc` pass has run, there will
be a global constant within the module which holds the constant matrix
representation for the custom operation. This pass will find that global
variable and update the custom operation to directly point to it.
Example:
```mlir
module {
func.func @__nvqpp__mlirgen__function_foo_generator_1.bar(%arg0: !cc.stdvec<f64>) -> !cc.stdvec<complex<f64>> {
...
%0 = cc.address_of @__nvqpp__mlirgen__function_foo_generator_1.bar.rodata_0 : !cc.ptr<!cc.array<complex<f64> x 4>>
...
return %3 : !cc.stdvec<complex<f64>>
}
func.func @__nvqpp__mlirgen__function_kernel_1._Z8kernel_1v() {
%0 = quake.alloca !quake.ref
quake.custom_op @__nvqpp__mlirgen__function_foo_generator_1.bar %0 : (!quake.ref) -> ()
return
}
cc.global constant private @__nvqpp__mlirgen__function_foo_generator_1.bar.rodata_0 ((dense<[(0.000000e+00,0.000000e+00), (1.000000e+00,0.000000e+00), (1.000000e+00,0.000000e+00), (0.000000e+00,0.000000e+00)]> : tensor<4xcomplex<f64>>) : !cc.array<complex<f64> x 4>
}
```
The `quake.custom_op` call would be converted to
```mlir
func.func @__nvqpp__mlirgen__function_kernel_1._Z8kernel_1v() {
%0 = quake.alloca !quake.ref
quake.custom_op @__nvqpp__mlirgen__function_foo_generator_1.bar.rodata_0 %0 : (!quake.ref) -> ()
return
}
```
}];
}
// GlobalizeArrayValues must be a module pass because it may promoted array
// constants from functions to global constants (changes their scope).
def GlobalizeArrayValues : Pass<"globalize-array-values", "mlir::ModuleOp"> {
let summary = "Convert const_array ops to globals.";
let description = [{
Often a `const_array` op can be canonicalized into scalar constants that
are then constant propagated to their uses in the quake ops. When this
happens, the `const_array` may become unused and be eliminated.
However, there can also be cases where the `const_array` remains alive, such
as when it is used in a `state_init` op. In such cases, we may be able to go
ahead and replace the `const_array` with a global constant. This pass makes
such conversions.
}];
}
// LambdaLifting is a module pass because it may modify the ModuleOp and add
// new FuncOps.
def LambdaLifting : Pass<"lambda-lifting", "mlir::ModuleOp"> {
let summary = "Lift lambda expressions to global functions.";
let description = [{
This pass implements classical lambda lifting.
1. Eliminate all free ssa-values in the function by adding arguments.
2. Move the function to the global scope. (ModuleOp)
3. Convert all uses to CallOps as needed.
}];
let constructor = "cudaq::opt::createLambdaLiftingPass()";
}
def LiftArrayAlloc : Pass<"lift-array-alloc", "mlir::func::FuncOp"> {
let summary = "Convert constant arrays built on the stack to array values";
let description = [{
The bridge or other passes may generate inline code to build an array of
arithmetic types. This construction can involve quite a few CC dialect
operations and can "hide" what is really being done in the volume of that
code. This pass folds and lifts those memory operations into a constant
array value operation.
Example:
```mlir
%cst = arith.constant 5.000000e+00 : f64
%cst_0 = arith.constant 6.000000e+00 : f64
%cst_1 = arith.constant 7.000000e+00 : f64
%cst_2 = arith.constant 8.000000e+00 : f64
%0 = cc.alloca !cc.array<f64 x 4>
%1 = cc.compute_ptr %0[0] : (!cc.ptr<!cc.array<f64 x 4>>) -> !cc.ptr<f64>
cc.store %cst, %1 : !cc.ptr<f64>
%2 = cc.compute_ptr %0[1] : (!cc.ptr<!cc.array<f64 x 4>>) -> !cc.ptr<f64>
cc.store %cst_0, %2 : !cc.ptr<f64>
%3 = cc.compute_ptr %0[2] : (!cc.ptr<!cc.array<f64 x 4>>) -> !cc.ptr<f64>
cc.store %cst_1, %3 : !cc.ptr<f64>
%4 = cc.compute_ptr %0[3] : (!cc.ptr<!cc.array<f64 x 4>>) -> !cc.ptr<f64>
cc.store %cst_2, %4 : !cc.ptr<f64>
```
would be converted to
```mlir
%0 = cc.const_array [5.0, 6.0, 7.0, 8.0] : !cc.array<f64 x 4>
```
This converts a value in memory SSA form to an SSA value, so additional
uses must also be considered. For example, if the array is subsequently
updated or escapes the function, it cannot be replaced by a value. If
it is elements are accessed in a read-only way, it is a legal transform
and will enable further constant folding in other passes.
See the globalize array values pass for converting `const_array` values
to global constants. Conversion to globals is intentionally deferred to
allow constant propagation to take place correctly.
}];
let dependentDialects = ["mlir::complex::ComplexDialect"];
}
def LinearCtrlRelations : Pass<"linear-ctrl-form", "mlir::func::FuncOp"> {
let summary = "Removes control type values between quantum ops.";
let description = [{
In the value semantics, quantum gates may be factored in terms of control
qubits by the introduction of values of type `!quake.control`. These
relaxed constraints can be removed within the IR to get a linear and overly
constrained representation of the dataflow of the *logical* qubits. The
overly constrained representation may be better suited to certain
transformations.
The following example is a factored value semantics. The wire type value is
converted to a control type value, `%ctrl`, which is a proper SSA-value
until it is converted back to a wire type, `%new.0`.
```mlir
%ctrl = to_ctrl %old.0 : (!wire) -> !control
%3 = x [%ctrl] %1 : (!control, !wire) -> !wire
%4 = h [%ctrl] %2 : (!control, !wire) -> !wire
%5 = y [%ctrl] %3 : (!control, !wire) -> !wire
%new.0 = from_ctrl %ctrl : (!control) -> !wire
```
Linearizing these control type values increases the coarity of the quantum
operations and prevents reordering the operations without rewiring the
use-def chains of wire values in control positions.
```mlir
%3:2 = x [%0] %1 : (!wire, !wire) -> (!wire, !wire)
%4:2 = h [%3#0] %2 : (!wire, !wire) -> (!wire, !wire)
%5:2 = y [%4#0] %3#1 : (!wire, !wire) -> (!wire, !wire)
```
}];
}
def LoopNormalize : Pass<"cc-loop-normalize"> {
let summary = "Normalize classical compute (C++) loops.";
let description = [{
Transform a monotonic loop with constant step (slope) into an invariant loop
or, if the bounds are constant, a simple counted loop.
}];
let dependentDialects = ["mlir::arith::ArithDialect"];
let options = [
Option<"allowClosedInterval", "allow-closed-iterval", "bool",
/*default=*/"true", "Allow loop iterations on a closed interval.">,
Option<"allowBreak", "allow-early-exit", "bool", /*default=*/"true",
"Allow unrolling of loop with early exit (i.e. break statement).">
];
}
def LoopPeeling : Pass<"cc-loop-peeling"> {
let summary = "Peeling classical do-while loops.";
let description = [{
This loop peeling pass currently implements the transformation of a C++
do-while loop into a peeled body and while sequence.
```c++
do {
body;
} while (condition);
```
is transformed to
```c++
body;
while (condition) {
body;
}
```
Other cases of loop peeling may be implemented in the future.
}];
let dependentDialects = ["mlir::cf::ControlFlowDialect"];
}
def LoopUnroll : Pass<"cc-loop-unroll"> {
let summary = "Unroll classical compute (C++) loops.";
let description = [{
If a cc.loop op is a simple, constant counted loop, it can be fully
unrolled into <i>n</i> copies of the body of the loop.
The signal-failure-if-any-loop-cannot-be-completely-unrolled option controls
whether to signal a failure if all loops cannot be fully unrolled. This is
necessary when synthesizing quantum circuits from CUDA-Q kernels, such
as when generating a QIR base profile. A quantum circuit requires all loops
be completely unrolled.
}];
let dependentDialects = ["mlir::arith::ArithDialect",
"mlir::cf::ControlFlowDialect"];
let options = [
Option<"threshold", "maximum-iterations", "unsigned", /*default=*/"50",
"Maximum iterations to unroll.">,
Option<"signalFailure",
"signal-failure-if-any-loop-cannot-be-completely-unrolled", "bool",
/*default=*/"false", "Signal failure if pass can't unroll all loops.">,
Option<"allowBreak", "allow-early-exit", "bool", /*default=*/"false",
"Allow unrolling of loop with early exit (i.e. break statement).">
];
}
def LowerToCFG : Pass<"lower-to-cfg", "mlir::func::FuncOp"> {
let summary = "Erase CLoop, CIf, etc. ops, replacing them with a CFG.";
let description = [{
This pass converts high-level control flow ops to a more primitive basic
CFG structure.
For example, a `quake.loop` model of a C `for` loop
```mlir
quake.loop while {
// while code
quake.condition %cond
} do {
// loop body
} step {
// step body
}
// exit loop
```
will be rewritten to
```mlir
^bb23:
// while code
cf.cond_br %cond, ^bb24, ^bb26
^bb24:
// loop body
cf.br ^bb25
^bb25:
// step body
cf.br ^bb23
^bb26:
// exit loop
```
}];
let dependentDialects = [ "mlir::cf::ControlFlowDialect" ];
let constructor = "cudaq::opt::createLowerToCFGPass()";
}
def MappingFunc: Pass<"qubit-mapping-func", "mlir::func::FuncOp"> {
let summary = "Perform qubit mapping to account for connectivity constraints";
let description = [{
Some backends cannot support any-to-any multi-qubit operations, so this pass
performs mapping of the qubits by inserting the necessary swap operations
into the Quake IR.
Note 1: this pass requires strictly value semantics (in the form of wire
sets) for any quantum operations. It will throw an error if any memory
reference semantics are present on quantum operations.
Note 2: this pass can introduce ancilla qubits.
Note 3: as a result of note 2, if the IR contains no measurements, this pass
will inject measurements so that the post-mapping measurements correspond
to all of the input (user) qubits.
}];
let options = [
Option<"extendedLayerSize", "extendedLayerSize", "unsigned", /*default=*/"20", "Extended layer size">,
Option<"extendedLayerWeight", "extendedLayerWeight", "float", /*default=*/"0.5", "Extended layer weight">,
Option<"decayDelta", "decayDelta", "float", /*default=*/"0.5", "Decay delta">,
Option<"roundsDecayReset", "roundsDecayReset", "unsigned", /*default=*/"5", "Number of rounds before decay is reset">
];
}
def MappingPrep: Pass<"qubit-mapping-prep", "mlir::ModuleOp"> {
let summary = "Prepare module for qubit mapping.";
let description = [{
Insert the required topology-aware `quake.wire_set` operation that
corresponds to the requested device.
}];
let options = [
Option<"device", "device", "std::string", /*default=*/"\"-\"",
"Device topology: path(N), ring(N), star(N), star(N,c), grid(w,h), file(/path/to/file), bypass">,
];
}
def MemToReg : Pass<"memtoreg", "mlir::func::FuncOp"> {
let summary = "Converts memory-SSA to register-SSA form.";
let description = [{
When classical is enabled, loads and stores of simple scalars that are stack
allocated are converted to register form. The values themselves are promoted
out of memory and passed directly. Store to load forwarding and the addition
of block arguments is performed to eliminate the loads and stores.
When quantum is enabled, this pass converts values of type `!quake.ref` to
values of type `!quake.wire`. A `ref` value has ordinary SSA semantics. It
is possible to make copies of its value, pass the ref value, operate on the
quantum information by reference, etc. A `wire` value does not have ordinary
SSA semantics. A use can destroy the value itself, therefore it is required
that `wire` values be threads in and out of each and every use.
}];
let dependentDialects = ["cudaq::cc::CCDialect", "quake::QuakeDialect"];
let options = [
Option<"classicalValues", "classical", "bool",
/*default=*/"true", "Promote classical stack slots to values.">,
Option<"quantumValues", "quantum", "bool",
/*default=*/"true", "Promote of quantum values.">
];
}
def MultiControlDecompositionPass: Pass<"multicontrol-decomposition",
"mlir::func::FuncOp"> {
let summary = "Break down multi-control quantum operations.";
let description = [{
This pass decomposes multi-control quantum operations. The decompostion
involves allocating new qubits to hold intermediate results. The number of
extra qubits depends on the particular operation being decomposed.
Pauli-X and Pauli-Z operations add _N_ - 2 qubits, while other operations
add _N_ - 1 qubits, where _N_ is the number of controls.
Note: When a `veq` is used as control, we need to know its size to be able
to decompose. In such cases, all qubits will be extracted. If the size is
unknown at compilation-time, the pass leaves the operation as-is.
}];
}
def ObserveAnsatz : Pass<"observe-ansatz", "mlir::func::FuncOp"> {
let summary = "Given spin_op input, append measures to the FuncOp";
let description = [{
Given an unmeasured Quake representation (i.e. a state prep ansatz), append
measures based on the given spin_op specified in binary symplectic form.
}];
let options = [
ListOption<"termBSF", "term-bsf", "unsigned",
"The measurement bases as a Pauli tensor product represented in binary symplectic form.">
];
}
def DeleteStates : Pass<"delete-states", "mlir::ModuleOp"> {
let summary =
"Remove unnecessary creation of new states from data";
let description = [{
Argument synthesis for state pointers for remote simulation substitutes state
argument by a new state created from materialized data. The new state is not
always necessary, and is not deleted from the heap. This pass:
- Replaces state by its data if possible
- Adds delete calls before exits if not possible to replace a new state
For example, the new state is replaced by its data if only used in `quake.init_state`
instruction:
Before DeleteStates (delete-states):
```
func.func @foo() attributes {"cudaq-entrypoint", "cudaq-kernel", no_this} {
%c8_i64 = arith.constant 8 : i64
%0 = cc.address_of @foo.rodata_synth_0 : !cc.ptr<!cc.array<complex<f32> x 8>>
%4 = cc.create_state %3, %c8_i64 : (!cc.ptr<!cc.array<complex<f32> x 8>>, i64) -> !cc.ptr<!cc.state>
%5 = cc.get_number_of_qubits %4 : (!cc.ptr<!cc.state>) -> i64
%6 = quake.alloca !quake.veq<?>[%5 : i64]
%7 = quake.init_state %6, %4 : (!quake.veq<?>, !cc.ptr<!cc.state>) -> !quake.veq<?>
return
}
```
After DeleteStates (delete-states):
```
module {
func.func @foo() attributes {"cudaq-entrypoint", "cudaq-kernel", no_this} {
%c8_i64 = arith.constant 8 : i64
%0 = cc.address_of @foo.rodata_synth_0 : !cc.ptr<!cc.array<complex<f32> x 8>>
%3 = quake.alloca !quake.veq<3>
%4 = quake.init_state %3, %0 : (!quake.veq<3>, !cc.ptr<!cc.array<complex<f32> x 4>>) -> !quake.veq<3>
return
}
```
Otherwise, if state is used in another instructions and cannot be easily
replaced, the new state is deleted before exits:
Before DeleteStates (delete-states):
``` func.func @foo() attributes {"cudaq-entrypoint", "cudaq-kernel", no_this} {
...
%4 = call @__nvqpp_cudaq_state_createFromData_fp32(%3, %c8_i64) : (!cc.ptr<i8>, i64) -> !cc.ptr<!cc.state>
call @__nvqpp__mlirgen__sub_kernel(%4) : (!cc.ptr<!cc.state>) -> ()
return
}
```
After DeleteStates (delete-states):
```
func.func @foo() attributes {"cudaq-entrypoint", "cudaq-kernel", no_this} {
...
%4 = call @__nvqpp_cudaq_state_createFromData_fp32(%3, %c8_i64) : (!cc.ptr<i8>, i64) -> !cc.ptr<!cc.state>
call @__nvqpp__mlirgen__sub_kernel(%4) : (!cc.ptr<!cc.state>) -> ()
call @__nvqpp_cudaq_state_delete(%4) : (!cc.ptr<!cc.state>) -> ()
return
}
```
}];
}
def StatePreparation : Pass<"state-prep", "mlir::func::FuncOp"> {
let summary =
"Convert state vector data into gates";
let description = [{
Convert quake representation that includes qubit initialization
from data into qubit initialization using gates.
For example:
```mlir
module {
func.func @foo() attributes {
%0 = cc.address_of @foo.rodata_0 : !cc.ptr<!cc.array<complex<f32> x 4>>
%1 = quake.alloca !quake.veq<2>
%2 = quake.init_state %1, %0 : (!quake.veq<2>, !cc.ptr<!cc.array<complex<f32> x 4>>) -> !quake.veq<2>
return
}
cc.global constant private @foo.rodata_0 (dense<[(0.707106769,0.000000e+00), (0.707106769,0.000000e+00), (0.000000e+00,0.000000e+00), (0.000000e+00,0.000000e+00)]> : tensor<4xcomplex<f32>>) : !cc.array<complex<f32> x 4>
}
```
Will be rewritten to:
```mlir
module {
func.func @foo() attributes {
%0 = quake.alloca !quake.veq<2>
%c1_i64 = arith.constant 1 : i64
%1 = quake.extract_ref %0[%c1_i64] : (!quake.veq<2>, i64) -> !quake.ref
%cst = arith.constant 0.000000e+00 : f64
quake.ry (%cst) %1 : (f64, !quake.ref) -> ()
%c0_i64 = arith.constant 0 : i64
%2 = quake.extract_ref %0[%c0_i64] : (!quake.veq<2>, i64) -> !quake.ref
%cst_0 = arith.constant 0.78539816339744839 : f64
quake.ry (%cst_0) %2 : (f64, !quake.ref) -> ()
quake.x [%1] %2 : (!quake.ref, !quake.ref) -> ()
%cst_1 = arith.constant 0.78539816339744839 : f64
quake.ry (%cst_1) %2 : (f64, !quake.ref) -> ()
quake.x [%1] %2 : (!quake.ref, !quake.ref) -> ()
return
}
}
```
}];
let options = [
Option<"phaseThreshold", "threshold", "double",
/*default=*/"1e-10", "Threshold to trigger phase equalization">,
];
}
def PromoteRefToVeqAlloc : Pass<"promote-qubit-allocation"> {
let summary = "Promote single qubit allocations.";
let description = [{
This pass converts all single qubit allocations in the quake dialect to
allocations of vectors of qubits of length one. This conversion makes all
allocations uniform for the conversion to QIR.
}];
}
def PruneCtrlRelations : Pass<"pruned-ctrl-form", "mlir::func::FuncOp"> {
let summary = "Removes artifical control constraints between quantum ops.";
let description = [{
In the value semantics, quantum gates may be overly constrained in terms of
control qubits. These constraints can be removed within the IR to get a more
accurate representation of the dataflow of the *logical* qubits. This
alternative representation may be better suited to certain transformations.
The following example is overly constrained. The control wire, beginning
with `%0` is imposing what appears to be a strict ordering of
`[x -> h -> y]`.
```mlir
%3:2 = x [%0] %1 : (!wire, !wire) -> (!wire, !wire)
%4:2 = h [%3#0] %2 : (!wire, !wire) -> (!wire, !wire)
%5:2 = y [%4#0] %3#1 : (!wire, !wire) -> (!wire, !wire)
```
This snippet can be rewritten as the following IR to reflect that the
control wire has an identity applied so is unchanged. We observe less strict
ordering constraints of `[h | x -> y]`. This might allow a scheduling pass
to swap the order of the `h` and `x` gates, for instance.
```mlir
%ctrl = to_ctrl %old.0 : (!wire) -> !control
%3 = x [%ctrl] %1 : (!control, !wire) -> !wire
%4 = h [%ctrl] %2 : (!control, !wire) -> !wire
%5 = y [%ctrl] %3 : (!control, !wire) -> !wire
%new.0 = from_ctrl %ctrl : (!control) -> !wire
```
}];
}
def PySynthCallableBlockArgs :
Pass<"py-synth-callable-block-args", "mlir::func::FuncOp"> {
let summary = "Synthesize / Inline cc.callable_func on function block arguments.";
let description = [{
This pass is leveraged by the Python bindings to synthesize any
cc.callable block arguments. By synthesis we mean replace all uses of the
callable block argument with a specific in-Module function call (func.call)
retrieved at runtime (the name of the function passed to the kernel at the
cc.callable block argument index).
}];
}
def QuakeSynthesize : Pass<"quake-synth", "mlir::ModuleOp"> {
let summary =
"Synthesize concrete quantum program from Quake code plus runtime values.";
let description = [{
Convert Quake representing a dynamic quantum kernel to Quake
representing a concrete quantum program instance using known
runtime values.
}];
let constructor = "cudaq::opt::createQuakeSynthesizer()";
}
def QuakeAddDeallocs : Pass<"add-dealloc", "mlir::func::FuncOp"> {
let summary = "Add quake deallocs to functions before they return.";
let description = [{
This pass ensures that a dealloc is inserted before functions return
if the function contains a AllocaOp. It should be run before converting
to QIR, for example, in order to generate correct code.
}];
let constructor = "cudaq::opt::createQuakeAddDeallocs()";
}
def QuakeAddMetadata : Pass<"quake-add-metadata", "mlir::func::FuncOp"> {
let summary = "Add various metadata attributes to a Quake function.";
let description = [{
This pass provides a hook for adding various metadata to a Quake
function's list of compile-time attributes. This type of metadata
exposure will enable quantum IR introspection in the CUDA-Q
runtime library.
}];
let constructor = "cudaq::opt::createQuakeAddMetadata()";
}
def RegToMem : Pass<"regtomem", "mlir::func::FuncOp"> {
let summary = "Converts register-SSA to memory-SSA form.";
let description = [{
Perform the reverse transformation of the memtoreg pass.
This pass converts values of type `!quake.wire` to be wrapped as references
where possible. This is an greedy/exhaustive transformation in that wires
will be removed from the Quake IR wherever possible. There may be cases
where the wire cannot be wrapped uniquely. In those cases, this
transformation will take no action and leave the IR (partially) with wire
type values.
}];
let dependentDialects = ["cudaq::cc::CCDialect", "quake::QuakeDialect"];
}
// UnitarySynthesis is a module pass because it may modify the `ModuleOp` by
// adding new `FuncOp`(s).
def UnitarySynthesis : Pass<"unitary-synthesis", "mlir::ModuleOp"> {
let summary =
"Convert a custom quantum operation into sequence of native quantum gates.";
let description = [{
Given a custom quantum operation specified in the form of its concrete
unitary matrix, convert it into a sequence of native quantum gates.
For example,
```mlir
quake.custom_op @__nvqpp__mlirgen__custom_x_generator_1.rodata %0 : (!quake.ref) -> ()
```
This call will be converted to an `ApplyOp`.
```mlir
quake.apply @__nvqpp__mlirgen__custom_x_generator_1.kernel %[[VAL_0]] : (!quake.ref) -> ()
```
The function has sequence of decomposed gates.
```mlir
func.func private @__nvqpp__mlirgen__custom_x_generator_1.kernel(%arg0: !quake.ref) {
%cst = arith.constant 1.5707963267948966 : f64
quake.ry (%cst) %arg0 : (f64, !quake.ref) -> ()
...