Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions source/pip/qsharp/_adaptive_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@

from __future__ import annotations
from dataclasses import dataclass, astuple
from enum import Enum
import pyqir
import struct
from typing import Any, Dict, List, Optional, Tuple, TypeAlias, cast
from ._adaptive_bytecode import *


class Bytecode(Enum):
Bit32 = 1
Bit64 = 2


# ---------------------------------------------------------------------------
# Gate name → OpID mapping (must match shader_types.rs OpID enum)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -193,16 +200,19 @@ class SwitchCase:
@dataclass
class IntOperand:
val: int = 0
bits: int = 32

def __post_init__(self):
# Mask to u32 range so negative Python ints become their
# two's-complement u32 representation (e.g. -7 → 0xFFFFFFF9).
self.val = self.val & 0xFFFFFFFF
# Mask to the appropriate word-width so negative Python ints become
# their two's-complement representation
# (e.g. -7 → 0xFFFFFFF9 for 32-bit, 0xFFFFFFFFFFFFFFF9 for 64-bit).
mask = (1 << self.bits) - 1
self.val = self.val & mask


class FloatOperand:
def __init__(self, val: float = 0.0) -> None:
self.val: int = encode_float_as_bits(val)
def __init__(self, val: float, bytecode_kind: Bytecode) -> None:
self.val: int = encode_float_as_bits(val, bytecode_kind)


