Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1534e80
make bytecode generic for 32 and 64 bit word size
orpuente-MS Mar 31, 2026
18ed36d
[wip] add cpu bytecode interpreter
orpuente-MS Apr 1, 2026
88cd287
cargo fmt
orpuente-MS Apr 1, 2026
b2634d3
better python signatures
orpuente-MS Apr 2, 2026
37bb014
add back operands laoyout comment in _emit_switch
orpuente-MS Apr 2, 2026
c5cc017
remove `s`, `s_adj`, and `z` noise overriding
orpuente-MS Apr 2, 2026
20007aa
make `VOID_RETURN` sentil value depend on bytecode word width
orpuente-MS Apr 2, 2026
11eac42
cleanup runtime.rs
orpuente-MS Apr 2, 2026
78134d3
move `z`, `s`, `s_adj` inherit-noise-from-rz logic to device specific…
orpuente-MS Apr 2, 2026
4c32c72
fix dynamic angles
orpuente-MS Apr 7, 2026
590d8b8
better comments
orpuente-MS Apr 7, 2026
1377d9b
Merge branch 'main' into oscarpuente/adaptive-support-for-cpu-simulators
orpuente-MS Apr 10, 2026
f2a7d77
Merge branch 'main' into oscarpuente/adaptive-support-for-cpu-simulators
orpuente-MS Apr 22, 2026
ac256ab
Merge branch 'main' into oscarpuente/adaptive-support-for-cpu-simulators
orpuente-MS Apr 22, 2026
d0a885c
Merge branch 'main' into oscarpuente/adaptive-support-for-cpu-simulators
orpuente-MS Apr 22, 2026
c29adc1
Merge branch 'main' into oscarpuente/adaptive-support-for-cpu-simulators
orpuente-MS Apr 23, 2026
9ef68fc
address PR feedback
orpuente-MS Apr 27, 2026
77a1580
Merge branch 'main' into oscarpuente/adaptive-support-for-cpu-simulators
orpuente-MS Apr 27, 2026
2bf618d
Fix multiple bugs uncovered by recently enable integration tests
orpuente-MS Apr 28, 2026
db84962
add int overflow check
orpuente-MS Apr 28, 2026
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
4 changes: 1 addition & 3 deletions source/pip/qsharp/_adaptive_bytecode.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
OP_RESET = 0x12
OP_READ_RESULT = 0x13
OP_RECORD_OUTPUT = 0x14
OP_READ_LOSS = 0x15

# ── Integer Arithmetic ───────────────────────────────────────────────────────
OP_ADD = 0x20
Expand Down Expand Up @@ -127,6 +128,3 @@
REG_TYPE_F32 = 3
REG_TYPE_F64 = 4
REG_TYPE_PTR = 5

# ── Sentinel values ──────────────────────────────────────────────────────────
VOID_RETURN = 0xFFFFFFFF # Function does not have a return value.
124 changes: 94 additions & 30 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 = 32
Bit64 = 64


# ---------------------------------------------------------------------------
# Gate name → OpID mapping (must match shader_types.rs OpID enum)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -48,6 +55,7 @@
"mz": 21,
"mresetz": 22,
"swap": 24,
"move": 28,
}

# Gates that take a result ID as a second argument
Expand All @@ -59,6 +67,12 @@
# Rotation gates that take an angle parameter as first argument
ROTATION_GATES = {"rx", "ry", "rz", "rxx", "ryy", "rzz"}

# Single-qubit gates whose QIR signature carries device-specific extra
# arguments after the qubit pointer (e.g. ``move(qubit, i64, i64)``). The
# extra args are scheduling metadata for hardware backends and are not
# qubit IDs, so we resolve only ``args[0]`` and ignore the rest.
MOVE_GATES = {"move"}

# ---------------------------------------------------------------------------
# ICmp / FCmp predicate mappings
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -160,7 +174,14 @@ class QuantumOp:
q1: int
q2: int
q3: int
angle: float
# ``angle`` is stored as the raw bit pattern of an IEEE-754 float
# (encoded via ``encode_float_as_bits``) so it can be packed into the
# same integer-typed FFI table as the qubit indices. The Rust side
# reinterprets these bits as f32/f64 depending on the bytecode width.
#
# This also follows the same pattern in which floats are encoded as ints
# in the ``Instruction`` class.
angle: int
Comment thread
billti marked this conversation as resolved.


@dataclass
Expand Down Expand Up @@ -192,17 +213,23 @@ class SwitchCase:

