microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
joaoboechat/remove-web-worker-dependency

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/pip/tests/test_adaptive_gpu_quantum_ops.py

491lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4"""End-to-end tests for the adaptive GPU bytecode interpreter pipeline.
5
6Tests run Adaptive Profile QIR through the full pipeline:
7Python AdaptiveProfilePass → Rust receiver → GPU interpreter → results.
8
9Requires QDK_GPU_TESTS env var and a GPU adapter.
10
11For smaller tests covering the full Adaptive Profile instruction set,
12see `test_adaptive_gpu_bytecode.py`.
13"""
14
15import os
16import sys
17from collections import Counter
18
19import pytest
20
21# Skip all tests in this module if QDK_GPU_TESTS is not set
22if not os.environ.get("QDK_GPU_TESTS"):
23 pytest.skip("Skipping GPU tests (QDK_GPU_TESTS not set)", allow_module_level=True)
24
25SKIP_REASON = "GPU is not available"
26GPU_AVAILABLE = False
27
28try:
29 from qsharp._native import try_create_gpu_adapter
30
31 gpu_info = try_create_gpu_adapter()
32 print(f"*** USING GPU: {gpu_info}", file=sys.stderr)
33 GPU_AVAILABLE = True
34except OSError as e:
35 SKIP_REASON = str(e)
36
37from qsharp._simulation import GpuSimulator
38
39
40# ---------------------------------------------------------------------------
41# QIR source
42# ---------------------------------------------------------------------------
43
44# Example 1: Measure-and-correct (H → MResetZ → read_result → branch → X)
45# After H and MResetZ, qubit 0 collapses to |0⟩ or |1⟩ with equal probability.
46# If measured 1, X is applied to flip it back to |0⟩.
47# The result register records the measurement outcome before correction.
48# Expected histogram: ~50% "0" and ~50% "1" on result 0.
49MEASURE_AND_CORRECT_QIR = """\
50%Result = type opaque
51%Qubit = type opaque
52
53define void @ENTRYPOINT__main() #0 {
54entry:
55 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
56 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
57 %r = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 0 to %Result*))
58 br i1 %r, label %then, label %end
59
60then:
61 call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*))
62 br label %end
63
64end:
65 call void @__quantum__rt__tuple_record_output(i64 1, i8* null)
66 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
67 ret void
68}
69
70declare void @__quantum__qis__h__body(%Qubit*)
71declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*)
72declare i1 @__quantum__qis__read_result__body(%Result*)
73declare void @__quantum__qis__x__body(%Qubit*)
74declare void @__quantum__rt__tuple_record_output(i64, i8*)
75declare void @__quantum__rt__result_record_output(%Result*, i8*)
76
77attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" }
78"""
79
80# Example 3: Conditionally terminating loop
81# Repeatedly applies H → Mz → read_result until result is 1.
82# Each iteration has 50% chance of exiting. Loop iteration count
83# follows a geometric distribution with p=0.5.
84# Result register 0 always records 1 (the exit condition).
85CONDITIONAL_LOOP_QIR = """\
86%Result = type opaque
87%Qubit = type opaque
88
89define void @ENTRYPOINT__main() #0 {
90entry:
91 br label %loop
92
93loop:
94 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
95 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
96 %r = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 0 to %Result*))
97 br i1 %r, label %done, label %loop
98
99done:
100 call void @__quantum__rt__tuple_record_output(i64 1, i8* null)
101 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
102 ret void
103}
104
105declare void @__quantum__qis__h__body(%Qubit*)
106declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*)
107declare i1 @__quantum__qis__read_result__body(%Result*)
108declare void @__quantum__rt__tuple_record_output(i64, i8*)
109declare void @__quantum__rt__result_record_output(%Result*, i8*)
110
111attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" }
112"""
113
114
115# ---------------------------------------------------------------------------
116# Tests
117# ---------------------------------------------------------------------------
118
119
120@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
121def test_measure_and_correct_histogram():
122 """Example 1: H → MResetZ → read_result → conditional X.
123
124 Run 10000 shots and verify ~50/50 split of "0" and "1" outcomes.
125 The measurement result records whether H collapsed to |1⟩ (then X corrects).
126 """
127 sim = GpuSimulator()
128 sim.set_program(MEASURE_AND_CORRECT_QIR)
129 results = sim.run_shots(10000, seed=42)
130
131 shot_results = results["shot_results"]
132 assert len(shot_results) == 10000
133
134 counts = Counter(shot_results)
135 # Each shot produces a single-bit result string: "0" or "1"
136 count_0 = counts.get("0", 0)
137 count_1 = counts.get("1", 0)
138
139 # Verify ~50/50 within 10% tolerance (very generous for 10000 shots)
140 assert count_0 > 4000, f"Expected ~5000 '0' results, got {count_0}"
141 assert count_1 > 4000, f"Expected ~5000 '1' results, got {count_1}"
142 assert count_0 + count_1 == 10000, "All shots should produce a result"
143
144
145@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
146def test_measure_and_correct_no_errors():
147 """Example 1: All shots should complete without GPU errors."""
148 sim = GpuSimulator()
149 sim.set_program(MEASURE_AND_CORRECT_QIR)
150 results = sim.run_shots(1000, seed=123)
151
152 shot_result_codes = results["shot_result_codes"]
153 assert all(
154 code == 0 for code in shot_result_codes
155 ), f"Some shots had non-zero error codes: {[c for c in shot_result_codes if c != 0]}"
156
157
158@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
159def test_conditional_loop_all_results_are_one():
160 """Example 3: The loop exits only when measurement yields 1.
161
162 Every shot's recorded result should be "1" since the loop continues
163 until that outcome.
164 """
165 shots = 5000
166 sim = GpuSimulator()
167 sim.set_program(CONDITIONAL_LOOP_QIR)
168 results = sim.run_shots(shots, seed=99)
169
170 shot_results = results["shot_results"]
171 assert len(shot_results) == shots
172
173 counts = Counter(shot_results)
174 # Every shot should exit with result "1"
175 assert (
176 counts.get("1", 0) == shots
177 ), f"Expected all {shots} shots to produce '1', got counts: {counts}"
178
179
180@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
181def test_conditional_loop_no_errors():
182 """Example 3: All shots should complete without GPU errors."""
183 sim = GpuSimulator()
184 sim.set_program(CONDITIONAL_LOOP_QIR)
185 results = sim.run_shots(1000, seed=456)
186
187 shot_result_codes = results["shot_result_codes"]
188 assert all(
189 code == 0 for code in shot_result_codes
190 ), f"Some shots had non-zero error codes: {[c for c in shot_result_codes if c != 0]}"
191
192
193# Example 2: Loop with phi node — GHZ state preparation
194# Applies H to qubit 0, then loops from i=1 to 4,
195# applying CNOT(q0, q_i) in each iteration using a phi node
196# to track the loop counter. After the loop, all 5 qubits
197# are measured. This creates a GHZ-like state (|00000⟩ + |11111⟩)/√2,
198# so all 5 measurements must agree.
199LOOP_WITH_PHI_QIR = """\
200%Result = type opaque
201%Qubit = type opaque
202
203define void @ENTRYPOINT__main() #0 {
204entry:
205 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
206 br label %loop
207
208loop:
209 %i = phi i64 [ 1, %entry ], [ %next_i, %loop ]
210 %qi = inttoptr i64 %i to %Qubit*
211 call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* %qi)
212 %next_i = add i64 %i, 1
213 %cond = icmp sle i64 %next_i, 4
214 br i1 %cond, label %loop, label %measure
215
216measure:
217 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
218 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*))
219 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 2 to %Result*))
220 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 3 to %Result*))
221 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 4 to %Qubit*), %Result* inttoptr (i64 4 to %Result*))
222 call void @__quantum__rt__tuple_record_output(i64 5, i8* null)
223 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
224 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null)
225 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 2 to %Result*), i8* null)
226 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 3 to %Result*), i8* null)
227 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 4 to %Result*), i8* null)
228 ret void
229}
230
231declare void @__quantum__qis__h__body(%Qubit*)
232declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*)
233declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*)
234declare void @__quantum__rt__tuple_record_output(i64, i8*)
235declare void @__quantum__rt__result_record_output(%Result*, i8*)
236
237attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="5" "required_num_results"="5" }
238"""
239
240# Example 4: Classical boolean computation
241# Applies H to qubits 0 and 1, measures both, then computes
242# the AND of the two results. If both are 1, applies X to qubit 0
243# (which was reset to |0⟩ by MResetZ). Final measurement of qubit 0
244# records whether both original measurements were 1.
245# Expected: ~25% "1" (both measured 1) and ~75% "0".
246BOOLEAN_COMPUTATION_QIR = """\
247%Result = type opaque
248%Qubit = type opaque
249
250define void @ENTRYPOINT__main() #0 {
251entry:
252 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
253 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
254 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
255 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*))
256 %r0 = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 0 to %Result*))
257 %r1 = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 1 to %Result*))
258 %both = and i1 %r0, %r1
259 br i1 %both, label %then, label %else
260
261then:
262 call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*))
263 br label %end
264
265else:
266 br label %end
267
268end:
269 call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 2 to %Result*))
270 call void @__quantum__rt__tuple_record_output(i64 1, i8* null)
271 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 2 to %Result*), i8* null)
272 ret void
273}
274
275declare void @__quantum__qis__h__body(%Qubit*)
276declare void @__quantum__qis__x__body(%Qubit*)
277declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*)
278declare i1 @__quantum__qis__read_result__body(%Result*)
279declare void @__quantum__rt__tuple_record_output(i64, i8*)
280declare void @__quantum__rt__result_record_output(%Result*, i8*)
281
282attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="3" }
283"""
284
285
286# ---------------------------------------------------------------------------
287# Tests — Example 2: Loop with phi (GHZ state)
288# ---------------------------------------------------------------------------
289
290
291@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
292def test_loop_with_phi_ghz_histogram():
293 """Example 2: H → loop CNOT(q0, q_i) for i=1..4 → measure all.
294
295 Creates (|00000⟩ + |11111⟩)/√2. All 5 measurements must agree.
296 Run 10000 shots and verify only "00000" and "11111" appear near 50/50.
297 """
298 sim = GpuSimulator()
299 sim.set_program(LOOP_WITH_PHI_QIR)
300 results = sim.run_shots(10000, seed=42)
301
302 shot_results = results["shot_results"]
303 assert len(shot_results) == 10000
304
305 counts = Counter(shot_results)
306 # Only "00000" and "11111" should appear
307 assert set(counts.keys()) <= {
308 "00000",
309 "11111",
310 }, f"Unexpected outcomes in GHZ state: {counts}"
311
312 count_00000 = counts.get("00000", 0)
313 count_11111 = counts.get("11111", 0)
314
315 assert count_00000 > 4000, f"Expected ~5000 '00000' results, got {count_00000}"
316 assert count_11111 > 4000, f"Expected ~5000 '11111' results, got {count_11111}"
317 assert count_00000 + count_11111 == 10000, "All shots should produce a result"
318
319
320@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
321def test_loop_with_phi_no_errors():
322 """Example 2: All shots should complete without GPU errors."""
323 sim = GpuSimulator()
324 sim.set_program(LOOP_WITH_PHI_QIR)
325 results = sim.run_shots(1000, seed=123)
326
327 shot_result_codes = results["shot_result_codes"]
328 assert all(
329 code == 0 for code in shot_result_codes
330 ), f"Some shots had non-zero error codes: {[c for c in shot_result_codes if c != 0]}"
331
332
333# ---------------------------------------------------------------------------
334# Tests — Example 4: Boolean computation (AND gate)
335# ---------------------------------------------------------------------------
336
337
338@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
339def test_boolean_computation_histogram():
340 """Example 4: H(q0), H(q1) → MResetZ both → AND results → conditional X.
341
342 r2=1 only when both r0=1 AND r1=1 (~25% of shots).
343 Run 10000 shots and verify ~25% "1" and ~75% "0".
344 """
345 sim = GpuSimulator()
346 sim.set_program(BOOLEAN_COMPUTATION_QIR)
347 results = sim.run_shots(10000, seed=42)
348
349 shot_results = results["shot_results"]
350 assert len(shot_results) == 10000
351
352 counts = Counter(shot_results)
353 count_0 = counts.get("0", 0)
354 count_1 = counts.get("1", 0)
355
356 assert 1500 < count_1 < 3500, f"Expected ~2500 '1' results (~25%), got {count_1}"
357 assert 6500 < count_0 < 8500, f"Expected ~7500 '0' results (~75%), got {count_0}"
358 assert count_0 + count_1 == 10000, "All shots should produce a result"
359
360
361@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
362def test_boolean_computation_no_errors():
363 """Example 4: All shots should complete without GPU errors."""
364 sim = GpuSimulator()
365 sim.set_program(BOOLEAN_COMPUTATION_QIR)
366 results = sim.run_shots(1000, seed=456)
367
368 shot_result_codes = results["shot_result_codes"]
369 assert all(
370 code == 0 for code in shot_result_codes
371 ), f"Some shots had non-zero error codes: {[c for c in shot_result_codes if c != 0]}"
372
373
374# ---------------------------------------------------------------------------
375# QIR fixture — Example 5: Teleport chain
376# ---------------------------------------------------------------------------
377
378# Example 5: Teleport chain
379# Creates two Bell pairs: (q0,q1) and (q2,q4).
380# Teleports q1's state to q4 via measure-and-correct on the q1-q2 channel,
381# with separate mz and reset operations. After teleportation, q0 and q4
382# are entangled. The final measurements of q0 and q4 (results 4 and 5,
383# labeled "0_t0" and "0_t1") should be correlated: either both "0" or
384# both "1", with ~50/50 distribution.
385TELEPORT_CHAIN_QIR = """\
386%Result = type opaque
387%Qubit = type opaque
388
389@0 = internal constant [5 x i8] c"0_t0\\00"
390@1 = internal constant [5 x i8] c"0_t1\\00"
391
392define void @TeleportChain() #0 {
393entry:
394 call void @__quantum__rt__initialize(i8* null)
395 br label %body
396body:
397 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
398 call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
399 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*))
400 call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Qubit* inttoptr (i64 4 to %Qubit*))
401 call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 2 to %Qubit*))
402 call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
403 call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
404 call void @__quantum__qis__reset__body(%Qubit* inttoptr (i64 1 to %Qubit*))
405 %0 = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 0 to %Result*))
406 br i1 %0, label %then__1, label %continue__1
407then__1:
408 call void @__quantum__qis__z__body(%Qubit* inttoptr (i64 4 to %Qubit*))
409 br label %continue__1
410continue__1:
411 call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 1 to %Result*))
412 call void @__quantum__qis__reset__body(%Qubit* inttoptr (i64 2 to %Qubit*))
413 %1 = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 1 to %Result*))
414 br i1 %1, label %then__2, label %continue__2
415then__2:
416 call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 4 to %Qubit*))
417 br label %continue__2
418continue__2:
419 call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 2 to %Result*))
420 call void @__quantum__qis__reset__body(%Qubit* inttoptr (i64 0 to %Qubit*))
421 call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 4 to %Qubit*), %Result* inttoptr (i64 3 to %Result*))
422 call void @__quantum__qis__reset__body(%Qubit* inttoptr (i64 4 to %Qubit*))
423 br label %exit
424exit:
425 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 2 to %Result*), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @0, i32 0, i32 0))
426 call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 3 to %Result*), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @1, i32 0, i32 0))
427 ret void
428}
429
430declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*)
431declare void @__quantum__qis__h__body(%Qubit*)
432declare void @__quantum__qis__x__body(%Qubit*)
433declare void @__quantum__qis__z__body(%Qubit*)
434declare void @__quantum__qis__reset__body(%Qubit*)
435declare void @__quantum__qis__mz__body(%Qubit*, %Result*) #1
436declare void @__quantum__rt__initialize(i8*)
437declare i1 @__quantum__qis__read_result__body(%Result*)
438declare void @__quantum__rt__result_record_output(%Result*, i8*)
439
440attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="5" "required_num_results"="4" }
441attributes #1 = { "irreversible" }
442"""
443
444
445# ---------------------------------------------------------------------------
446# Tests — Example 5: Teleport chain
447# ---------------------------------------------------------------------------
448
449
450@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
451def test_teleport_chain_histogram():
452 """Example 5: Teleport chain with 2 Bell pairs and measure-and-correct.
453
454 Creates Bell pairs (q0,q1) and (q2,q4), then teleports q1's state to q4
455 via the q1→q2 channel. After teleportation, q0 and q4 are entangled.
456 Final measurements of q0 and q4 (results 2 and 3, labeled "0_t0" and
457 "0_t1") should be correlated: both "0" or both "1", near 50/50.
458 """
459 sim = GpuSimulator()
460 sim.set_program(TELEPORT_CHAIN_QIR)
461 results = sim.run_shots(10000, seed=42)
462
463 shot_results = results["shot_results"]
464 assert len(shot_results) == 10000
465
466 counts = Counter(shot_results)
467 # Only "00" and "11" should appear (results 4 and 5 are correlated)
468 assert set(counts.keys()) <= {
469 "00",
470 "11",
471 }, f"Unexpected outcomes in teleport chain: {counts}"
472
473 count_00 = counts.get("00", 0)
474 count_11 = counts.get("11", 0)
475
476 assert count_00 > 4000, f"Expected ~5000 '00' results, got {count_00}"
477 assert count_11 > 4000, f"Expected ~5000 '11' results, got {count_11}"
478 assert count_00 + count_11 == 10000, "All shots should produce a result"
479
480
481@pytest.mark.skipif(not GPU_AVAILABLE, reason=SKIP_REASON)
482def test_teleport_chain_no_errors():
483 """Example 5: All shots should complete without GPU errors."""
484 sim = GpuSimulator()
485 sim.set_program(TELEPORT_CHAIN_QIR)
486 results = sim.run_shots(1000, seed=789)
487
488 shot_result_codes = results["shot_result_codes"]
489 assert all(
490 code == 0 for code in shot_result_codes
491 ), f"Some shots had non-zero error codes: {[c for c in shot_result_codes if c != 0]}"