11#!/usr/bin/env python3
22"""A QRMI workload for a pod that fluence placed.
33
4- fluence picked a backend for this pod and the webhook put its name in the
5- environment as FLUXION_BACKEND, with the backend attributes as FLUXION_<KEY>.
6- This script takes it from there with QRMI. It acquires a session on that
7- backend with the credentials mounted on the pod, runs a Bell circuit through
8- the QRMI sampler, prints the counts, and releases the session. fluence holds
9- the graph allocation and this pod holds the session. A worker in a shared gang
10- is handed the scout session instead and submits into it without acquiring.
11-
12- For an IBM backend the credentials are the four <backend>_QRMI_IBM_QRS_*
13- variables, or IBM_CLOUD_TOKEN and IBM_CLOUD_CRN from which they are derived.
14- Without credentials this is a rehearsal. It prints what it would need and
15- exits cleanly, so the same pod proves the placement and the handoff on a
16- cluster with no account.
4+ fluence chose the backend and the webhook put its name in FLUXION_BACKEND.
5+ This acquires a session on it, runs a Bell circuit through the QRMI sampler,
6+ prints the counts, and releases the session. Without credentials it prints
7+ what it would need and exits, so the same pod works as a rehearsal.
178"""
189
1910import argparse
2415from qiskit import QuantumCircuit
2516from qiskit .transpiler .preset_passmanagers import generate_preset_pass_manager
2617from qrmi import QuantumResource , ResourceType
27- from qrmi .primitives .ibm import SamplerV2 , get_target
18+ from qrmi .primitives .ibm import SamplerV2 , get_backend
2819
29- # QRMI type strings to ResourceType names
3020RESOURCE_TYPES = {
31- "qiskit-runtime-service" : "IBMQiskitRuntimeService" ,
32- "ibm-quantum-system" : "IBMQuantumSystem" ,
33- "pasqal-cloud" : "PasqalCloud" ,
34- "pasqal-local" : "PasqalLocal" ,
35- "iqm-server" : "IQMServer" ,
36- "alice-bob-felis" : "AliceBobFelis" ,
37- }
38-
39- # what QRMI reads for each type, looked up as <backend><suffix>
40- REQUIRED_ENV = {
41- "qiskit-runtime-service" : (
42- "_QRMI_IBM_QRS_ENDPOINT" ,
43- "_QRMI_IBM_QRS_IAM_ENDPOINT" ,
44- "_QRMI_IBM_QRS_IAM_APIKEY" ,
45- "_QRMI_IBM_QRS_SERVICE_CRN" ,
46- ),
47- "ibm-quantum-system" : (
48- "_QRMI_IBM_QS_ENDPOINT" ,
49- "_QRMI_IBM_QS_IAM_ENDPOINT" ,
50- "_QRMI_IBM_QS_IAM_APIKEY" ,
51- "_QRMI_IBM_QS_SERVICE_CRN" ,
52- ),
53- "iqm-server" : ("_QRMI_IQM_ISA_ENDPOINT" , "_QRMI_IQM_ISA_TOKEN" ),
21+ "qiskit-runtime-service" : ("IBMQiskitRuntimeService" , "_QRMI_IBM_QRS" ),
22+ "ibm-quantum-system" : ("IBMQuantumSystem" , "_QRMI_IBM_QS" ),
5423}
55-
56- # the IBM endpoints are fixed, so the credentials reduce to a key and a CRN
57- IBM_QRS_DERIVED = {
58- "_QRMI_IBM_QRS_ENDPOINT" : "https://quantum.cloud.ibm.com/api/v1" ,
59- "_QRMI_IBM_QRS_IAM_ENDPOINT" : "https://iam.cloud.ibm.com" ,
24+ REQUIRED = ("_ENDPOINT" , "_IAM_ENDPOINT" , "_IAM_APIKEY" , "_SERVICE_CRN" )
25+ ENDPOINTS = {
26+ "_ENDPOINT" : "https://quantum.cloud.ibm.com/api/v1" ,
27+ "_IAM_ENDPOINT" : "https://iam.cloud.ibm.com" ,
6028}
6129
6230
6331def section (title ):
64- print ()
65- print ("== {} ==" .format (title ), flush = True )
32+ print ("\n == {} ==" .format (title ))
6633
6734
6835def print_environment ():
69- """What fluence injected, and which credentials are set, by name only."""
7036 section ("environment from fluence" )
7137 for name in sorted (os .environ ):
72- if name .startswith ("FLUXION_" ) or name .startswith ("FLUENCE_" ):
73- print ("{:<40} {}" .format (name , os .environ [name ]))
74- section ("QRMI environment" )
75- shown = False
76- for name in sorted (os .environ ):
77- if "QRMI" in name or name in ("IBM_CLOUD_TOKEN" , "IBM_CLOUD_CRN" ):
78- print ("{:<40} (set, value not shown)" .format (name ))
79- shown = True
80- if not shown :
81- print ("(none)" )
38+ if name .startswith (("FLUXION_" , "FLUENCE_" )):
39+ print ("{:<32} {}" .format (name , os .environ [name ]))
40+ section ("credentials, names only" )
41+ names = [n for n in sorted (os .environ ) if "QRMI" in n or n .startswith ("IBM_CLOUD_" )]
42+ print ("\n " .join (names ) or "(none)" )
8243
8344
8445def backend_from_fluence ():
85- """The backend fluence chose and its QRMI type, from the injected env."""
8646 backend = os .environ .get ("FLUXION_BACKEND" )
8747 if not backend :
88- sys .exit (
89- "no FLUXION_BACKEND: this pod was not placed by fluence, or the "
90- "webhook did not inject it (does the pod request a qpu?)"
91- )
48+ sys .exit ("no FLUXION_BACKEND, this pod was not placed by fluence" )
9249 rtype = os .environ .get ("FLUXION_QRMI_TYPE" , "qiskit-runtime-service" )
9350 if rtype not in RESOURCE_TYPES :
94- sys .exit ("unknown QRMI type {!r } for backend {}" .format (rtype , backend ))
51+ sys .exit ("unsupported QRMI type {} for {}" .format (rtype , backend ))
9552 section ("backend" )
9653 print ("backend: " , backend )
9754 print ("type: " , rtype )
9855 return backend , rtype
9956
10057
101- def credentials (backend , rtype ):
102- """Derive the QRMI variables from the IBM Secret and return what is still missing."""
103- if rtype == "qiskit-runtime-service" :
104- token = os .environ .get ("IBM_CLOUD_TOKEN" )
105- crn = os .environ .get ("IBM_CLOUD_CRN" )
106- derived = dict (IBM_QRS_DERIVED )
107- if token :
108- derived ["_QRMI_IBM_QRS_IAM_APIKEY" ] = token
109- if crn :
110- derived ["_QRMI_IBM_QRS_SERVICE_CRN" ] = crn
111- for suffix , value in derived .items ():
112- os .environ .setdefault (backend + suffix , value )
113- missing = [
114- backend + s
115- for s in REQUIRED_ENV .get (rtype , ())
116- if not os .environ .get (backend + s )
117- ]
118- if missing :
119- print ("missing: " , ", " .join (missing ))
120- else :
121- print ("credentials present for" , backend )
122- return missing
123-
124-
125- def acquire (backend , rtype , ready_timeout ):
126- """Acquire a QRMI session on the backend and wait until it accepts work."""
127- qr = QuantumResource (backend , getattr (ResourceType , RESOURCE_TYPES [rtype ]))
128- lock = qr .acquire ()
129- print ("session: " , lock )
130- # so anything else in this pod that constructs the resource joins this session
131- os .environ [backend + "_QRMI_JOB_ACQUISITION_TOKEN" ] = str (lock )
132- deadline = time .time () + ready_timeout
133- while ready_timeout > 0 :
134- try :
135- if qr .is_accessible ():
136- print ("backend: " , backend , "is accepting work" )
137- break
138- why = "not accepting work"
139- except Exception as e : # keep polling until the deadline
140- why = str (e )
58+ def missing_credentials (backend , rtype ):
59+ """Derive the QRMI variables from IBM_CLOUD_TOKEN and IBM_CLOUD_CRN, and
60+ return the names still unset."""
61+ prefix = backend + RESOURCE_TYPES [rtype ][1 ]
62+ derived = dict (ENDPOINTS )
63+ if os .environ .get ("IBM_CLOUD_TOKEN" ):
64+ derived ["_IAM_APIKEY" ] = os .environ ["IBM_CLOUD_TOKEN" ]
65+ if os .environ .get ("IBM_CLOUD_CRN" ):
66+ derived ["_SERVICE_CRN" ] = os .environ ["IBM_CLOUD_CRN" ]
67+ for suffix , value in derived .items ():
68+ os .environ .setdefault (prefix + suffix , value )
69+ return [prefix + s for s in REQUIRED if not os .environ .get (prefix + s )]
70+
71+
72+ def resource (backend , rtype ):
73+ return QuantumResource (backend , getattr (ResourceType , RESOURCE_TYPES [rtype ][0 ]))
74+
75+
76+ def wait_operational (qr , backend , timeout ):
77+ """The backend object, once it reports itself operational."""
78+ deadline = time .time () + timeout
79+ while True :
80+ dev = get_backend (qr )
81+ status = dev .status ()
82+ if status .operational :
83+ print ("backend: " , backend , status .status_msg )
84+ return dev
14185 if time .time () >= deadline :
142- qr .release (lock )
14386 sys .exit (
144- "{} did not become usable within {}s: {}" .format (
145- backend , ready_timeout , why
87+ "{} not operational after {}s: {}" .format (
88+ backend , timeout , status . status_msg
14689 )
14790 )
14891 time .sleep (5 )
149- return qr , lock
15092
15193
152- def build_circuit (qubits ):
94+ def bell_circuit (qubits ):
15395 qc = QuantumCircuit (qubits )
15496 qc .h (0 )
15597 for q in range (1 , qubits ):
@@ -158,129 +100,90 @@ def build_circuit(qubits):
158100 return qc
159101
160102
161- def transpile_for_device (qc , qr ):
162- target = get_target (qr )
163- isa = generate_preset_pass_manager (optimization_level = 1 , target = target ).run (qc )
103+ def run (qr , dev , qubits , shots ):
104+ isa = generate_preset_pass_manager (optimization_level = 1 , target = dev .target ).run (
105+ bell_circuit (qubits )
106+ )
164107 print ("circuit: " , dict (isa .count_ops ()))
165- return isa
166-
167-
168- def run_sampler (qr , isa , shots ):
169- sampler = SamplerV2 (qr , options = {"default_shots" : shots })
170- job = sampler .run ([isa ])
171- print ("task: " , job .job_id (), "submitted into the session" , flush = True )
172- return job
173-
174-
175- def wait_for_task (job , interval = 2.0 ):
108+ job = SamplerV2 (qr , options = {"default_shots" : shots }).run ([isa ])
109+ print ("task: " , job .job_id ())
176110 started = time .time ()
177111 last = None
178112 while not job .in_final_state ():
179- status = job .status ()
180- if status != last :
181- print (
182- "status: " ,
183- status ,
184- "at {:.0f}s" .format (time .time () - started ),
185- flush = True ,
186- )
187- last = status
188- time .sleep (interval )
113+ if job .status () != last :
114+ last = job .status ()
115+ print ("status: " , last , "at {:.0f}s" .format (time .time () - started ))
116+ time .sleep (2 )
189117 print ("status: " , job .status (), "after {:.0f}s" .format (time .time () - started ))
190118 if job .errored ():
191- print ("logs:" )
192119 print (job .logs ())
193120 sys .exit ("the task failed" )
194-
195-
196- def report_counts (job , qubits , shots ):
197121 counts = job .result ()[0 ].data .meas .get_counts ()
198122 print ("counts: " , dict (sorted (counts .items (), key = lambda kv : - kv [1 ])))
199123 ideal = {"0" * qubits , "1" * qubits }
200124 good = sum (v for k , v in counts .items () if k in ideal )
201125 print ("fidelity:" , "{}/{} shots in {}" .format (good , shots , sorted (ideal )))
202126
203127
204- def release (qr , lock ):
205- qr .release (lock )
206- print ("released:" , lock )
207-
208-
209- def linger (seconds , step = 5 ):
210- if seconds <= 0 :
211- return
212- section ("lingering for {}s so the pod can be watched" .format (seconds ))
213- left = seconds
214- while left > 0 :
215- print ("working, {}s left" .format (left ), flush = True )
216- time .sleep (min (step , left ))
217- left -= step
218-
219-
220128def main ():
129+ sys .stdout .reconfigure (line_buffering = True )
221130 ap = argparse .ArgumentParser (description = __doc__ .split ("\n " )[0 ])
222131 ap .add_argument ("--shots" , type = int , default = 100 )
223- ap .add_argument (
224- "--qubits" , type = int , default = 2 , help = "width of the GHZ/Bell circuit, 2"
225- )
226- ap .add_argument (
227- "--linger" ,
228- type = int ,
229- default = int (os .environ .get ("LINGER" , "0" )),
230- help = "seconds to stay alive after the work, 0" ,
231- )
132+ ap .add_argument ("--qubits" , type = int , default = 2 , help = "width of the circuit" )
232133 ap .add_argument (
233134 "--ready-timeout" ,
234135 type = float ,
235- default = float ( os . environ . get ( "READY_TIMEOUT" , " 120" )) ,
236- help = "seconds to wait for the backend to accept work after acquiring, 120 " ,
136+ default = 120 ,
137+ help = "seconds to wait for the backend" ,
237138 )
238139 args = ap .parse_args ()
239140
240141 print_environment ()
241142 backend , rtype = backend_from_fluence ()
242-
243- section ("credentials, names only" )
244- if credentials (backend , rtype ):
245- print ()
246- print ("rehearsal: no credentials, so nothing is acquired or submitted. The" )
247- print ("placement and the handoff from fluence worked. A Secret with the" )
248- print ("variables above would make this a real run." )
249- linger (args .linger )
143+ missing = missing_credentials (backend , rtype )
144+ if missing :
145+ print ("missing: " , ", " .join (missing ))
146+ print (
147+ "\n rehearsal, nothing acquired or submitted. The placement and the handoff worked."
148+ )
250149 return
251150
252- role = os .environ .get ("FLUENCE_COORDINATION_ROLE" )
253151 session = os .environ .get ("FLUENCE_QUANTUM_SESSION_ID" )
254- if role == "worker" and session :
255- # a worker joins the scout session rather than acquiring its own
256- section ("circuit in the scout session, through QRMI" )
257- print ("session: " , session , "from the scout" )
152+ if os .environ .get ("FLUENCE_COORDINATION_ROLE" ) == "worker" :
153+ # a worker submits into the scout's session instead of acquiring one
154+ if not session :
155+ print (
156+ "worker with no session, task" ,
157+ os .environ .get ("FLUENCE_QUANTUM_JOB_ID" , "?" ),
158+ )
159+ return
160+ section ("circuit in the scout's session" )
161+ print ("session: " , session )
258162 os .environ [backend + "_QRMI_JOB_ACQUISITION_TOKEN" ] = session
259- qr = QuantumResource (backend , getattr (ResourceType , RESOURCE_TYPES [rtype ]))
260- isa = transpile_for_device (build_circuit (args .qubits ), qr )
261- job = run_sampler (qr , isa , args .shots )
262- wait_for_task (job )
263- report_counts (job , args .qubits , args .shots )
264- linger (args .linger )
265- return
266- if role == "worker" :
267- print (
268- "role: worker with no session, task" ,
269- os .environ .get ("FLUENCE_QUANTUM_JOB_ID" , "?" ),
163+ qr = resource (backend , rtype )
164+ run (
165+ qr ,
166+ wait_operational (qr , backend , args .ready_timeout ),
167+ args .qubits ,
168+ args .shots ,
270169 )
271- linger (args .linger )
272170 return
273171
274172 section ("session and circuit, through QRMI" )
275- qr , lock = acquire (backend , rtype , args .ready_timeout )
173+ qr = resource (backend , rtype )
174+ lock = qr .acquire ()
175+ print ("session: " , lock )
176+ os .environ [backend + "_QRMI_JOB_ACQUISITION_TOKEN" ] = str (lock )
276177 try :
277- isa = transpile_for_device (build_circuit (args .qubits ), qr )
278- job = run_sampler (qr , isa , args .shots )
279- wait_for_task (job )
280- report_counts (job , args .qubits , args .shots )
178+ run (
179+ qr ,
180+ wait_operational (qr , backend , args .ready_timeout ),
181+ args .qubits ,
182+ args .shots ,
183+ )
281184 finally :
282- release (qr , lock )
283- linger ( args . linger )
185+ qr . release (lock )
186+ print ( "released:" , lock )
284187
285188
286189if __name__ == "__main__" :
0 commit comments