@dataclass
class IntOperand:
val: int = 0
val: int
bits: int

def __post_init__(self):
Comment thread
orpuente-MS marked this conversation as resolved.
# 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
min_val = -(1 << (self.bits - 1))
if self.val < min_val or self.val > mask:
raise ValueError(f"Value {self.val} does not fit in {self.bits} bits")
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 +282,24 @@ 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]


def void_return(bytecode_kind: Bytecode):
if bytecode_kind == Bytecode.Bit32:
return 0xFFFF_FFFF
else:
return 0xFFFF_FFFF_FFFF_FFFF


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 +312,8 @@ def __init__(self):
self.register_types: List[RegisterType] = []

# Internal tracking.
self._bytecode_kind = bytecode_kind
self._int_bits = bytecode_kind.value
self._next_reg: int = 0
self._next_block: int = 0
self._next_qop: int = 0
Expand Down Expand Up @@ -401,7 +440,7 @@ def _emit_quantum_op(
q1: int = 0,
q2: int = 0,
q3: int = 0,
angle: float = 0.0,
angle: int = 0,
) -> int:
idx = self._next_qop
self._next_qop += 1
Expand All @@ -425,11 +464,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 All @@ -442,7 +481,7 @@ def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Re
# Try extracting as a qubit/result pointer constant.
pid = pyqir.ptr_id(value)
if pid is not None:
return IntOperand(pid)
return IntOperand(pid, self._int_bits)
# Null pointer
if value.is_null:
reg = self._alloc_reg(value, REG_TYPE_PTR)
Expand Down Expand Up @@ -681,9 +720,15 @@ def _emit_call(self, call: pyqir.Call) -> None:
| "__quantum__rt__begin_parallel"
| "__quantum__rt__end_parallel"
| "__quantum__qis__barrier__body"
| "__quantum__rt__read_loss"
):
pass # No-op
case "__quantum__rt__read_loss":
# Allocate a bool register and emit OP_READ_LOSS so the runtime
# can ask the simulator whether the given result was produced
# by measuring a lost qubit. Programs may branch on this value.
dst = self._alloc_reg(call, REG_TYPE_BOOL)
result_reg = self._resolve_result_operand(call.args[0])
self._emit(OP_READ_LOSS, dst=dst, src0=result_reg)
case _ if callee in self._func_to_id:
self._emit_ir_function_call(call)
case _ if "qdk_noise" in call.callee.attributes.func:
Expand All @@ -699,7 +744,11 @@ def _emit_call(self, call: pyqir.Call) -> None:
def _resolve_qubit_operands(
self, args: List[pyqir.Value]
) -> Tuple[IntOperand | Reg, IntOperand | Reg, IntOperand | Reg]:
qs: List[IntOperand | Reg] = [IntOperand(), IntOperand(), IntOperand()]
qs: List[IntOperand | Reg] = [
IntOperand(0, self._int_bits),
IntOperand(0, self._int_bits),
IntOperand(0, self._int_bits),
]
for i, arg in enumerate(args):
qs[i] = self._resolve_qubit_operand(arg)
return (qs[0], qs[1], qs[2])
Expand Down Expand Up @@ -744,17 +793,36 @@ def _emit_quantum_call(self, call: pyqir.Call) -> None:
aux1=q,
)
return
if gate_name in MOVE_GATES:
# ``move(qubit, i64, i64)``: only the first arg is a qubit; the
# remaining args are device-specific scheduling metadata that
# the simulator ignores. Emit a single-qubit OP_QUANTUM_GATE so
# the runtime invokes ``Simulator::mov`` (which applies the
# configured ``noise.mov`` faults to that qubit).
q1, q2, q3 = self._resolve_qubit_operands([call.args[0]])
angle = FloatOperand(0.0, self._bytecode_kind)
qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val)
self._emit(
OP_QUANTUM_GATE,
src0=angle,
aux0=qop_idx,
aux1=q1,
aux2=q2,
aux3=q3,
)
return
if gate_name in ROTATION_GATES:
qubit_arg_offset = 1
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)
self._emit(
OP_QUANTUM_GATE,
src0=angle,
aux0=qop_idx,
aux1=q1,
aux2=q2,
Expand Down Expand Up @@ -795,8 +863,8 @@ def _emit_noise_intrinsic_call(self, call: pyqir.Call) -> None:
self._emit(
OP_QUANTUM_GATE,
aux0=qop_idx,
aux1=IntOperand(qubit_count),
aux2=IntOperand(arg_offset),
aux1=IntOperand(qubit_count, self._int_bits),
aux2=IntOperand(arg_offset, self._int_bits),
)
elif self._noise_intrinsics is not None:
raise ValueError(f"Missing noise intrinsic: {callee_name}")
Expand Down Expand Up @@ -860,18 +928,14 @@ def _emit_switch(self, switch_instr: pyqir.Switch) -> None:
compilation). ``operands`` is not affected by this behavior.
"""
# operands layout: [cond, default_block, case_val0, case_block0, ...]
ops = switch_instr.operands
cond_reg = self._resolve_operand(ops[0])
default_block = self._block_to_id[ops[1]]
cond_reg = self._resolve_operand(switch_instr.operands[0])
default_block = self._block_to_id[switch_instr.default]
case_offset = len(self.switch_cases)
num_case_pairs = (len(ops) - 2) // 2
for i in range(num_case_pairs):
case_val = ops[2 + 2 * i]
case_block = ops[2 + 2 * i + 1]
target_block = self._block_to_id[case_block]
for case_val, block in switch_instr.cases:
target_block = self._block_to_id[block]
switch_case = SwitchCase(case_val.value, target_block)
self.switch_cases.append(switch_case)
case_count = num_case_pairs
case_count = len(switch_instr.cases)
self._emit(
OP_SWITCH,
src0=cond_reg,
Expand All @@ -896,7 +960,7 @@ def _emit_ret(self, instr: Any) -> None:
self._emit(OP_RET, dst=ret_reg)
else:
# Void return — use immediate 0 as exit code.
self._emit(OP_RET, dst=IntOperand(0))
self._emit(OP_RET, dst=IntOperand(0, self._int_bits))

# ------------------------------------------------------------------
# Comparison emitters
Expand Down Expand Up @@ -960,7 +1024,7 @@ def _emit_ir_function_call(self, call: Any) -> None:
self.call_args.append(reg.val)
# Allocate return register if function has non-void return type
if call.type.is_void:
return_reg = VOID_RETURN # no return
return_reg = void_return(self._bytecode_kind) # no return
else:
return_reg = self._alloc_reg(call, REG_TYPE_I32)
self._emit(
Expand Down
28 changes: 28 additions & 0 deletions source/pip/qsharp/_device/_atom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,34 @@ def simulate(
if noise is None:
noise = NoiseConfig()

# Override t, t_adj, s, s_adj, and z noise if they are unset and rz noise is set.
if noise and not noise.rz.is_noiseless():
if noise.t.is_noiseless():
noise.t.x = noise.rz.x
noise.t.y = noise.rz.y
noise.t.z = noise.rz.z
noise.t.loss = noise.rz.loss
if noise.t_adj.is_noiseless():
noise.t_adj.x = noise.rz.x
noise.t_adj.y = noise.rz.y
noise.t_adj.z = noise.rz.z
noise.t_adj.loss = noise.rz.loss
if noise.s.is_noiseless():
noise.s.x = noise.rz.x
noise.s.y = noise.rz.y
noise.s.z = noise.rz.z
noise.s.loss = noise.rz.loss
if noise.s_adj.is_noiseless():
noise.s_adj.x = noise.rz.x
noise.s_adj.y = noise.rz.y
noise.s_adj.z = noise.rz.z
noise.s_adj.loss = noise.rz.loss
if noise.z.is_noiseless():
noise.z.x = noise.rz.x
noise.z.y = noise.rz.y
noise.z.z = noise.rz.z
noise.z.loss = noise.rz.loss

compiled = self.compile(qir)
module = Module.from_ir(Context(), str(compiled))
ValidateNoConditionalBranches().run(module)
Expand Down
43 changes: 42 additions & 1 deletion source/pip/qsharp/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,11 @@ class NoiseTable:
The phase flip noise to use in simulation.
"""

def is_noiseless(self) -> bool:
"""
Returns `true` if there is no noise set.
"""

class NoiseIntrinsicsTable:
def __contains__(self, name: str) -> bool:
"""
Expand Down Expand Up @@ -1017,6 +1022,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 All @@ -1035,9 +1076,9 @@ def try_create_gpu_adapter() -> str:

def run_parallel_shots(
input: List[QirInstruction],
shots: int,
qubit_count: int,
result_count: int,
shots: int,
noise: Optional[NoiseConfig],
seed: Optional[int],
) -> List[str]:
Expand Down
Loading
Loading