@dataclass
Expand Down Expand Up @@ -255,14 +265,17 @@ def unwrap_operands(
return (dst, src0, src1, aux0, aux1, aux2, aux3)


def encode_float_as_bits(val: float) -> int:
return struct.unpack("<I", struct.pack("<f", val))[0]
def encode_float_as_bits(val: float, bytecode_kind: Bytecode) -> int:
if bytecode_kind == Bytecode.Bit32:
return struct.unpack("<I", struct.pack("<f", val))[0]
else:
return struct.unpack("<Q", struct.pack("<d", val))[0]


class AdaptiveProfilePass:
"""Walks Adaptive Profile QIR and emits the intermediate format for Rust."""

def __init__(self):
def __init__(self, bytecode_kind: Bytecode):
# Output tables.
self.blocks: List[Block] = []
self.instructions: List[Instruction] = []
Expand All @@ -275,6 +288,7 @@ def __init__(self):
self.register_types: List[RegisterType] = []

# Internal tracking.
self._bytecode_kind = bytecode_kind
self._next_reg: int = 0
self._next_block: int = 0
self._next_qop: int = 0
Expand All @@ -283,6 +297,7 @@ def __init__(self):
self._func_to_id: Dict[str, int] = {} # function name → function ID
self._current_func_is_entry: bool = True
self._noise_intrinsics: Optional[Dict[str, int]] = None
self._int_bits = 32 if bytecode_kind == Bytecode.Bit32 else 64

def run(
self,
Expand Down Expand Up @@ -425,11 +440,11 @@ def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Re

if isinstance(value, pyqir.IntConstant):
val = value.value
return IntOperand(val)
return IntOperand(val, self._int_bits)

if isinstance(value, pyqir.FloatConstant):
val = value.value
return FloatOperand(val)
return FloatOperand(val, self._bytecode_kind)

# Forward reference (e.g. phi incoming from a later block).
# Pre-allocate a register; the defining instruction will reuse it
Expand Down Expand Up @@ -752,7 +767,7 @@ def _emit_quantum_call(self, call: pyqir.Call) -> None:
angle = self._resolve_angle_operand(call.args[0])
else:
qubit_arg_offset = 0
angle = FloatOperand()
angle = FloatOperand(0.0, self._bytecode_kind)
qubit_arg_offset = 1 if gate_name in ROTATION_GATES else 0
q1, q2, q3 = self._resolve_qubit_operands(call.args[qubit_arg_offset:])
qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val)
Expand Down
36 changes: 36 additions & 0 deletions source/pip/qsharp/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,42 @@ def run_cpu_full_state(
"""
...

def run_cpu_adaptive(
input: dict,
shots: int,
noise: Optional[NoiseConfig] = None,
seed: Optional[int] = None,
) -> List[str]:
"""
Run an adaptive profile QIR program on a CPU full-state simulator.

The input is an `AdaptiveProgram` converted to a dict using the
.as_dict() method. Uses 64-bit bytecode for full LLVM i64 semantics.

Returns a list of result strings. Each result string is composed
of '0's, '1's, and 'L's, representing if each measurement result
was a Zero, One, or Loss respectively.
"""
...

def run_clifford_adaptive(
input: dict,
shots: int,
noise: Optional[NoiseConfig] = None,
seed: Optional[int] = None,
) -> List[str]:
"""
Run an adaptive profile QIR program on a Clifford stabilizer simulator.

The input is an `AdaptiveProgram` converted to a dict using the
.as_dict() method. Uses 64-bit bytecode for full LLVM i64 semantics.

Returns a list of result strings. Each result string is composed
of '0's, '1's, and 'L's, representing if each measurement result
was a Zero, One, or Loss respectively.
"""
...

def try_create_gpu_adapter() -> str:
"""
Checks if a compatible GPU adapter is available on the system.
Expand Down
131 changes: 80 additions & 51 deletions source/pip/qsharp/_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
QirInstructionId,
QirInstruction,
run_clifford,
run_clifford_adaptive,
run_parallel_shots,
run_adaptive_parallel_shots,
run_cpu_adaptive,
run_cpu_full_state,
NoiseConfig,
GpuContext,
Expand All @@ -25,7 +27,12 @@
)
from ._qsharp import QirInputData, Result
from typing import TYPE_CHECKING
from ._adaptive_pass import AdaptiveProfilePass, OP_RECORD_OUTPUT
from ._adaptive_pass import (
AdaptiveProfilePass,
AdaptiveProgram,
Bytecode,
OP_RECORD_OUTPUT,
)

if TYPE_CHECKING: # This is in the pyi file only
from ._native import GpuShotResults
Expand Down Expand Up @@ -485,26 +492,67 @@ def is_adaptive(mod: pyqir.Module) -> bool:
return func_attrs["qir_profiles"].string_value == "adaptive_profile"


def str_to_result(result: str):
match result:
case "0":
return Result.Zero
case "1":
return Result.One
case "L":
return Result.Loss
case _:
raise ValueError(f"Invalid result {result}")


def run_adaptive(
rust_run_adaptive_fn: Callable,
program: AdaptiveProgram,
shots: int,
noise: Optional[NoiseConfig],
seed: int,
):
"""
Runs an adaptive program given a rust simulator. Adds output recording logic.
"""
results = rust_run_adaptive_fn(program.as_dict(), shots, noise, seed)
# Extract recorded output result indices from the bytecode.
# OP_RECORD_OUTPUT with aux1=0 is result_record_output where
# src0 is the result index in the results buffer.
recorded_result_indices = []
for ins in program.instructions:
if (ins.opcode & 0xFF) == OP_RECORD_OUTPUT and ins.aux1 == 0:
recorded_result_indices.append(ins.src0)
# Filter shot_results to only include recorded output indices
filtered = []
for s in results:
filtered.append([str_to_result(s[i]) for i in recorded_result_indices])
return filtered


def run_qir_clifford(
input: Union[QirInputData, str, bytes],
shots: Optional[int] = 1,
noise: Optional[NoiseConfig] = None,
seed: Optional[int] = None,
) -> List:
(mod, shots, noise, seed) = preprocess_simulation_input(input, shots, noise, seed)
if noise is None:
(gates, num_qubits, num_results) = AggregateGatesPass().run(mod)
if is_adaptive(mod):
program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise)
return run_adaptive(run_clifford_adaptive, program, shots, noise, seed)
else:
(gates, num_qubits, num_results) = CorrelatedNoisePass(noise).run(mod)
recorder = OutputRecordingPass()
recorder.run(mod)

