forked from NVIDIA/cuda-quantum
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkernel_builder.py
More file actions
1591 lines (1370 loc) · 64.8 KB
/
kernel_builder.py
File metadata and controls
1591 lines (1370 loc) · 64.8 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
# ============================================================================ #
# 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. #
# ============================================================================ #
from functools import partialmethod
import hashlib
import uuid
import random
import re
import string
import sys
import numpy as np
from typing import get_origin
from .quake_value import QuakeValue
from .kernel_decorator import PyKernelDecorator
from .utils import mlirTypeFromPyType, nvqppPrefix, emitFatalError, emitWarning, mlirTypeToPyType, emitErrorIfInvalidPauli, globalRegisteredOperations
from .common.givens import givens_builder
from .common.fermionic_swap import fermionic_swap_builder
from .captured_data import CapturedDataStorage
from ..mlir.ir import *
from ..mlir.passmanager import *
from ..mlir.execution_engine import *
from ..mlir.dialects import quake, cc
from ..mlir.dialects import builtin, func, arith, math, complex as complexDialect
from ..mlir._mlir_libs._quakeDialects import cudaq_runtime, load_intrinsic, register_all_dialects, gen_vector_of_complex_constant
kDynamicPtrIndex: int = -2147483648
qvector = cudaq_runtime.qvector
# This file reproduces the cudaq::kernel_builder in Python
# We need static initializers to run in the CAPI `ExecutionEngine`,
# so here we run a simple JIT compile at global scope
with Context():
module = Module.parse(r"""
llvm.func @none() {
llvm.return
}""")
ExecutionEngine(module)
def __generalOperation(self,
opName,
parameters,
controls,
target,
isAdj=False,
context=None):
"""
This is a utility function that applies a general quantum
operation to the internal PyKernel MLIR ModuleOp.
"""
opCtor = getattr(quake, '{}Op'.format(opName.title()))
if quake.RefType.isinstance(target.mlirValue.type):
opCtor([], parameters, controls, [target.mlirValue], is_adj=isAdj)
return
# Must be a `veq`, get the size
size = quake.VeqSizeOp(self.getIntegerType(), target.mlirValue)
def body(idx):
extracted = quake.ExtractRefOp(quake.RefType.get(context),
target.mlirValue,
-1,
index=idx).result
opCtor([], parameters, controls, [extracted], is_adj=isAdj)
self.createInvariantForLoop(size, body)
def get_parameter_value(self, parameter):
paramVal = parameter
if isinstance(parameter, float):
fty = mlirTypeFromPyType(float, self.ctx)
paramVal = arith.ConstantOp(fty, FloatAttr.get(fty, parameter))
elif isinstance(parameter, QuakeValue):
paramVal = parameter.mlirValue
return paramVal
def __singleTargetOperation(self, opName, target, isAdj=False):
"""
Utility function for adding a single target quantum operation to the
MLIR representation for the PyKernel.
"""
with self.insertPoint, self.loc:
__generalOperation(self,
opName, [], [],
target,
isAdj=isAdj,
context=self.ctx)
def __singleTargetControlOperation(self, opName, control, target, isAdj=False):
"""
Utility function for adding a single target controlled quantum operation to the
MLIR representation for the PyKernel.
"""
with self.insertPoint, self.loc:
fwdControls = None
if isinstance(control, list):
fwdControls = [c.mlirValue for c in control]
elif quake.RefType.isinstance(
control.mlirValue.type) or quake.VeqType.isinstance(
control.mlirValue.type):
fwdControls = [control.mlirValue]
else:
emitFatalError(f"invalid control type for {opName}.")
__generalOperation(self,
opName, [],
fwdControls,
target,
isAdj=isAdj,
context=self.ctx)
def __singleTargetSingleParameterOperation(self,
opName,
parameter,
target,
isAdj=False):
"""
Utility function for adding a single target, one parameter quantum operation to the
MLIR representation for the PyKernel.
"""
with self.insertPoint, self.loc:
__generalOperation(self,
opName, [get_parameter_value(self, parameter)], [],
target,
isAdj=isAdj,
context=self.ctx)
def __singleTargetSingleParameterControlOperation(self,
opName,
parameter,
controls,
target,
isAdj=False):
"""
Utility function for adding a single target, one parameter, controlled quantum operation to the
MLIR representation for the PyKernel.
"""
with self.insertPoint, self.loc:
fwdControls = None
if isinstance(controls, list):
fwdControls = [c.mlirValue for c in controls]
elif quake.RefType.isinstance(
controls.mlirValue.type) or quake.VeqType.isinstance(
controls.mlirValue.type):
fwdControls = [controls.mlirValue]
else:
emitFatalError(f"invalid controls type for {opName}.")
__generalOperation(self,
opName, [get_parameter_value(self, parameter)],
fwdControls,
target,
isAdj=isAdj,
context=self.ctx)
def supportCommonCast(mlirType, otherTy, arg, FromType, ToType, PyType):
argEleTy = cc.StdvecType.getElementType(mlirType)
eleTy = cc.StdvecType.getElementType(otherTy)
if ToType.isinstance(eleTy) and FromType.isinstance(argEleTy):
return [PyType(i) for i in arg]
return None
def __generalCustomOperation(self, opName, *args):
"""
Utility function for adding a generic quantum operation to the MLIR
representation for the PyKernel.
A controlled version can be invoked by passing additional arguments
to the operation. For an N-qubit operation, the last N arguments are
treated as `targets` and excess arguments as `controls`.
"""
global globalRegisteredOperations
unitary = globalRegisteredOperations[opName]
numTargets = int(np.log2(np.sqrt(unitary.size)))
qubits = []
with self.insertPoint, self.loc:
for arg in args:
if isinstance(arg, QuakeValue):
qubits.append(arg.mlirValue)
else:
emitFatalError(f"invalid argument type passed to {opName}.")
targets = []
controls = []
if numTargets == len(qubits):
targets = qubits
elif numTargets < len(qubits):
numControls = len(qubits) - numTargets
targets = qubits[-numTargets:]
controls = qubits[:numControls]
else:
emitFatalError(
f"too few arguments passed to {opName}, expected ({numTargets})"
)
globalName = f'{nvqppPrefix}{opName}_generator_{numTargets}.rodata'
currentST = SymbolTable(self.module.operation)
if not globalName in currentST:
with InsertionPoint(self.module.body):
gen_vector_of_complex_constant(self.loc, self.module,
globalName, unitary.tolist())
quake.CustomUnitarySymbolOp([],
generator=FlatSymbolRefAttr.get(globalName),
parameters=[],
controls=controls,
targets=targets,
is_adj=False)
return
class PyKernel(object):
"""
The :class:`Kernel` provides an API for dynamically constructing quantum
circuits. The :class:`Kernel` programmatically represents the circuit as an MLIR
function using the Quake dialect.
Attributes:
name (:obj:`str`): The name of the :class:`Kernel` function. Read-only.
arguments (List[:class:`QuakeValue`]): The arguments accepted by the
:class:`Kernel` function. Read-only.
argument_count (int): The number of arguments accepted by the
:class:`Kernel` function. Read-only.
"""
def __init__(self, argTypeList):
self.ctx = Context()
register_all_dialects(self.ctx)
quake.register_dialect(self.ctx)
cc.register_dialect(self.ctx)
cudaq_runtime.registerLLVMDialectTranslation(self.ctx)
self.metadata = {'conditionalOnMeasure': False}
self.regCounter = 0
self.loc = Location.unknown(context=self.ctx)
self.module = Module.create(loc=self.loc)
self.funcName = '{}__nvqppBuilderKernel_{}'.format(
nvqppPrefix, ''.join(
random.choice(string.ascii_uppercase + string.digits)
for _ in range(10)))
self.name = self.funcName.removeprefix(nvqppPrefix)
self.funcNameEntryPoint = self.funcName + '_PyKernelEntryPointRewrite'
attr = DictAttr.get(
{
self.funcName:
StringAttr.get(self.funcNameEntryPoint, context=self.ctx)
},
context=self.ctx)
self.module.operation.attributes.__setitem__('quake.mangled_name_map',
attr)
self.capturedDataStorage = CapturedDataStorage(ctx=self.ctx,
loc=self.loc,
name=self.name,
module=self.module)
with self.ctx, InsertionPoint(self.module.body), self.loc:
self.mlirArgTypes = [
mlirTypeFromPyType(argType[0], self.ctx, argInstance=argType[1])
for argType in
[self.__processArgType(ty) for ty in argTypeList]
]
self.funcOp = func.FuncOp(self.funcName, (self.mlirArgTypes, []),
loc=self.loc)
self.funcOp.attributes.__setitem__('cudaq-entrypoint',
UnitAttr.get())
self.funcOp.attributes.__setitem__('cudaq-kernel', UnitAttr.get())
e = self.funcOp.add_entry_block()
self.arguments = [self.__createQuakeValue(b) for b in e.arguments]
self.argument_count = len(self.arguments)
with InsertionPoint(e):
func.ReturnOp([])
self.insertPoint = InsertionPoint.at_block_begin(e)
def __del__(self):
"""
When a kernel builder is deleted we need to clean up
any state data if there is any.
"""
self.capturedDataStorage.__del__()
def __processArgType(self, ty):
"""
Process input argument type. Specifically, try to infer the
element type for a list, e.g. list[float].
"""
if ty in [cudaq_runtime.qvector, cudaq_runtime.qubit]:
return ty, None
if get_origin(ty) == list or isinstance(ty, list):
if '[' in str(ty) and ']' in str(ty):
allowedTypeMap = {
'int': int,
'bool': bool,
'float': float,
'complex': complex,
'numpy.complex128': np.complex128,
'numpy.complex64': np.complex64,
'pauli_word': cudaq_runtime.pauli_word
}
# Infer the slice type
result = re.search(r'ist\[(.*)\]', str(ty))
eleTyName = result.group(1)
if 'cudaq_runtime.pauli_word' in str(ty):
eleTyName = 'pauli_word'
pyType = allowedTypeMap[eleTyName]
if eleTyName != None and eleTyName in allowedTypeMap:
return list, [pyType()]
emitFatalError(f'Invalid type for kernel builder {ty}')
return ty, None
def getIntegerAttr(self, type, value):
"""
Return an MLIR Integer Attribute of the given IntegerType.
"""
return IntegerAttr.get(type, value)
def getIntegerType(self, width=64):
"""
Return an MLIR `IntegerType` of the given bit width (defaults to 64 bits).
"""
return IntegerType.get_signless(width)
def getConstantInt(self, value, width=64):
"""
Create a constant integer operation and return its MLIR result Value.
Takes as input the concrete integer value. Can specify the integer bit width.
"""
ty = self.getIntegerType(width)
return arith.ConstantOp(ty, self.getIntegerAttr(ty, value)).result
def getFloatType(self, width=64):
"""
Return an MLIR float type (single or double precision).
"""
# Note:
# `numpy.float64` is the same as `float` type, with width of 64 bit.
# `numpy.float32` type has width of 32 bit.
return F64Type.get() if width == 64 else F32Type.get()
def getFloatAttr(self, type, value):
"""
Return an MLIR float attribute (single or double precision).
"""
return FloatAttr.get(type, value)
def getConstantFloat(self, value, width=64):
"""
Create a constant float operation and return its MLIR result Value.
Takes as input the concrete float value.
"""
ty = self.getFloatType(width=width)
return self.getConstantFloatWithType(value, ty)
def getConstantFloatWithType(self, value, ty):
"""
Create a constant float operation and return its MLIR result Value.
Takes as input the concrete float value.
"""
return arith.ConstantOp(ty, self.getFloatAttr(ty, value)).result
def getComplexType(self, width=64):
"""
Return an MLIR complex type (single or double precision).
"""
# Note:
# `numpy.complex128` is the same as `complex` type,
# with element width of 64bit (`np.complex64` and `float`)
# `numpy.complex64` type has element type of `np.float32`.
return self.getComplexTypeWithElementType(
self.getFloatType(width=width))
def getComplexTypeWithElementType(self, eTy):
"""
Return an MLIR complex type (single or double precision).
"""
return ComplexType.get(eTy)
def simulationPrecision(self):
"""
Return precision for the current simulation backend,
see `cudaq_runtime.SimulationPrecision`.
"""
target = cudaq_runtime.get_target()
return target.get_precision()
def simulationDType(self):
"""
Return the data type for the current simulation backend,
either `numpy.complex128` or `numpy.complex64`.
"""
if self.simulationPrecision() == cudaq_runtime.SimulationPrecision.fp64:
return self.getComplexType(width=64)
return self.getComplexType(width=32)
def ifPointerThenLoad(self, value):
"""
If the given value is of pointer type, load the pointer
and return that new value.
"""
if cc.PointerType.isinstance(value.type):
return cc.LoadOp(value).result
return value
def ifNotPointerThenStore(self, value):
"""
If the given value is not of a pointer type, allocate a
slot on the stack, store the the value in the slot, and
return the slot address.
"""
if not cc.PointerType.isinstance(value.type):
slot = cc.AllocaOp(cc.PointerType.get(self.ctx, value.type),
TypeAttr.get(value.type)).result
cc.StoreOp(value, slot)
return slot
return value
def promoteOperandType(self, ty, operand):
if ComplexType.isinstance(ty):
complexType = ComplexType(ty)
floatType = complexType.element_type
if ComplexType.isinstance(operand.type):
otherComplexType = ComplexType(operand.type)
otherFloatType = otherComplexType.element_type
if (floatType != otherFloatType):
real = self.promoteOperandType(
floatType,
complexDialect.ReOp(operand).result)
imag = self.promoteOperandType(
floatType,
complexDialect.ImOp(operand).result)
operand = complexDialect.CreateOp(complexType, real,
imag).result
else:
real = self.promoteOperandType(floatType, operand)
imag = self.getConstantFloatWithType(0.0, floatType)
operand = complexDialect.CreateOp(complexType, real,
imag).result
if F64Type.isinstance(ty):
if F32Type.isinstance(operand.type):
operand = arith.ExtFOp(ty, operand).result
if IntegerType.isinstance(operand.type):
operand = arith.SIToFPOp(ty, operand).result
if F32Type.isinstance(ty):
if F64Type.isinstance(operand.type):
operand = arith.TruncFOp(ty, operand).result
if IntegerType.isinstance(operand.type):
operand = arith.SIToFPOp(ty, operand).result
return operand
def __getMLIRValueFromPythonArg(self, arg, argTy):
"""
Given a python runtime argument, create and return an equivalent constant MLIR Value.
"""
pyType = type(arg)
mlirType = mlirTypeFromPyType(pyType,
self.ctx,
argInstance=arg,
argTypeToCompareTo=argTy)
if IntegerType.isinstance(mlirType):
return self.getConstantInt(arg, mlirType.width)
if F64Type.isinstance(mlirType):
return self.getConstantFloat(arg)
if ComplexType.isinstance(mlirType):
return complexDialect.CreateOp(mlirType,
self.getConstantFloat(arg.real),
self.getConstantFloat(
arg.imag)).result
if cc.StdvecType.isinstance(mlirType):
size = self.getConstantInt(len(arg))
eleTy = cc.StdvecType.getElementType(mlirType)
arrTy = cc.ArrayType.get(self.ctx, eleTy)
alloca = cc.AllocaOp(cc.PointerType.get(self.ctx, arrTy),
TypeAttr.get(eleTy),
seqSize=size).result
def body(idx):
eleAddr = cc.ComputePtrOp(
cc.PointerType.get(self.ctx, eleTy), alloca, [idx],
DenseI32ArrayAttr.get([-2147483648],
context=self.ctx)).result
element = arg[body.counter]
elementVal = None
if IntegerType.isinstance(eleTy):
elementVal = self.getConstantInt(element)
elif F64Type.isinstance(eleTy):
elementVal = self.getConstantFloat(element)
elif cc.StdvecType.isinstance(eleTy):
elementVal = self.__getMLIRValueFromPythonArg(
element, eleTy)
else:
emitFatalError(
f"CUDA-Q kernel builder could not process runtime list-like element type ({pyType})."
)
cc.StoreOp(elementVal, eleAddr)
# Python is weird, but interesting.
body.counter += 1
body.counter = 0
self.createInvariantForLoop(size, body)
return cc.StdvecInitOp(cc.StdvecType.get(self.ctx, eleTy), alloca,
size).result
emitFatalError(
"CUDA-Q kernel builder could not translate runtime argument of type {pyType} to internal IR value."
)
def createInvariantForLoop(self,
endVal,
bodyBuilder,
startVal=None,
stepVal=None,
isDecrementing=False):
"""
Create an invariant loop using the CC dialect.
"""
startVal = self.getConstantInt(0) if startVal == None else startVal
stepVal = self.getConstantInt(1) if stepVal == None else stepVal
iTy = self.getIntegerType()
inputs = [startVal]
resultTys = [iTy]
loop = cc.LoopOp(resultTys, inputs, BoolAttr.get(False))
whileBlock = Block.create_at_start(loop.whileRegion, [iTy])
with InsertionPoint(whileBlock):
condPred = IntegerAttr.get(
iTy, 2) if not isDecrementing else IntegerAttr.get(iTy, 4)
cc.ConditionOp(
arith.CmpIOp(condPred, whileBlock.arguments[0], endVal).result,
whileBlock.arguments)
bodyBlock = Block.create_at_start(loop.bodyRegion, [iTy])
with InsertionPoint(bodyBlock):
bodyBuilder(bodyBlock.arguments[0])
cc.ContinueOp(bodyBlock.arguments)
stepBlock = Block.create_at_start(loop.stepRegion, [iTy])
with InsertionPoint(stepBlock):
incr = arith.AddIOp(stepBlock.arguments[0], stepVal).result
cc.ContinueOp([incr])
loop.attributes.__setitem__('invariant', UnitAttr.get())
return
def __createQuakeValue(self, value):
return QuakeValue(value, self)
def __cloneOrGetFunction(self, name, currentModule, otherModule):
"""
Get a the function with the given name. First look in the
current `ModuleOp` for this `kernel_builder`, if found return it as is. If
not found, find it in the other `kernel_builder` `ModuleOp` and return a
clone of it. Throw an exception if no kernel with the given name is found
"""
thisSymbolTable = SymbolTable(currentModule.operation)
if name in thisSymbolTable:
return thisSymbolTable[name]
otherSymbolTable = SymbolTable(otherModule.operation)
if name in otherSymbolTable:
cloned = otherSymbolTable[name].operation.clone()
currentModule.body.append(cloned)
if 'cudaq-entrypoint' in cloned.operation.attributes:
cloned.operation.attributes.__delitem__('cudaq-entrypoint')
return cloned
emitFatalError(f"Could not find function with name {name}")
def __addAllCalledFunctionsRecursively(self, otherFunc, currentModule,
otherModule):
"""
Search the given `FuncOp` for all `CallOps` recursively.
If found, see if the called function is in the current `ModuleOp`
for this `kernel_builder`, if so do nothing. If it is not found,
then find it in the other `ModuleOp`, clone it, and add it to this
`ModuleOp`.
"""
def walk(topLevel, functor):
for region in topLevel.regions:
for block in region:
for op in block:
functor(op)
walk(op, functor)
def visitAllCallOps(funcOp):
def functor(op):
calleeName = ''
if isinstance(op, func.CallOp) or isinstance(op, quake.ApplyOp):
calleeName = FlatSymbolRefAttr(
op.attributes['callee']).value
if len(calleeName) == 0:
return
currentST = SymbolTable(currentModule.operation)
if calleeName in currentST:
return
otherST = SymbolTable(otherModule.operation)
if calleeName not in otherST:
emitFatalError(
f"Invalid called function `{calleeName}`- cannot find the function in the symbol table"
)
cloned = otherST[calleeName].operation.clone()
if 'cudaq-entrypoint' in cloned.operation.attributes:
cloned.operation.attributes.__delitem__('cudaq-entrypoint')
currentModule.body.append(cloned)
visitAllCallOps(cloned)
walk(funcOp, functor)
visitAllCallOps(otherFunc)
return
def __applyControlOrAdjoint(self, target, isAdjoint, controls, *args):
"""
Utility method for adding a Quake `ApplyOp` in the case of cudaq.control or
cudaq.adjoint. This function will search recursively for all required function
operations and add them tot he module.
"""
with self.insertPoint, self.loc:
otherModule = Module.parse(str(target.module), self.ctx)
otherFuncCloned = self.__cloneOrGetFunction(
nvqppPrefix + target.name, self.module, otherModule)
self.__addAllCalledFunctionsRecursively(otherFuncCloned,
self.module, otherModule)
otherFTy = otherFuncCloned.body.blocks[0].arguments
mlirValues = []
for i, v in enumerate(args):
argTy = otherFTy[i].type
if not isinstance(v, QuakeValue):
# here we have to map constant Python data
# to an MLIR Value
value = self.__getMLIRValueFromPythonArg(v, argTy)
else:
value = v.mlirValue
inTy = value.type
if (quake.VeqType.isinstance(inTy) and
quake.VeqType.isinstance(argTy)):
if quake.VeqType.hasSpecifiedSize(
inTy) and not quake.VeqType.hasSpecifiedSize(argTy):
value = quake.RelaxSizeOp(argTy, value).result
mlirValues.append(value)
if isAdjoint or len(controls) > 0:
quake.ApplyOp([], [],
controls,
mlirValues,
callee=FlatSymbolRefAttr.get(
otherFuncCloned.name.value),
is_adj=isAdjoint)
else:
func.CallOp(otherFuncCloned, mlirValues)
def __str__(self, canonicalize=True):
"""
Return a string representation of this kernels MLIR Module.
"""
if canonicalize:
pm = PassManager.parse("builtin.module(canonicalize,cse)",
context=self.ctx)
cloned = cudaq_runtime.cloneModule(self.module)
pm.run(cloned)
return str(cloned)
return str(self.module)
def qalloc(self, initializer=None):
"""
Allocate a register of qubits of size `qubit_count` and return a
handle to them as a :class:`QuakeValue`.
Args:
initializer (Union[`int`,`QuakeValue`, `list[T]`): The number of qubits to allocate or a concrete state to allocate and initialize the qubits.
Returns:
:class:`QuakeValue`: A handle to the allocated qubits in the MLIR.
```python
# Example:
kernel = cudaq.make_kernel()
qubits = kernel.qalloc(10)
```
"""
with self.insertPoint, self.loc:
# If the initializer is an integer, create `veq<N>`
if isinstance(initializer, int):
veqTy = quake.VeqType.get(self.ctx, initializer)
return self.__createQuakeValue(quake.AllocaOp(veqTy).result)
if isinstance(initializer, list):
initializer = np.array(initializer, dtype=type(initializer[0]))
if isinstance(initializer, np.ndarray):
if len(initializer.shape) != 1:
raise RuntimeError(
"invalid initializer for qalloc (np.ndarray must be 1D, vector-like)"
)
if initializer.dtype not in [
complex, np.complex128, np.complex64, float, np.float64,
np.float32, int
]:
raise RuntimeError(
"invalid initializer for qalloc (must be int, float, or complex dtype)"
)
# Get current simulation precision and convert the initializer if needed.
simulationPrecision = self.simulationPrecision()
if simulationPrecision == cudaq_runtime.SimulationPrecision.fp64:
if initializer.dtype not in [complex, np.complex128]:
initializer = initializer.astype(dtype=np.complex128)
if simulationPrecision == cudaq_runtime.SimulationPrecision.fp32:
if initializer.dtype != np.complex64:
initializer = initializer.astype(dtype=np.complex64)
# Get the size of the array
size = len(initializer)
numQubits = np.log2(size)
if not numQubits.is_integer():
raise RuntimeError(
"invalid input state size for qalloc (not a power of 2)"
)
# check state is normalized
norm = sum([np.conj(a) * a for a in initializer])
if np.abs(norm.imag) > 1e-4 or np.abs(1. - norm.real) > 1e-4:
raise RuntimeError(
"invalid input state for qalloc (not normalized)")
veqTy = quake.VeqType.get(self.ctx, int(numQubits))
qubits = quake.AllocaOp(veqTy).result
data = self.capturedDataStorage.storeArray(initializer)
init = quake.InitializeStateOp(qubits.type, qubits, data).result
return self.__createQuakeValue(init)
# Captured state
if isinstance(initializer, cudaq_runtime.State):
statePtr = self.capturedDataStorage.storeCudaqState(initializer)
i64Ty = self.getIntegerType()
numQubits = quake.GetNumberOfQubitsOp(i64Ty, statePtr).result
veqTy = quake.VeqType.get(self.ctx)
qubits = quake.AllocaOp(veqTy, size=numQubits).result
init = quake.InitializeStateOp(veqTy, qubits, statePtr).result
return self.__createQuakeValue(init)
# If the initializer is a QuakeValue, see if it is
# an integer or a `stdvec` type
if isinstance(initializer, QuakeValue):
veqTy = quake.VeqType.get(self.ctx)
if IntegerType.isinstance(initializer.mlirValue.type):
# This is an integer size
return self.__createQuakeValue(
quake.AllocaOp(veqTy,
size=initializer.mlirValue).result)
if cc.StdvecType.isinstance(initializer.mlirValue.type):
size = cc.StdvecSizeOp(self.getIntegerType(),
initializer.mlirValue).result
value = initializer.mlirValue
eleTy = cc.StdvecType.getElementType(value.type)
numQubits = math.CountTrailingZerosOp(size).result
qubits = quake.AllocaOp(veqTy, size=numQubits).result
ptrTy = cc.PointerType.get(self.ctx, eleTy)
data = cc.StdvecDataOp(ptrTy, value).result
init = quake.InitializeStateOp(veqTy, qubits, data).result
return self.__createQuakeValue(init)
# State pointer
if cc.PointerType.isinstance(initializer.mlirValue.type):
valuePtrTy = initializer.mlirValue.type
valueTy = cc.PointerType.getElementType(valuePtrTy)
if cc.StateType.isinstance(valueTy):
statePtr = initializer.mlirValue
i64Ty = self.getIntegerType()
numQubits = quake.GetNumberOfQubitsOp(i64Ty,
statePtr).result
veqTy = quake.VeqType.get(self.ctx)
qubits = quake.AllocaOp(veqTy, size=numQubits).result
init = quake.InitializeStateOp(veqTy, qubits,
statePtr).result
return self.__createQuakeValue(init)
# If no initializer, create a single qubit
if initializer == None:
qubitTy = quake.RefType.get(self.ctx)
return self.__createQuakeValue(quake.AllocaOp(qubitTy).result)
raise RuntimeError(
f"invalid initializer argument for qalloc: {initializer}")
def __isPauliWordType(self, ty):
"""
A Pauli word type in our MLIR dialects is a `cc.charspan`. Return
True if the provided type is equivalent to this, False otherwise.
"""
return cc.CharspanType.isinstance(ty)
def exp_pauli(self, theta, *args):
"""
Apply a general Pauli tensor product rotation, `exp(i theta P)`, on
the specified qubit register. The Pauli tensor product is provided
as a string, e.g. `XXYX` for a 4-qubit term. The angle parameter
can be provided as a concrete float or a `QuakeValue`.
"""
with self.insertPoint, self.loc:
quantumVal = None
qubitsList = []
pauliWordVal = None
for arg in args:
if isinstance(arg, cudaq_runtime.SpinOperator) or hasattr(
arg, "_to_spinop"):
if arg.get_term_count() > 1:
emitFatalError(
'exp_pauli operation requires a SpinOperator composed of a single term.'
)
arg = arg.to_string(False)
if isinstance(arg, str):
retTy = cc.PointerType.get(
self.ctx,
cc.ArrayType.get(self.ctx, IntegerType.get_signless(8),
int(len(arg) + 1)))
pauliWordVal = cc.CreateStringLiteralOp(retTy, arg)
elif isinstance(arg, QuakeValue) and quake.VeqType.isinstance(
arg.mlirValue.type):
quantumVal = arg.mlirValue
elif isinstance(arg, QuakeValue) and self.__isPauliWordType(
arg.mlirValue.type):
pauliWordVal = arg.mlirValue
elif isinstance(arg, QuakeValue) and quake.RefType.isinstance(
arg.mlirValue.type):
qubitsList.append(arg.mlirValue)
thetaVal = None
if isinstance(theta, float):
fty = mlirTypeFromPyType(float, self.ctx)
thetaVal = arith.ConstantOp(fty, FloatAttr.get(fty,
theta)).result
else:
thetaVal = theta.mlirValue
if len(qubitsList) > 0:
quantumVal = quake.ConcatOp(quake.VeqType.get(
self.ctx), [quantumVal] if quantumVal is not None else [] +
qubitsList).result
quake.ExpPauliOp(thetaVal, quantumVal, pauli=pauliWordVal)
def givens_rotation(self, angle, qubitA, qubitB):
"""
Add Givens rotation kernel (theta angle as a QuakeValue) to the
kernel builder object
"""
givens_builder(self, angle, qubitA, qubitB)
def fermionic_swap(self, angle, qubitA, qubitB):
"""
Add Fermionic SWAP rotation kernel (phi angle as a QuakeValue) to the
kernel builder object
"""
fermionic_swap_builder(self, angle, qubitA, qubitB)
def from_state(self, qubits, state):
emitFatalError("from_state not implemented.")
def u3(self, theta, phi, delta, target):
"""
Apply the universal three-parameters operator to target qubit.
The three parameters are Euler angles - θ, φ, and λ.
```python
# Example
kernel = cudaq.make_kernel()
q = cudaq.qubit()
kernel.u3(np.pi, np.pi, np.pi / 2, q)
```
"""
with self.insertPoint, self.loc:
parameters = [
get_parameter_value(self, p) for p in [theta, phi, delta]
]
if quake.RefType.isinstance(target.mlirValue.type):
quake.U3Op([], parameters, [], [target.mlirValue])
return
# Must be a `veq`, get the size
size = quake.VeqSizeOp(self.getIntegerType(), target.mlirValue)
def body(idx):
extracted = quake.ExtractRefOp(quake.RefType.get(self.ctx),
target.mlirValue,
-1,
index=idx).result
quake.U3Op([], parameters, [], [extracted])
self.createInvariantForLoop(size, body)
def cu3(self, theta, phi, delta, controls, target):
"""
Controlled u3 operation.
The controls parameter is expected to be a list of QuakeValue.
```python
# Example:
kernel = cudaq.make_kernel()
qubits = kernel.qalloc(2)
kernel.cu3(np.pi, np.pi, np.pi / 2, qubits[0], qubits[1]))
```
"""
with self.insertPoint, self.loc:
fwdControls = None
if isinstance(controls, list):
fwdControls = [c.mlirValue for c in controls]
elif quake.RefType.isinstance(
controls.mlirValue.type) or quake.VeqType.isinstance(
controls.mlirValue.type):
fwdControls = [controls.mlirValue]
else:
emitFatalError(f"invalid controls type for cu3.")
quake.U3Op(
[], [get_parameter_value(self, p) for p in [theta, phi, delta]],
fwdControls, [target.mlirValue])
def cswap(self, controls, qubitA, qubitB):
"""
Controlled swap of the states of the provided qubits.
The controls parameter is expected to be a list of QuakeValue.
```python
# Example:
kernel = cudaq.make_kernel()
# Allocate qubit/s to the `kernel`.
qubits = kernel.qalloc(2)
# Place the 0th qubit in the 1-state.
kernel.x(qubits[0])
# Swap their states.
kernel.swap(qubits[0], qubits[1]))
```
"""
fwdControls = None
if isinstance(controls, list):
fwdControls = [c.mlirValue for c in controls]
elif quake.RefType.isinstance(
controls.mlirValue.type) or quake.VeqType.isinstance(
controls.mlirValue.type):
fwdControls = [controls.mlirValue]
else:
emitFatalError(
f"Invalid control type for cswap ({type(controls)}).")
with self.insertPoint, self.loc:
quake.SwapOp([], [], fwdControls,
[qubitA.mlirValue, qubitB.mlirValue])