return list(
map(
recorder.process_output,
run_clifford(gates, num_qubits, num_results, shots, noise, seed),
if noise is None:
(gates, num_qubits, num_results) = AggregateGatesPass().run(mod)
else:
(gates, num_qubits, num_results) = CorrelatedNoisePass(noise).run(mod)
recorder = OutputRecordingPass()
recorder.run(mod)

return list(
map(
recorder.process_output,
run_clifford(gates, num_qubits, num_results, shots, noise, seed),
)
)
)


def run_qir_cpu(
Expand All @@ -514,31 +562,23 @@ def run_qir_cpu(
seed: Optional[int] = None,
) -> List:
(mod, shots, noise, seed) = preprocess_simulation_input(input, shots, noise, seed)
if noise is None:
(gates, num_qubits, num_results) = AggregateGatesPass().run(mod)
if is_adaptive(mod):
program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise)
return run_adaptive(run_cpu_adaptive, program, shots, noise, seed)
else:
(gates, num_qubits, num_results) = CorrelatedNoisePass(noise).run(mod)
recorder = OutputRecordingPass()
recorder.run(mod)

return list(
map(
recorder.process_output,
run_cpu_full_state(gates, num_qubits, num_results, shots, noise, seed),
)
)

if noise is None:
(gates, num_qubits, num_results) = AggregateGatesPass().run(mod)
else:
(gates, num_qubits, num_results) = CorrelatedNoisePass(noise).run(mod)
recorder = OutputRecordingPass()
recorder.run(mod)

def str_to_result(result: str):
match result:
case "0":
return Result.Zero
case "1":
return Result.One
case "L":
return Result.Loss
case _:
raise ValueError(f"Invalid result {result}")
return list(
map(
recorder.process_output,
run_cpu_full_state(gates, num_qubits, num_results, shots, noise, seed),
)
)


def run_qir_gpu(
Expand All @@ -551,21 +591,8 @@ def run_qir_gpu(
# Ccx is not support in the GPU simulator, decompose it
DecomposeCcxPass().run(mod)
if is_adaptive(mod):
program = AdaptiveProfilePass().run(mod, noise)
results = run_adaptive_parallel_shots(program.as_dict(), shots, noise, seed)

# Extract recorded output result indices from the bytecode.
# OP_RECORD_OUTPUT with aux1=0 is result_record_output where
# src0 is the result index in the results buffer.
recorded_result_indices = []
for ins in program.instructions:
if (ins.opcode & 0xFF) == OP_RECORD_OUTPUT and ins.aux1 == 0:
recorded_result_indices.append(ins.src0)
# Filter shot_results to only include recorded output indices
filtered = []
for s in results:
filtered.append([str_to_result(s[i]) for i in recorded_result_indices])
return filtered
program = AdaptiveProfilePass(Bytecode.Bit32).run(mod, noise)
return run_adaptive(run_adaptive_parallel_shots, program, shots, noise, seed)
else:
if noise is None:
(gates, num_qubits, num_results) = AggregateGatesPass().run(mod)
Expand Down Expand Up @@ -646,7 +673,9 @@ def set_program(self, input: Union[QirInputData, str, bytes]):
noise_intrinsics = None
if self.tables is not None:
noise_intrinsics = {name: table_id for table_id, name, _ in self.tables}
program = AdaptiveProfilePass().run(mod, noise_intrinsics=noise_intrinsics)
program = AdaptiveProfilePass(Bytecode.Bit32).run(
mod, noise_intrinsics=noise_intrinsics
)
self.gpu_context.set_adaptive_program(program.as_dict())

# Extract recorded output result indices from the bytecode.
Expand Down
4 changes: 3 additions & 1 deletion source/pip/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
noisy_simulator::register_noisy_simulator_submodule,
qir_simulation::{
IdleNoiseParams, NoiseConfig, NoiseTable, QirInstruction, QirInstructionId,
cpu_simulators::{run_clifford, run_cpu_full_state},
cpu_simulators::{run_clifford, run_clifford_adaptive, run_cpu_adaptive, run_cpu_full_state},
gpu_full_state::{
GpuContext, run_adaptive_parallel_shots, run_parallel_shots, try_create_gpu_adapter,
},
Expand Down Expand Up @@ -134,6 +134,8 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(run_clifford, m)?)?;
m.add_function(wrap_pyfunction!(try_create_gpu_adapter, m)?)?;
m.add_function(wrap_pyfunction!(run_cpu_full_state, m)?)?;
m.add_function(wrap_pyfunction!(run_cpu_adaptive, m)?)?;
m.add_function(wrap_pyfunction!(run_clifford_adaptive, m)?)?;
m.add_function(wrap_pyfunction!(run_parallel_shots, m)?)?;
m.add_function(wrap_pyfunction!(run_adaptive_parallel_shots, m)?)?;
m.add("QSharpError", py.get_type::<QSharpError>())?;
Expand Down
Loading
Loading