wai-quantum 0.5.0: provenance, sampling members, device programs, OpenCL, a simulated FPGA, f64 on accelerators, and QPU clients #253
49 changed files with 10061 additions and 37 deletions
47
.github/workflows/wai-quantum-cuda.yml
vendored
47
.github/workflows/wai-quantum-cuda.yml
vendored
|
|
@ -7,7 +7,9 @@ name: wai-quantum CUDA member
|
|||
# - cuda-hw (jetson-hub, the RTX 4050): the device tests must find exactly the
|
||||
# declared device, agree with the CPU on the arithmetic probe and on random
|
||||
# states (out-of-range runs included), reproduce all four pinned certificates
|
||||
# (two gatesets, random-v1 and fabric-v1), and catch every kernel mutant.
|
||||
# (two gatesets, random-v1 and fabric-v1), and catch every kernel mutant;
|
||||
# and the dense f64 state vector must come back with the CPU's bits on its
|
||||
# pinned corpus and at 16 and 20 qubits, every state-vector mutant caught.
|
||||
# - cuda-absent (a container with no GPU and no CUDA libraries): the same build
|
||||
# finds no member and says why, rather than failing; the device tests run over
|
||||
# no member with the declaration `none`.
|
||||
|
|
@ -30,8 +32,33 @@ on:
|
|||
- '.github/workflows/wai-quantum-cuda.yml'
|
||||
|
||||
jobs:
|
||||
# Does this pull request's own diff touch the paths below? This forge matches a
|
||||
# workflow's `paths:` against the push, so a merge of main into a branch fires the
|
||||
# workflow whatever the pull request changes; every job here waits on this answer.
|
||||
# A push to main, a schedule, or anything the script cannot determine answers
|
||||
# `true` (scripts/pr-touches-paths.py).
|
||||
relevant:
|
||||
name: does this change touch it
|
||||
runs-on: linux-arm64
|
||||
outputs:
|
||||
run: ${{ steps.gate.outputs.run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-cuda.yml \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}")
|
||||
echo "this change touches wai-quantum-cuda: $r"
|
||||
echo "run=$r" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cuda-hw:
|
||||
name: member · CUDA (jetson-hub)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux-hw
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
|
|
@ -76,9 +103,25 @@ jobs:
|
|||
n="$(grep -c "^cuda certified: key=$key .* parity=bit-exact " "$report")"
|
||||
[ "$n" = 4 ] || { echo "::error::$n of 4 certificates were earned"; exit 1; }
|
||||
grep -qx "cuda mutants: key=$key killed 6 of 6" "$report" || { echo "::error::a kernel mutant survived"; exit 1; }
|
||||
- name: The dense f64 state vector, bit for bit
|
||||
shell: bash
|
||||
working-directory: src/wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
report="$RUNNER_TEMP/cuda-sv-report.txt"; : > "$report"
|
||||
WAI_QUANTUM_CUDA_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-cuda" nice -n 10 \
|
||||
cargo test --locked --release --features quantum_fabric_cuda,quantum_sv --test cuda_sv -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/sv.log"
|
||||
grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::not every state-vector test ran and passed"; exit 1; }
|
||||
cat "$report"
|
||||
key="$WAI_QUANTUM_CUDA_EXPECT"
|
||||
grep -q "^cuda sv certified: key=$key .* parity=bit-exact cases=48 digest=b7d129d0593adf70dc164477664970c5c0b8c09f79f1a0573036bd60888dbcd1$" "$report" || { echo "::error::the state vector is not the CPU's bit for bit"; exit 1; }
|
||||
grep -qx "cuda sv mutants: key=$key killed 4 of 4" "$report" || { echo "::error::a state-vector mutant survived"; exit 1; }
|
||||
|
||||
cuda-absent:
|
||||
name: member · CUDA absent (linux)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
|
|
@ -103,3 +146,5 @@ jobs:
|
|||
WAI_QUANTUM_CUDA_REPORT="$report" cargo test --locked --features quantum_fabric_cuda --test cuda_member -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/device.log"
|
||||
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/device.log" || { echo "::error::not every device test ran and passed"; exit 1; }
|
||||
grep -qx 'cuda devices: as declared (none)' "$report" || { echo "::error::the absent case was not established"; exit 1; }
|
||||
cargo test --locked --features quantum_fabric_cuda,quantum_sv --test cuda_sv -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/sv.log"
|
||||
grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::the state-vector tests did not run over no device"; exit 1; }
|
||||
|
|
|
|||
136
.github/workflows/wai-quantum-fpga.yml
vendored
Normal file
136
.github/workflows/wai-quantum-fpga.yml
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
name: wai-quantum FPGA member
|
||||
|
||||
# The fabric's FPGA member, simulated: the gate datapath in Verilog, compiled at
|
||||
# run time by Verilator into a cycle-accurate simulation and certified
|
||||
# bit-exact against the CPU reference's pinned corpus digests. Its certificates
|
||||
# say they were earned in a simulator.
|
||||
#
|
||||
# - fpga-sim (the macOS runner, which has Verilator and Yosys): the device
|
||||
# tests must find exactly the declared simulation, agree with the CPU on the
|
||||
# arithmetic probe and on random states (out-of-range runs included),
|
||||
# reproduce all four pinned certificates, and catch every datapath mutant;
|
||||
# and the datapath must elaborate for synthesis with no latch and no check
|
||||
# failure.
|
||||
# - fpga-absent (a container with no Verilator): the same build finds no member
|
||||
# and says why, rather than failing; the device tests run over no member with
|
||||
# the declaration `none`.
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-fpga.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-fpga.yml'
|
||||
|
||||
jobs:
|
||||
# Does this pull request's own diff touch the paths below? This forge matches a
|
||||
# workflow's `paths:` against the push, so a merge of main into a branch fires the
|
||||
# workflow whatever the pull request changes; every job here waits on this answer.
|
||||
# A push to main, a schedule, or anything the script cannot determine answers
|
||||
# `true` (scripts/pr-touches-paths.py).
|
||||
relevant:
|
||||
name: does this change touch it
|
||||
runs-on: linux-arm64
|
||||
outputs:
|
||||
run: ${{ steps.gate.outputs.run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-fpga.yml \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}")
|
||||
echo "this change touches wai-quantum-fpga: $r"
|
||||
echo "run=$r" >> "$GITHUB_OUTPUT"
|
||||
|
||||
fpga-sim:
|
||||
name: member · FPGA datapath, simulated (macos)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: macos
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
WAI_QUANTUM_FPGA_EXPECT: fpga-sim-verilator
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
# launchd's PATH omits cargo and the package manager's tools on the macOS
|
||||
# host runner.
|
||||
- name: Put cargo and the simulator on PATH
|
||||
shell: bash
|
||||
run: |
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
|
||||
- name: The datapath elaborates for synthesis
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum/src/quantum_fabric
|
||||
run: |
|
||||
set -euo pipefail
|
||||
yosys -p 'read_verilog lanes64.v; chparam -set AW 6 wq_lanes64; hierarchy -top wq_lanes64; proc; flatten; opt; memory -nomap; check -assert; stat' 2>&1 | tee "$RUNNER_TEMP/yosys.log"
|
||||
! grep -q 'Latch inferred' "$RUNNER_TEMP/yosys.log" || { echo "::error::the datapath infers a latch"; exit 1; }
|
||||
grep -qE '^ +68 +\$mul$' "$RUNNER_TEMP/yosys.log" || { echo "::error::the datapath is not 17 products of four 16-bit partials"; exit 1; }
|
||||
- name: The member's unit tests
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nice -n 10 cargo test --locked --features quantum_fabric_fpga --lib -- quantum_fabric_fpga:: quantum_fabric::fpga:: 2>&1 | tee "$RUNNER_TEMP/unit.log"
|
||||
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/unit.log" || { echo "::error::no unit test ran"; exit 1; }
|
||||
- name: The member's device tests
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
report="$RUNNER_TEMP/fpga-report.txt"; : > "$report"
|
||||
export WAI_QUANTUM_FPGA_CACHE="$RUNNER_TEMP/fpga-cache"
|
||||
WAI_QUANTUM_FPGA_REPORT="$report" nice -n 10 \
|
||||
cargo test --locked --release --features quantum_fabric_fpga --test fpga_member -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/device.log"
|
||||
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/device.log" || { echo "::error::not every device test ran and passed"; exit 1; }
|
||||
cat "$report"
|
||||
key="$WAI_QUANTUM_FPGA_EXPECT"
|
||||
grep -qx "fpga devices: as declared ($key)" "$report" || { echo "::error::the simulation found is not the declared one"; exit 1; }
|
||||
n="$(grep -c "^fpga certified: key=$key .* parity=bit-exact provenance=simulator:verilator " "$report")"
|
||||
[ "$n" = 4 ] || { echo "::error::$n of 4 certificates were earned"; exit 1; }
|
||||
grep -qx "fpga mutants: key=$key killed 6 of 6" "$report" || { echo "::error::a datapath mutant survived"; exit 1; }
|
||||
|
||||
fpga-absent:
|
||||
name: member · FPGA simulation absent (linux)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
WAI_QUANTUM_FPGA_EXPECT: none
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust 1.98.0
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
command -v rustup >/dev/null 2>&1 || \
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
|
||||
rustup toolchain install 1.98.0 --profile minimal
|
||||
rustup default 1.98.0
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: No simulator here, and the member says so
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
! command -v verilator >/dev/null || { echo "::error::this container has Verilator; the absent case needs one without"; exit 1; }
|
||||
report="$RUNNER_TEMP/fpga-report.txt"; : > "$report"
|
||||
WAI_QUANTUM_FPGA_REPORT="$report" cargo test --locked --features quantum_fabric_fpga --test fpga_member -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/device.log"
|
||||
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/device.log" || { echo "::error::not every device test ran and passed"; exit 1; }
|
||||
grep -qx 'fpga devices: as declared (none)' "$report" || { echo "::error::the absent case was not established"; exit 1; }
|
||||
209
.github/workflows/wai-quantum-opencl.yml
vendored
Normal file
209
.github/workflows/wai-quantum-opencl.yml
vendored
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
name: wai-quantum OpenCL member
|
||||
|
||||
# The fabric's OpenCL member: the gate kernel in OpenCL C, built at run time by
|
||||
# each device's own compiler and certified bit-exact against the CPU
|
||||
# reference's pinned corpus digests.
|
||||
#
|
||||
# - opencl-hw (jetson-hub): the platforms the host declares (by default the
|
||||
# discrete GPU's and the CPU's). The device
|
||||
# tests must find exactly the declared devices, agree with the CPU on the
|
||||
# arithmetic probe and on random states (out-of-range runs included),
|
||||
# reproduce all four pinned certificates on each device (two gatesets,
|
||||
# random-v1 and fabric-v1), and catch every kernel mutant on each; and the
|
||||
# dense f64 state vector must come back with the CPU's bits on each.
|
||||
# - opencl-apple (the macOS runner): the same on the platform's GPU, through its
|
||||
# OpenCL 1.2; that GPU has no binary64, so the state vector is refused there,
|
||||
# with the reason.
|
||||
# - opencl-absent (a container with no OpenCL): the same build finds no member
|
||||
# and says why, rather than failing; the device tests run over no member with
|
||||
# the declaration `none`.
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-opencl.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-opencl.yml'
|
||||
|
||||
jobs:
|
||||
# Does this pull request's own diff touch the paths below? This forge matches a
|
||||
# workflow's `paths:` against the push, so a merge of main into a branch fires the
|
||||
# workflow whatever the pull request changes; every job here waits on this answer.
|
||||
# A push to main, a schedule, or anything the script cannot determine answers
|
||||
# `true` (scripts/pr-touches-paths.py).
|
||||
relevant:
|
||||
name: does this change touch it
|
||||
runs-on: linux-arm64
|
||||
outputs:
|
||||
run: ${{ steps.gate.outputs.run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-opencl.yml \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}")
|
||||
echo "this change touches wai-quantum-opencl: $r"
|
||||
echo "run=$r" >> "$GITHUB_OUTPUT"
|
||||
|
||||
opencl-hw:
|
||||
name: member · OpenCL GPU and CPU (jetson-hub)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux-hw
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
# The devices are declared by the host, in its runner's environment
|
||||
# (WAI_QUANTUM_HW_OPENCL_EXPECT); with no declaration, the CPU and the
|
||||
# discrete GPU.
|
||||
CARGO_PROFILE_DEV_DEBUG: '0'
|
||||
CARGO_BUILD_JOBS: '8'
|
||||
steps:
|
||||
- name: Fetch the source
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
want="${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
rm -rf src && mkdir src && cd src
|
||||
git init -q
|
||||
# Fetch the commit itself, not the ref: a later commit on the ref that this
|
||||
# workflow's paths do not match would otherwise be fetched instead.
|
||||
git fetch -q --depth 1 http://100.64.224.0:3400/Transaction-Science/open-standards.git "$want"
|
||||
git checkout -q FETCH_HEAD
|
||||
got="$(git rev-parse HEAD)"
|
||||
[ "$got" = "$want" ] || { echo "::error::fetched $got, not $want"; exit 1; }
|
||||
- name: The member's unit tests
|
||||
shell: bash
|
||||
working-directory: src/wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-opencl" nice -n 10 cargo test --locked --features quantum_fabric_opencl --lib -- quantum_fabric_opencl:: quantum_fabric::opencl:: 2>&1 | tee "$RUNNER_TEMP/unit.log"
|
||||
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/unit.log" || { echo "::error::no unit test ran"; exit 1; }
|
||||
- name: The member's device tests
|
||||
shell: bash
|
||||
working-directory: src/wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
# The CPU platform's runtime loads its threading and memory libraries
|
||||
# from the toolkit's own directories; without them the ICD loader
|
||||
# skips the platform silently.
|
||||
oneapi=/opt/intel/oneapi
|
||||
export LD_LIBRARY_PATH="$oneapi/compiler/latest/lib:$oneapi/tbb/latest/lib/intel64/gcc4.8:$oneapi/tbb/latest/lib:$oneapi/umf/latest/lib:$oneapi/tcm/latest/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export WAI_QUANTUM_OPENCL_EXPECT="${WAI_QUANTUM_HW_OPENCL_EXPECT:-opencl-cpu-intel,opencl-gpu-nvidia}"
|
||||
echo "devices declared: $WAI_QUANTUM_OPENCL_EXPECT"
|
||||
report="$RUNNER_TEMP/opencl-report.txt"; : > "$report"
|
||||
WAI_QUANTUM_OPENCL_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-opencl" nice -n 10 \
|
||||
cargo test --locked --release --features quantum_fabric_opencl --test opencl_member -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/device.log"
|
||||
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/device.log" || { echo "::error::not every device test ran and passed"; exit 1; }
|
||||
cat "$report"
|
||||
grep -qx "opencl devices: as declared ($WAI_QUANTUM_OPENCL_EXPECT)" "$report" || { echo "::error::the devices found are not the declared ones"; exit 1; }
|
||||
for key in ${WAI_QUANTUM_OPENCL_EXPECT//,/ }; do
|
||||
n="$(grep -c "^opencl certified: key=$key .* parity=bit-exact " "$report")"
|
||||
[ "$n" = 4 ] || { echo "::error::$key: $n of 4 certificates were earned"; exit 1; }
|
||||
grep -qx "opencl mutants: key=$key killed 6 of 6" "$report" || { echo "::error::$key: a kernel mutant survived"; exit 1; }
|
||||
done
|
||||
- name: The dense f64 state vector, bit for bit
|
||||
shell: bash
|
||||
working-directory: src/wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
oneapi=/opt/intel/oneapi
|
||||
export LD_LIBRARY_PATH="$oneapi/compiler/latest/lib:$oneapi/tbb/latest/lib/intel64/gcc4.8:$oneapi/tbb/latest/lib:$oneapi/umf/latest/lib:$oneapi/tcm/latest/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export WAI_QUANTUM_OPENCL_EXPECT="${WAI_QUANTUM_HW_OPENCL_EXPECT:-opencl-cpu-intel,opencl-gpu-nvidia}"
|
||||
report="$RUNNER_TEMP/opencl-sv-report.txt"; : > "$report"
|
||||
WAI_QUANTUM_OPENCL_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-opencl" nice -n 10 \
|
||||
cargo test --locked --release --features quantum_fabric_opencl,quantum_sv --test opencl_sv -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/sv.log"
|
||||
grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::not every state-vector test ran and passed"; exit 1; }
|
||||
cat "$report"
|
||||
for key in ${WAI_QUANTUM_OPENCL_EXPECT//,/ }; do
|
||||
if grep -qx "opencl sv refused: key=$key the device has no binary64" "$report"; then continue; fi
|
||||
grep -q "^opencl sv certified: key=$key .* parity=bit-exact cases=48 digest=b7d129d0593adf70dc164477664970c5c0b8c09f79f1a0573036bd60888dbcd1$" "$report" || { echo "::error::$key: the state vector is not the CPU's bit for bit"; exit 1; }
|
||||
grep -qx "opencl sv mutants: key=$key killed 4 of 4" "$report" || { echo "::error::$key: a state-vector mutant survived"; exit 1; }
|
||||
done
|
||||
|
||||
opencl-apple:
|
||||
name: member · OpenCL GPU (macos)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: macos
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
WAI_QUANTUM_OPENCL_EXPECT: opencl-gpu-apple
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
# launchd's PATH omits cargo on the macOS host runner.
|
||||
- name: Put cargo on PATH
|
||||
shell: bash
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: The member's device tests
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
report="$RUNNER_TEMP/opencl-report.txt"; : > "$report"
|
||||
WAI_QUANTUM_OPENCL_REPORT="$report" nice -n 10 \
|
||||
cargo test --locked --release --features quantum_fabric_opencl --test opencl_member -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/device.log"
|
||||
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/device.log" || { echo "::error::not every device test ran and passed"; exit 1; }
|
||||
cat "$report"
|
||||
key="$WAI_QUANTUM_OPENCL_EXPECT"
|
||||
grep -qx "opencl devices: as declared ($key)" "$report" || { echo "::error::the devices found are not the declared ones"; exit 1; }
|
||||
n="$(grep -c "^opencl certified: key=$key .* parity=bit-exact " "$report")"
|
||||
[ "$n" = 4 ] || { echo "::error::$n of 4 certificates were earned"; exit 1; }
|
||||
grep -qx "opencl mutants: key=$key killed 6 of 6" "$report" || { echo "::error::a kernel mutant survived"; exit 1; }
|
||||
- name: The state vector refuses a device without binary64, and says so
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
report="$RUNNER_TEMP/opencl-sv-report.txt"; : > "$report"
|
||||
WAI_QUANTUM_OPENCL_REPORT="$report" nice -n 10 \
|
||||
cargo test --locked --release --features quantum_fabric_opencl,quantum_sv --test opencl_sv -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/sv.log"
|
||||
grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::not every state-vector test ran and passed"; exit 1; }
|
||||
grep -qx "opencl sv refused: key=$WAI_QUANTUM_OPENCL_EXPECT the device has no binary64" "$report" || { echo "::error::the refusal was not established"; exit 1; }
|
||||
|
||||
opencl-absent:
|
||||
name: member · OpenCL absent (linux)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
WAI_QUANTUM_OPENCL_EXPECT: none
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust 1.98.0
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
command -v rustup >/dev/null 2>&1 || \
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
|
||||
rustup toolchain install 1.98.0 --profile minimal
|
||||
rustup default 1.98.0
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: No OpenCL here, and the member says so
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
report="$RUNNER_TEMP/opencl-report.txt"; : > "$report"
|
||||
WAI_QUANTUM_OPENCL_REPORT="$report" cargo test --locked --features quantum_fabric_opencl --test opencl_member -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/device.log"
|
||||
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/device.log" || { echo "::error::not every device test ran and passed"; exit 1; }
|
||||
grep -qx 'opencl devices: as declared (none)' "$report" || { echo "::error::the absent case was not established"; exit 1; }
|
||||
cargo test --locked --features quantum_fabric_opencl,quantum_sv --test opencl_sv -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/sv.log"
|
||||
grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::the state-vector tests did not run over no device"; exit 1; }
|
||||
82
.github/workflows/wai-quantum-qpu.yml
vendored
Normal file
82
.github/workflows/wai-quantum-qpu.yml
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
name: wai-quantum-qpu
|
||||
|
||||
# The clients for quantum processing services: every client against a service
|
||||
# that follows its provider's published contract (the fake parses the program
|
||||
# in the provider's format, runs it on wai-quantum's simulator, and answers in
|
||||
# the provider's result shape and bit order), certification through the
|
||||
# fabric, the probe that catches a service with another bit order, and curl
|
||||
# end to end against a local server. No request leaves the runner.
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'wai/wai-quantum-qpu/**'
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-qpu.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'wai/wai-quantum-qpu/**'
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-qpu.yml'
|
||||
|
||||
jobs:
|
||||
# Does this pull request's own diff touch the paths below? This forge matches a
|
||||
# workflow's `paths:` against the push, so a merge of main into a branch fires the
|
||||
# workflow whatever the pull request changes; every job here waits on this answer.
|
||||
# A push to main, a schedule, or anything the script cannot determine answers
|
||||
# `true` (scripts/pr-touches-paths.py).
|
||||
relevant:
|
||||
name: does this change touch it
|
||||
runs-on: linux-arm64
|
||||
outputs:
|
||||
run: ${{ steps.gate.outputs.run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-qpu.yml \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}")
|
||||
echo "this change touches wai-quantum-qpu: $r"
|
||||
echo "run=$r" >> "$GITHUB_OUTPUT"
|
||||
|
||||
clients:
|
||||
name: clients against their contracts (linux)
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust 1.98.0
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
command -v rustup >/dev/null 2>&1 || \
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
|
||||
rustup toolchain install 1.98.0 --profile minimal --component clippy
|
||||
rustup default 1.98.0
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Tests
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum-qpu
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v curl >/dev/null || { echo "::error::no curl: the transport test would pass over nothing"; exit 1; }
|
||||
cargo test --locked 2>&1 | tee "$RUNNER_TEMP/test.log"
|
||||
grep -qE '^test result: ok\. 8 passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::not every client test ran and passed"; exit 1; }
|
||||
grep -q '^test curl_carries_a_request_to_a_server_and_back \.\.\. ok$' "$RUNNER_TEMP/test.log" || { echo "::error::the transport test did not run"; exit 1; }
|
||||
- name: Clippy
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum-qpu
|
||||
run: cargo clippy --locked --all-targets --no-deps -- -D warnings
|
||||
15
.github/workflows/wai-quantum-webgpu.yml
vendored
15
.github/workflows/wai-quantum-webgpu.yml
vendored
|
|
@ -277,11 +277,10 @@ jobs:
|
|||
runs-on: linux-hw
|
||||
timeout-minutes: 150
|
||||
env:
|
||||
# Set from the key list the first run prints; add any other ICD the host
|
||||
# exposes (an integrated GPU, a software driver) as it reports them.
|
||||
# The discrete GPU, and the software Vulkan driver the host also has. The wide
|
||||
# sizes run on the GPU only.
|
||||
WAI_QUANTUM_WEBGPU_EXPECT: gpu-vulkan-10de,swgpu-vulkan-10005
|
||||
# The adapters are declared by the host, in its runner's environment
|
||||
# (WAI_QUANTUM_HW_WEBGPU_EXPECT), so a GPU and its declaration change in one
|
||||
# step there; with no declaration, the discrete GPU and the software Vulkan
|
||||
# driver. The wide sizes run on the GPU only.
|
||||
WAI_QUANTUM_WEBGPU_WIDE: '1'
|
||||
CARGO_PROFILE_DEV_DEBUG: '0'
|
||||
CARGO_BUILD_JOBS: '8'
|
||||
|
|
@ -302,7 +301,11 @@ jobs:
|
|||
- name: The member's tests
|
||||
shell: bash
|
||||
working-directory: src
|
||||
run: CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum" nice -n 10 scripts/wai-quantum-webgpu-member.sh
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export WAI_QUANTUM_WEBGPU_EXPECT="${WAI_QUANTUM_HW_WEBGPU_EXPECT:-gpu-vulkan-10de,swgpu-vulkan-10005}"
|
||||
echo "adapters declared: $WAI_QUANTUM_WEBGPU_EXPECT"
|
||||
CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum" nice -n 10 scripts/wai-quantum-webgpu-member.sh
|
||||
- name: The conformance corpus through the member
|
||||
shell: bash
|
||||
working-directory: src/wai/wai-rs
|
||||
|
|
|
|||
|
|
@ -6,6 +6,69 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### wai-quantum 0.5.0: provenance, sampling members, device programs, and three more members
|
||||
|
||||
Breaking: `Certificate` gains a public field, `provenance`, and `FabricClass` is now
|
||||
`#[non_exhaustive]`, so a class can be added in a minor release. A silicon certificate
|
||||
hashes exactly as before; every pinned digest is unchanged. The `quantum_fabric` feature now
|
||||
includes `quantum_toolchain`.
|
||||
|
||||
- **Provenance in the certificate.** `Provenance::{Silicon, Emulator(name), Simulator(name)}`,
|
||||
inside the certificate's hash unless it is silicon. Placement ranks silicon first, then
|
||||
emulators, then simulators, so a simulated member never stands in for hardware that is
|
||||
present.
|
||||
- **Sampling members.** The kernels `wai.quantum.circuit/sample` and
|
||||
`wai.quantum.circuit2/sample`, the `Sampler` trait, `certify_sampler` (classical fidelity
|
||||
against the exact distribution, over stated shots) and the `qpu-v1` corpus. `VirtualQpu`
|
||||
samples a stated noise model (global depolarizing and readout flips) and says it is a
|
||||
simulator. `VirtualDevice` adds a coupling map and a native basis: each circuit is
|
||||
compiled, the program verified, and the gate errors charged for the program it would
|
||||
run, routing included.
|
||||
- **Programs for devices** (`quantum_toolchain`). `device::device_qasm` writes OpenQASM 2
|
||||
or 3 in the dialect's standard gates only, with every multi-controlled gate lowered
|
||||
exactly (dyadic angles throughout; refused, never rounded, past the table) and every
|
||||
qubit measured into `c[i]`; `counts_from_bitstrings` and `counts_from_integers` read a
|
||||
service's counts back. `native::compile` targets a processor's own basis (`rz`/`sx`/`x`/`cz`,
|
||||
`prx`/`cz`, or `gpi`/`gpi2`/`zz` in turns) on its own qubits, routed over its coupling
|
||||
map; `NativeProgram::verify` checks a program against its circuit by simulation before
|
||||
it is sent. `from_qasm` now reads `ch`.
|
||||
- **The OpenCL member** (`quantum_fabric_opencl`, opt-in): the exact 64-bit kernel in
|
||||
OpenCL C, built by each device's own compiler; the ICD loader is opened when first used.
|
||||
Certified bit-exact on GPUs and CPUs of several vendors; an OpenCL accelerator is refused,
|
||||
since no fabric class names it.
|
||||
- **The FPGA member, simulated** (`quantum_fabric_fpga`, opt-in): the datapath in Verilog,
|
||||
each 32×32 product from four 16-bit partial products, compiled at run time by Verilator
|
||||
into a cycle-accurate simulation. Its certificates say they were earned in a simulator.
|
||||
- **The dense f64 state vector on accelerators** (`quantum_fabric::sv64`, with
|
||||
`quantum_sv`): the CPU's own launches (diagonal with the exact-one skip, the one-qubit
|
||||
formula, and ordered row sums), each an IEEE binary64 operation with no fused
|
||||
multiply-add, on the CUDA member (`CudaMember::sv`) and on OpenCL devices with binary64
|
||||
(`OpenClMember::sv`). Certified against the pinned corpus digest `SV_DEFAULT_PIN`; a
|
||||
device without binary64 refuses, saying so.
|
||||
- **wai-quantum-qpu 0.1.0** (new crate): clients for three quantum processing protocols
|
||||
(`sampler_v2`, `prx_circuits`, `native_turns`), each compiling exactly to the device,
|
||||
verifying before submission and reading counts back onto the circuit's qubits;
|
||||
`QpuSampler` makes a service a fabric member; the `wai-qpu` command reads a device,
|
||||
runs the bit-order probe, and certifies. `curl` is the transport, with secrets kept off
|
||||
command lines. Tested against services that follow each protocol's published contract.
|
||||
- **CI.** Workflows for the OpenCL member, the simulated FPGA member and the QPU clients;
|
||||
the CUDA and OpenCL hardware jobs certify the f64 state vector; the hardware host
|
||||
declares its devices in its runner's environment.
|
||||
|
||||
### wai-quantum 0.4.4: the fabric's CUDA member
|
||||
|
||||
Additive. The default features, `full`, and every pinned digest are unchanged.
|
||||
|
||||
- **The CUDA member** (`quantum_fabric_cuda`, opt-in, native targets only): the gate
|
||||
kernel in CUDA C with exact 64-bit integers, compiled at run time by the runtime
|
||||
compiler for each device's own architecture (`--fmad=false`), one launch per gate. The
|
||||
driver and the compiler are loaded when first used, so a build with the feature runs on
|
||||
a host without CUDA and finds no member there, saying why. Each device is probed against
|
||||
the CPU's arithmetic and certified bit-exact by reproducing the pinned corpus digests; a
|
||||
result that would leave 32 bits is refused, never returned.
|
||||
- **CI.** `wai-quantum-cuda.yml`: the device tests on a hardware host's GPU (every
|
||||
certificate, every kernel mutant caught), and the same build on a host with no CUDA.
|
||||
|
||||
### Reader limits for the extension formats' durations
|
||||
|
||||
A score, haptic signal, audio scene, splat4d clip or film declares its length in its
|
||||
|
|
|
|||
2
wai/tools/quantum-meter/Cargo.lock
generated
2
wai/tools/quantum-meter/Cargo.lock
generated
|
|
@ -1017,7 +1017,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
|
|
@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-pack/Cargo.lock
generated
2
wai/wai-pack/Cargo.lock
generated
|
|
@ -703,7 +703,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
457
wai/wai-quantum-qpu/Cargo.lock
generated
Normal file
457
wai/wai-quantum-qpu/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"cpufeatures 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wai-quantum-qpu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"serde_json",
|
||||
"wai-quantum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
15
wai/wai-quantum-qpu/Cargo.toml
Normal file
15
wai/wai-quantum-qpu/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "wai-quantum-qpu"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "Apache-2.0"
|
||||
description = "Quantum processors as members of the wai-quantum fabric: clients for the services that run circuits on them. Each circuit is compiled exactly to the processor's own gates and connectivity, checked against the circuit before it is sent, and its counts are read back onto the circuit's qubits and certified statistically against the exact distribution."
|
||||
repository = "https://git.transaction.science/Transaction-Science/open-standards"
|
||||
keywords = ["quantum", "qpu", "openqasm", "verification", "receipts"]
|
||||
categories = ["science", "api-bindings"]
|
||||
|
||||
[dependencies]
|
||||
wai-quantum = { version = "0.5.0", path = "../wai-quantum", default-features = false, features = ["quantum_fabric"] }
|
||||
serde_json = "1"
|
||||
blake3 = "1"
|
||||
65
wai/wai-quantum-qpu/README.md
Normal file
65
wai/wai-quantum-qpu/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# wai-quantum-qpu
|
||||
|
||||
Quantum processors as members of the [wai-quantum](../wai-quantum) fabric: clients for the services that run circuits on them.
|
||||
|
||||
For every circuit, a `QpuSampler`:
|
||||
|
||||
1. reads the device's native gates and connectivity from the service;
|
||||
2. compiles the circuit to them exactly, with `wai_quantum::quantum_toolchain::native`, and checks the program against the circuit by simulation before anything is sent;
|
||||
3. submits it, waits for the job, and reads the counts back onto the circuit's qubits: logical qubit `i` is measured into classical bit `i`, which is bit `i` of the counts' index, whatever physical qubit it ended on.
|
||||
|
||||
`QpuSampler::member` certifies the device statistically against the exact distribution of every circuit of a corpus, as the fabric certifies any sampler. Its certificates carry the device's provenance: silicon for a processor, a simulator for a service's simulator. Placement ranks a simulator after silicon.
|
||||
|
||||
## Services
|
||||
|
||||
| Module | Protocol | Native gates | Angles on the wire |
|
||||
|---|---|---|---|
|
||||
| `sampler_v2` | Sampler V2 jobs: an IAM bearer token, one OpenQASM 3 circuit per job | `rz`, `sx`, `x`, `cz` on physical qubits `$p` | radians |
|
||||
| `prx_circuits` | circuit jobs of native instructions on named qubits, one measurement key per classical bit | `prx`, `cz` | radians |
|
||||
| `native_turns` | circuit jobs in a native gateset that runs as written; a simulator with device noise models | `gpi`, `gpi2`, `zz` | turns, so every dyadic angle is exact |
|
||||
|
||||
Each module's `BASE` constant is the endpoint it is written against. Each client is configured from the environment (`from_env`) or in code. Keys and tokens are held in `Secret`, which never prints:
|
||||
|
||||
| Module | Variables |
|
||||
|---|---|
|
||||
| `sampler_v2` | `WAI_QPU_SAMPLER_API_KEY`, `WAI_QPU_SAMPLER_CRN`, `WAI_QPU_SAMPLER_BACKEND` |
|
||||
| `prx_circuits` | `WAI_QPU_PRX_TOKEN`, `WAI_QPU_PRX_QC` |
|
||||
| `native_turns` | `WAI_QPU_TURNS_KEY`, `WAI_QPU_TURNS_BACKEND` (default `simulator`), `WAI_QPU_TURNS_NOISE` |
|
||||
|
||||
```rust
|
||||
use wai_quantum_qpu::{QpuSampler, check_probe, probe_circuit, transport::Curl};
|
||||
use wai_quantum_qpu::native_turns::NativeTurns;
|
||||
|
||||
let sampler = QpuSampler::new(NativeTurns::from_env(Curl::new(120))?)?;
|
||||
// The first job on any service: an X on qubit 0, whose counts must land on index 1.
|
||||
let (counts, job) = sampler.run(&probe_circuit(3), 1000)?;
|
||||
check_probe(&counts, 0.95)?;
|
||||
```
|
||||
|
||||
## The command
|
||||
|
||||
```text
|
||||
wai-qpu device <service> # the device: qubits, basis, provenance
|
||||
wai-qpu probe <service> [--qubits N] [--shots N] # the bit-order probe
|
||||
wai-qpu certify <service> [--shots N] [--cases N] [--report FILE] [--yes]
|
||||
```
|
||||
|
||||
`<service>` is `sampler-v2`, `prx-circuits` or `native-turns`, configured from its variables above. A certification runs every circuit of the corpus in both gatesets as a job. On a device whose provenance is silicon that spends the service's quota, so it runs only with `--yes`. `--report` writes each certificate with its hash, and every job, as JSON.
|
||||
|
||||
## Transport
|
||||
|
||||
`transport::Curl` runs `curl` as a program, so the crate carries no TLS stack. Headers, keys and tokens included, go in a curl configuration file that only this user can read, removed when the request ends. The body goes on curl's standard input. Nothing secret appears on a command line. `transport::Scripted` answers from a function, for tests.
|
||||
|
||||
## What is checked here, and what only a service can check
|
||||
|
||||
The tests run each client against a service that follows that provider's published contract. The fake parses the submitted program in the provider's own format, runs it on the crate's simulator, and answers in the provider's result shape and bit order. A client whose request, units or bit order departs from the contract reads back the wrong distribution, and the tests fail. A probe test shows that a service using the other bit order is caught.
|
||||
|
||||
A real service settles its own conventions. The first job on each should be `probe_circuit`, checked with `check_probe`, before any other result is trusted.
|
||||
|
||||
The work runs at the service, so every run is unmetered here. An energy figure the service reports, such as a per-job estimate, is kept in the job record as the service's figure (`JobRecord::service_kwh`). It is never recorded as a measurement.
|
||||
|
||||
Every circuit a certification runs is a job on the service, and spends that service's quota.
|
||||
|
||||
## Licence
|
||||
|
||||
Apache-2.0.
|
||||
129
wai/wai-quantum-qpu/src/bin/wai-qpu.rs
Normal file
129
wai/wai-quantum-qpu/src/bin/wai-qpu.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
//! `wai-qpu`: read a quantum processing service's device, run the bit-order
|
||||
//! probe on it, or certify it through the wai-quantum fabric.
|
||||
//!
|
||||
//! ```text
|
||||
//! wai-qpu device <service>
|
||||
//! wai-qpu probe <service> [--qubits N] [--shots N]
|
||||
//! wai-qpu certify <service> [--shots N] [--cases N] [--report FILE] [--yes]
|
||||
//! ```
|
||||
//!
|
||||
//! `<service>` is `sampler-v2`, `prx-circuits` or `native-turns`, each
|
||||
//! configured from its environment variables (see the crate's README). A
|
||||
//! certification runs every circuit of the corpus in both gatesets as a job; on
|
||||
//! a device whose provenance is silicon it spends the service's quota, so it
|
||||
//! asks for `--yes`.
|
||||
|
||||
use std::process::ExitCode;
|
||||
use wai_quantum::quantum_fabric::Provenance;
|
||||
use wai_quantum::quantum_fabric::sampling::{QPU_DEFAULT, QpuCorpus};
|
||||
use wai_quantum_qpu::native_turns::NativeTurns;
|
||||
use wai_quantum_qpu::prx_circuits::PrxCircuits;
|
||||
use wai_quantum_qpu::sampler_v2::SamplerV2;
|
||||
use wai_quantum_qpu::transport::Curl;
|
||||
use wai_quantum_qpu::{Provider, QpuError, QpuSampler, basis_label, check_probe, probe_circuit, report};
|
||||
|
||||
const USAGE: &str = "usage: wai-qpu device|probe|certify <sampler-v2|prx-circuits|native-turns> [--qubits N] [--shots N] [--cases N] [--report FILE] [--yes]";
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Args {
|
||||
command: String,
|
||||
service: String,
|
||||
qubits: u8,
|
||||
shots: u64,
|
||||
cases: Option<usize>,
|
||||
report: Option<String>,
|
||||
yes: bool,
|
||||
}
|
||||
|
||||
fn parse(mut it: impl Iterator<Item = String>) -> Result<Args, String> {
|
||||
let command = it.next().ok_or(USAGE)?;
|
||||
let service = it.next().ok_or(USAGE)?;
|
||||
let mut a = Args { command, service, qubits: 3, shots: 1000, cases: None, report: None, yes: false };
|
||||
while let Some(flag) = it.next() {
|
||||
let mut value = || it.next().ok_or_else(|| format!("{flag} needs a value"));
|
||||
match flag.as_str() {
|
||||
"--qubits" => a.qubits = value()?.parse().map_err(|_| "--qubits takes a number")?,
|
||||
"--shots" => a.shots = value()?.parse().map_err(|_| "--shots takes a number")?,
|
||||
"--cases" => a.cases = Some(value()?.parse().map_err(|_| "--cases takes a number")?),
|
||||
"--report" => a.report = Some(value()?),
|
||||
"--yes" => a.yes = true,
|
||||
other => return Err(format!("unknown flag {other}\n{USAGE}")),
|
||||
}
|
||||
}
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
fn provider(service: &str) -> Result<Box<dyn Provider>, QpuError> {
|
||||
let curl = Curl::new(120);
|
||||
Ok(match service {
|
||||
"sampler-v2" => Box::new(SamplerV2::from_env(curl)?),
|
||||
"prx-circuits" => Box::new(PrxCircuits::from_env(curl)?),
|
||||
"native-turns" => Box::new(NativeTurns::from_env(curl)?),
|
||||
other => return Err(QpuError::Config(format!("no service {other:?}: sampler-v2, prx-circuits or native-turns"))),
|
||||
})
|
||||
}
|
||||
|
||||
fn run(a: Args) -> Result<(), String> {
|
||||
let sampler = QpuSampler::new(provider(&a.service).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
let d = sampler.device();
|
||||
println!("device: {} · {} qubits · basis {} · {}", d.name, d.coupling.qubits(), basis_label(d.basis), d.provenance.label());
|
||||
match a.command.as_str() {
|
||||
"device" => Ok(()),
|
||||
"probe" => {
|
||||
let (counts, job) = sampler.run(&probe_circuit(a.qubits), a.shots).map_err(|e| e.to_string())?;
|
||||
println!("job {}: {} shots, {} on index 1", job.id, job.shots, counts.get(1).copied().unwrap_or(0));
|
||||
check_probe(&counts, 0.8)?;
|
||||
println!("probe: the service's bit order is the one the client reads");
|
||||
Ok(())
|
||||
}
|
||||
"certify" => {
|
||||
let corpus = QpuCorpus { cases: a.cases.unwrap_or(QPU_DEFAULT.cases), ..QPU_DEFAULT };
|
||||
let jobs = 2 * corpus.cases;
|
||||
if d.provenance == Provenance::Silicon && !a.yes {
|
||||
return Err(format!("certifying runs {jobs} jobs of {} shots on a processor and spends the service's quota; add --yes to run them", a.shots));
|
||||
}
|
||||
println!("certifying: {jobs} jobs of {} shots, corpus {}", a.shots, corpus.generator());
|
||||
let member = sampler.member(corpus, a.shots).map_err(|e| e.to_string())?;
|
||||
let r = report(&sampler, &member);
|
||||
for c in r["certificates"].as_array().into_iter().flatten() {
|
||||
println!("{} · {} · {} · certificate {}", c["kernel"].as_str().unwrap_or(""), c["parity"], c["provenance"].as_str().unwrap_or(""), c["hash"].as_str().unwrap_or(""));
|
||||
}
|
||||
if let Some(path) = a.report {
|
||||
std::fs::write(&path, serde_json::to_string_pretty(&r).unwrap_or_default()).map_err(|e| format!("{path}: {e}"))?;
|
||||
println!("report: {path}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
other => Err(format!("no command {other:?}\n{USAGE}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match parse(std::env::args().skip(1)).and_then(run) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("wai-qpu: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn args(s: &str) -> Result<Args, String> {
|
||||
parse(s.split_whitespace().map(String::from))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arguments_parse_and_bad_ones_are_named() {
|
||||
let a = args("certify native-turns --shots 2000 --cases 4 --report r.json --yes").unwrap();
|
||||
assert_eq!((a.command.as_str(), a.service.as_str(), a.shots, a.cases, a.report.as_deref(), a.yes), ("certify", "native-turns", 2000, Some(4), Some("r.json"), true));
|
||||
assert_eq!(args("probe sampler-v2").unwrap().qubits, 3);
|
||||
assert!(args("probe").is_err());
|
||||
assert!(args("probe x --shots").unwrap_err().contains("needs a value"));
|
||||
assert!(args("probe x --bogus").unwrap_err().contains("unknown flag"));
|
||||
assert!(provider("elsewhere").is_err());
|
||||
}
|
||||
}
|
||||
532
wai/wai-quantum-qpu/src/lib.rs
Normal file
532
wai/wai-quantum-qpu/src/lib.rs
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
//! Quantum processors as members of the wai-quantum fabric.
|
||||
//!
|
||||
//! A [`Provider`] is a client for one service that runs circuits on a quantum
|
||||
//! processor ([`sampler_v2`], [`prx_circuits`], [`native_turns`]). A [`QpuSampler`] over it is a
|
||||
//! fabric [`Sampler`]: for each circuit it
|
||||
//!
|
||||
//! 1. reads the device's native gates and connectivity from the service
|
||||
//! ([`Provider::device`]);
|
||||
//! 2. compiles the circuit to them exactly ([`native::compile`]) and checks the
|
||||
//! program against the circuit by simulation before anything is sent
|
||||
//! ([`NativeProgram::verify`]);
|
||||
//! 3. submits it, waits for the job, and reads the counts back onto the
|
||||
//! circuit's qubits: logical qubit `i` is measured into classical bit `i`,
|
||||
//! which is bit `i` of the counts' index, whatever physical qubit it ended
|
||||
//! on.
|
||||
//!
|
||||
//! [`QpuSampler::member`] certifies the device statistically against the exact
|
||||
//! distribution of every circuit of a corpus, as the fabric certifies any
|
||||
//! sampler, with the provenance the device has: silicon for a processor, a
|
||||
//! simulator for a service's simulator.
|
||||
//!
|
||||
//! # What is not claimed
|
||||
//!
|
||||
//! - **Bit order.** Each client maps the service's result keys to classical
|
||||
//! bits by the service's published convention. The first job on each service
|
||||
//! should be [`probe_circuit`] (an `X` on qubit 0 only), whose counts must
|
||||
//! all land on index 1; [`check_probe`] says whether they did.
|
||||
//! - **Energy.** The work runs at the service, so a run on this host's meter
|
||||
//! would read only the wait: every run is unmetered. A figure the service
|
||||
//! reports is kept in the job record as the service's ([`JobRecord::service_kwh`]),
|
||||
//! never as a measurement.
|
||||
//! - **No network here** without a [`transport::Transport`]: [`transport::Curl`]
|
||||
//! runs `curl` as a program; tests use [`transport::Scripted`].
|
||||
|
||||
pub mod sampler_v2;
|
||||
pub mod native_turns;
|
||||
pub mod prx_circuits;
|
||||
pub mod transport;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::time::Duration;
|
||||
use wai_quantum::quantum::{Circuit, Gateset, circuit_hash};
|
||||
use wai_quantum::quantum_fabric::sampling::{Counts, QpuCorpus, SampleFuture, Sampler, certify_sampler, check_counts, sample_kernel};
|
||||
use wai_quantum::quantum_fabric::{FabricClass, KernelEntry, Member, MemberError, Presence, Provenance, RunMeter, ready};
|
||||
use wai_quantum::quantum_toolchain::native::{self, Basis, Coupling, NativeError, NativeProgram};
|
||||
|
||||
/// A key or token: kept out of `Debug` output.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Secret(String);
|
||||
|
||||
impl Secret {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Secret(value.into())
|
||||
}
|
||||
|
||||
/// From the environment variable `name`, or why not.
|
||||
pub fn from_env(name: &str) -> Result<Self, QpuError> {
|
||||
match std::env::var(name) {
|
||||
Ok(v) if !v.trim().is_empty() => Ok(Secret(v.trim().to_string())),
|
||||
_ => Err(QpuError::Config(format!("{name} is not set"))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expose(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Secret {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Secret(…)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a request, a job or a result failed.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum QpuError {
|
||||
/// A key, an endpoint or a setting is missing or malformed.
|
||||
Config(String),
|
||||
/// The request did not complete.
|
||||
Transport(String),
|
||||
/// The service answered with an error status; the start of its body.
|
||||
Http { status: u16, body: String },
|
||||
/// The service's answer is not the shape its contract states.
|
||||
Protocol(String),
|
||||
/// The circuit could not be compiled for the device.
|
||||
Compile(NativeError),
|
||||
/// The job ended without a result.
|
||||
Job { id: String, status: String, reason: String },
|
||||
/// The job did not finish within the sampler's wait.
|
||||
Timeout { id: String },
|
||||
/// The counts read back are not counts of this circuit.
|
||||
Counts(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for QpuError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
QpuError::Config(s) => write!(f, "configuration: {s}"),
|
||||
QpuError::Transport(s) => write!(f, "transport: {s}"),
|
||||
QpuError::Http { status, body } => write!(f, "HTTP {status}: {body}"),
|
||||
QpuError::Protocol(s) => write!(f, "unexpected answer: {s}"),
|
||||
QpuError::Compile(e) => write!(f, "compiling for the device: {e}"),
|
||||
QpuError::Job { id, status, reason } => write!(f, "job {id} {status}: {reason}"),
|
||||
QpuError::Timeout { id } => write!(f, "job {id} did not finish in time"),
|
||||
QpuError::Counts(s) => write!(f, "counts: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for QpuError {}
|
||||
|
||||
impl From<QpuError> for MemberError {
|
||||
fn from(e: QpuError) -> MemberError {
|
||||
match e {
|
||||
QpuError::Compile(NativeError::TooFewQubits { need, largest }) => MemberError::TooWide { n_qubits: need, max: largest.min(255) as u8 },
|
||||
other => MemberError::Other(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A response's body as JSON, or the error its status means.
|
||||
pub(crate) fn json_ok(r: &transport::Response) -> Result<serde_json::Value, QpuError> {
|
||||
if !(200..300).contains(&r.status) {
|
||||
let body: String = String::from_utf8_lossy(&r.body).chars().take(400).collect();
|
||||
return Err(QpuError::Http { status: r.status, body });
|
||||
}
|
||||
serde_json::from_slice(&r.body).map_err(|e| QpuError::Protocol(format!("not JSON: {e}")))
|
||||
}
|
||||
|
||||
/// A field that must be present.
|
||||
pub(crate) fn field<'a>(v: &'a serde_json::Value, path: &[&str]) -> Result<&'a serde_json::Value, QpuError> {
|
||||
let mut at = v;
|
||||
for key in path {
|
||||
at = at.get(key).ok_or_else(|| QpuError::Protocol(format!("no `{}`", path.join("."))))?;
|
||||
}
|
||||
Ok(at)
|
||||
}
|
||||
|
||||
pub(crate) fn field_str<'a>(v: &'a serde_json::Value, path: &[&str]) -> Result<&'a str, QpuError> {
|
||||
field(v, path)?.as_str().ok_or_else(|| QpuError::Protocol(format!("`{}` is not a string", path.join("."))))
|
||||
}
|
||||
|
||||
/// What a service runs: its name, native basis, connectivity and provenance.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Device {
|
||||
/// `"<service>:<device>"`, for instance `"sampler-v2:<backend>"`.
|
||||
pub name: String,
|
||||
pub basis: Basis,
|
||||
pub coupling: Coupling,
|
||||
/// [`Provenance::Silicon`] for a processor, a simulator for a service's
|
||||
/// simulator.
|
||||
pub provenance: Provenance,
|
||||
}
|
||||
|
||||
/// Where a job is.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum JobStatus {
|
||||
/// Queued or running.
|
||||
Pending,
|
||||
Done,
|
||||
/// Failed or cancelled, and why.
|
||||
Ended { status: String, reason: String },
|
||||
}
|
||||
|
||||
/// What a job reported besides its counts.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct JobInfo {
|
||||
/// The energy the service reports for the job, in kWh: the service's
|
||||
/// figure, not a measurement here.
|
||||
pub service_kwh: Option<f64>,
|
||||
}
|
||||
|
||||
/// One service's client.
|
||||
pub trait Provider {
|
||||
/// The device, read from the service.
|
||||
fn device(&self) -> Result<Device, QpuError>;
|
||||
|
||||
/// Submits `program` for `shots` shots: the job's id.
|
||||
fn submit(&self, program: &NativeProgram, shots: u64) -> Result<String, QpuError>;
|
||||
|
||||
/// Where job `id` is.
|
||||
fn status(&self, id: &str) -> Result<JobStatus, QpuError>;
|
||||
|
||||
/// Job `id`'s counts over the circuit's basis index (`2^n` entries, logical
|
||||
/// qubit `i` at bit `i`), and what else it reported.
|
||||
fn counts(&self, id: &str, program: &NativeProgram, shots: u64) -> Result<(Counts, JobInfo), QpuError>;
|
||||
}
|
||||
|
||||
impl<P: Provider + ?Sized> Provider for Box<P> {
|
||||
fn device(&self) -> Result<Device, QpuError> {
|
||||
(**self).device()
|
||||
}
|
||||
|
||||
fn submit(&self, program: &NativeProgram, shots: u64) -> Result<String, QpuError> {
|
||||
(**self).submit(program, shots)
|
||||
}
|
||||
|
||||
fn status(&self, id: &str) -> Result<JobStatus, QpuError> {
|
||||
(**self).status(id)
|
||||
}
|
||||
|
||||
fn counts(&self, id: &str, program: &NativeProgram, shots: u64) -> Result<(Counts, JobInfo), QpuError> {
|
||||
(**self).counts(id, program, shots)
|
||||
}
|
||||
}
|
||||
|
||||
/// One job a sampler ran.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct JobRecord {
|
||||
pub id: String,
|
||||
/// The circuit's hash, in hex.
|
||||
pub circuit: String,
|
||||
/// BLAKE3 of the program's OpenQASM 3 text, in hex.
|
||||
pub program: String,
|
||||
pub shots: u64,
|
||||
/// The `SWAP`s routing inserted.
|
||||
pub swaps: usize,
|
||||
/// Status checks before the job finished.
|
||||
pub polls: u32,
|
||||
/// The service's own energy figure, when it gives one.
|
||||
pub service_kwh: Option<f64>,
|
||||
}
|
||||
|
||||
/// How long a sampler waits for a job: `polls` checks, `interval` apart.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Wait {
|
||||
pub interval: Duration,
|
||||
pub polls: u32,
|
||||
}
|
||||
|
||||
impl Wait {
|
||||
/// Every 5 s, for up to 2 hours.
|
||||
pub const DEFAULT: Wait = Wait { interval: Duration::from_secs(5), polls: 1440 };
|
||||
}
|
||||
|
||||
/// A provider as a fabric sampler: see the crate documentation.
|
||||
pub struct QpuSampler<P: Provider> {
|
||||
provider: P,
|
||||
device: Device,
|
||||
id: String,
|
||||
wait: Wait,
|
||||
sleep: Box<dyn Fn(Duration)>,
|
||||
jobs: RefCell<Vec<JobRecord>>,
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
impl<P: Provider> QpuSampler<P> {
|
||||
/// A sampler over `provider`, its device read now.
|
||||
pub fn new(provider: P) -> Result<Self, QpuError> {
|
||||
let device = provider.device()?;
|
||||
let mut o = b"wai:quantum-qpu-member\x01".to_vec();
|
||||
o.extend_from_slice(device.name.as_bytes());
|
||||
o.push(0);
|
||||
o.extend_from_slice(basis_label(device.basis).as_bytes());
|
||||
o.extend_from_slice(&device.coupling.qubits().to_be_bytes());
|
||||
for a in 0..device.coupling.qubits() {
|
||||
for b in a + 1..device.coupling.qubits() {
|
||||
if device.coupling.connected(a, b) {
|
||||
o.extend_from_slice(&a.to_be_bytes());
|
||||
o.extend_from_slice(&b.to_be_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
o.extend_from_slice(device.provenance.label().as_bytes());
|
||||
let slug: String = device.name.chars().map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '-' }).collect();
|
||||
let id = format!("remote/qpu-{slug}-{}", hex(&blake3::hash(&o).as_bytes()[..6]));
|
||||
Ok(QpuSampler { provider, device, id, wait: Wait::DEFAULT, sleep: Box::new(std::thread::sleep), jobs: RefCell::new(Vec::new()) })
|
||||
}
|
||||
|
||||
/// The same sampler waiting `wait` for each job, sleeping with `sleep`.
|
||||
pub fn with_wait(mut self, wait: Wait, sleep: impl Fn(Duration) + 'static) -> Self {
|
||||
self.wait = wait;
|
||||
self.sleep = Box::new(sleep);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn device(&self) -> &Device {
|
||||
&self.device
|
||||
}
|
||||
|
||||
pub fn provider(&self) -> &P {
|
||||
&self.provider
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// `"qpu-<basis>/1"`: the compiler's basis and this crate's protocol
|
||||
/// version.
|
||||
pub fn implementation(&self) -> String {
|
||||
format!("qpu-{}/1", basis_label(self.device.basis))
|
||||
}
|
||||
|
||||
/// Every job this sampler has run, in order.
|
||||
pub fn jobs(&self) -> Vec<JobRecord> {
|
||||
self.jobs.borrow().clone()
|
||||
}
|
||||
|
||||
/// The program the device would run for `c`, checked against `c`.
|
||||
pub fn compile(&self, c: &Circuit) -> Result<NativeProgram, QpuError> {
|
||||
let program = native::compile(c, self.device.basis, &self.device.coupling).map_err(QpuError::Compile)?;
|
||||
program.verify(c, &self.device.coupling).map_err(QpuError::Compile)?;
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
/// `shots` shots of `c` on the device: its counts over `c`'s basis index,
|
||||
/// and the job's record.
|
||||
pub fn run(&self, c: &Circuit, shots: u64) -> Result<(Counts, JobRecord), QpuError> {
|
||||
let program = self.compile(c)?;
|
||||
let id = self.provider.submit(&program, shots)?;
|
||||
let mut polls = 0u32;
|
||||
loop {
|
||||
match self.provider.status(&id)? {
|
||||
JobStatus::Done => break,
|
||||
JobStatus::Ended { status, reason } => return Err(QpuError::Job { id, status, reason }),
|
||||
JobStatus::Pending => {
|
||||
polls += 1;
|
||||
if polls > self.wait.polls {
|
||||
return Err(QpuError::Timeout { id });
|
||||
}
|
||||
(self.sleep)(self.wait.interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (counts, info) = self.provider.counts(&id, &program, shots)?;
|
||||
let total: u64 = counts.iter().sum();
|
||||
if counts.len() != 1usize << c.n_qubits || total != shots {
|
||||
return Err(QpuError::Counts(format!("{} outcomes summing to {total}, for {} qubits and {shots} shots", counts.len(), c.n_qubits)));
|
||||
}
|
||||
let record = JobRecord {
|
||||
id,
|
||||
circuit: circuit_hash(c).map(|h| hex(&h)).unwrap_or_default(),
|
||||
program: hex(blake3::hash(program.qasm3().as_bytes()).as_bytes()),
|
||||
shots,
|
||||
swaps: program.swaps,
|
||||
polls,
|
||||
service_kwh: info.service_kwh,
|
||||
};
|
||||
self.jobs.borrow_mut().push(record.clone());
|
||||
Ok((counts, record))
|
||||
}
|
||||
|
||||
/// A fabric [`Member`] of class [`FabricClass::Qpu`]: one sampling entry
|
||||
/// per gateset, each certified in this call on `corpus` with `shots` per
|
||||
/// circuit, with the device's provenance. Every circuit of the corpus is a
|
||||
/// job on the service, so this spends the service's quota.
|
||||
pub fn member(&self, corpus: QpuCorpus, shots: u64) -> Result<Member, MemberError> {
|
||||
let mut kernels = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
let certificate = ready(certify_sampler(self, &self.implementation(), gs, corpus, shots, self.device.provenance.clone()))
|
||||
.ok_or_else(|| MemberError::Other("the certification did not resolve".into()))??;
|
||||
kernels.push(KernelEntry { kernel: sample_kernel(gs).into(), implementation: self.implementation(), certificate, energy: None });
|
||||
}
|
||||
Ok(Member {
|
||||
id: self.id.clone(),
|
||||
class: FabricClass::Qpu,
|
||||
presence: Presence::Detected,
|
||||
kernels,
|
||||
detail: format!(
|
||||
"{}; {} qubits; basis {}; {}; circuits compiled here, checked by simulation before submission, and certified on their counts",
|
||||
self.device.name,
|
||||
self.device.coupling.qubits(),
|
||||
basis_label(self.device.basis),
|
||||
self.device.provenance.label()
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Provider> Sampler for QpuSampler<P> {
|
||||
fn member_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn sample<'r>(&'r self, implementation: &'r str, c: &'r Circuit, shots: u64, _meter: &'r mut dyn RunMeter) -> SampleFuture<'r> {
|
||||
Box::pin(async move {
|
||||
if implementation != self.implementation() {
|
||||
return Err(MemberError::Other(format!("{} runs implementation {}, not {implementation:?}", self.id, self.implementation())));
|
||||
}
|
||||
let (counts, _) = self.run(c, shots)?;
|
||||
check_counts(c, shots, &counts)?;
|
||||
// The work ran at the service: unmetered here.
|
||||
Ok((counts, 0, None))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A certification's record: the device, the member, each certificate with its
|
||||
/// hash, and every job, as JSON.
|
||||
pub fn report<P: Provider>(sampler: &QpuSampler<P>, member: &Member) -> serde_json::Value {
|
||||
let certificates: Vec<serde_json::Value> = member
|
||||
.kernels
|
||||
.iter()
|
||||
.map(|k| {
|
||||
let c = &k.certificate;
|
||||
let parity = match c.parity {
|
||||
wai_quantum::quantum_fabric::Parity::Statistical { fidelity, shots } => serde_json::json!({"statistical": {"fidelity": fidelity, "shots": shots}}),
|
||||
ref other => serde_json::json!(format!("{other:?}")),
|
||||
};
|
||||
serde_json::json!({
|
||||
"kernel": c.kernel,
|
||||
"implementation": c.implementation,
|
||||
"member": c.member,
|
||||
"generator": c.generator,
|
||||
"cases": c.cases,
|
||||
"corpus": hex(&c.corpus),
|
||||
"parity": parity,
|
||||
"provenance": c.provenance.label(),
|
||||
"hash": hex(&c.hash()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let jobs: Vec<serde_json::Value> = sampler
|
||||
.jobs()
|
||||
.iter()
|
||||
.map(|j| serde_json::json!({"id": j.id, "circuit": j.circuit, "program": j.program, "shots": j.shots, "swaps": j.swaps, "polls": j.polls, "service_kwh": j.service_kwh}))
|
||||
.collect();
|
||||
serde_json::json!({
|
||||
"device": sampler.device().name,
|
||||
"qubits": sampler.device().coupling.qubits(),
|
||||
"basis": basis_label(sampler.device().basis),
|
||||
"provenance": sampler.device().provenance.label(),
|
||||
"member": member.id,
|
||||
"detail": member.detail,
|
||||
"certificates": certificates,
|
||||
"jobs": jobs,
|
||||
})
|
||||
}
|
||||
|
||||
/// The basis's name in ids: `"rzsxxcz"`, `"prxcz"`, `"gpizz"`.
|
||||
pub fn basis_label(b: Basis) -> &'static str {
|
||||
match b {
|
||||
Basis::RzSxXCz => "rzsxxcz",
|
||||
Basis::PrxCz => "prxcz",
|
||||
Basis::GpiZz => "gpizz",
|
||||
}
|
||||
}
|
||||
|
||||
/// The first job to run on a service: an `X` on qubit 0 of `n` qubits, every
|
||||
/// qubit measured. Its counts must all land on index 1 (`c[0] = 1`, every
|
||||
/// other bit 0); if they do not, the client's bit order is wrong for that
|
||||
/// service.
|
||||
pub fn probe_circuit(n: u8) -> Circuit {
|
||||
let mut c = Circuit::new(n);
|
||||
c.x(0);
|
||||
c
|
||||
}
|
||||
|
||||
/// Whether `counts` of [`probe_circuit`] put at least `threshold` of the shots
|
||||
/// on index 1, and where the most shots landed.
|
||||
pub fn check_probe(counts: &[u64], threshold: f64) -> Result<(), String> {
|
||||
let total: u64 = counts.iter().sum();
|
||||
let at_one = counts.get(1).copied().unwrap_or(0);
|
||||
let (top, _) = counts.iter().enumerate().max_by_key(|&(_, &n)| n).unwrap_or((0, &0));
|
||||
if total > 0 && at_one as f64 >= threshold * total as f64 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("{at_one} of {total} shots on index 1; the most on index {top}: the service's bit order is not the one the client assumes"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Probabilities over a register of `width` bits as counts summing to
|
||||
/// `shots`: each `p·shots` rounded down, then the remaining shots to the
|
||||
/// largest remainders, ties to the lower index. For a service that reports
|
||||
/// probabilities rather than counts.
|
||||
pub fn counts_from_probabilities(p: &[f64], shots: u64) -> Counts {
|
||||
let total: f64 = p.iter().sum();
|
||||
if total <= 0.0 {
|
||||
return vec![0; p.len()];
|
||||
}
|
||||
let scaled: Vec<f64> = p.iter().map(|x| x / total * shots as f64).collect();
|
||||
let mut counts: Vec<u64> = scaled.iter().map(|x| x.floor() as u64).collect();
|
||||
let mut left = shots.saturating_sub(counts.iter().sum());
|
||||
let mut order: Vec<usize> = (0..p.len()).collect();
|
||||
order.sort_by(|&a, &b| (scaled[b] - scaled[b].floor()).total_cmp(&(scaled[a] - scaled[a].floor())).then(a.cmp(&b)));
|
||||
for i in order {
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
counts[i] += 1;
|
||||
left -= 1;
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
/// The circuit's index of a register read on physical qubits: logical qubit
|
||||
/// `i` is physical bit `final_layout[i]` of `physical`.
|
||||
pub(crate) fn logical_index(physical: u64, final_layout: &[u16]) -> usize {
|
||||
final_layout.iter().enumerate().fold(0usize, |x, (i, &p)| x | ((((physical >> p) & 1) as usize) << i))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn secrets_stay_out_of_debug_output() {
|
||||
let s = Secret::new("k-123");
|
||||
assert_eq!(format!("{s:?}"), "Secret(…)");
|
||||
assert_eq!(s.expose(), "k-123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probabilities_become_counts_that_sum_to_the_shots() {
|
||||
assert_eq!(counts_from_probabilities(&[0.5, 0.5], 101), vec![51, 50]);
|
||||
assert_eq!(counts_from_probabilities(&[0.2, 0.3, 0.5], 10), vec![2, 3, 5]);
|
||||
assert_eq!(counts_from_probabilities(&[1.0 / 3.0; 3], 100).iter().sum::<u64>(), 100);
|
||||
assert_eq!(counts_from_probabilities(&[0.0, 0.0], 5), vec![0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_physical_register_reads_back_onto_logical_qubits() {
|
||||
// Logical 0 ended on physical 4, logical 1 on physical 2.
|
||||
assert_eq!(logical_index(0b10000, &[4, 2]), 0b01);
|
||||
assert_eq!(logical_index(0b00100, &[4, 2]), 0b10);
|
||||
assert_eq!(logical_index(0b11111, &[4, 2]), 0b11);
|
||||
assert_eq!(logical_index(0b01011, &[4, 2]), 0b00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_probe_checks_where_the_shots_landed() {
|
||||
assert!(check_probe(&[1, 98, 1, 0], 0.9).is_ok());
|
||||
let e = check_probe(&[0, 2, 98, 0], 0.9).unwrap_err();
|
||||
assert!(e.contains("the most on index 2"), "{e}");
|
||||
}
|
||||
}
|
||||
196
wai/wai-quantum-qpu/src/native_turns.rs
Normal file
196
wai/wai-quantum-qpu/src/native_turns.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
//! A native-gate circuit service in turns: the gates run as written.
|
||||
//!
|
||||
//! - **Auth**: `Authorization: apiKey <key>`.
|
||||
//! - **Device**: `GET /backends/{backend}`: its `qubits`. Every pair of a
|
||||
//! trapped-ion register interacts, so the map is complete and routing never
|
||||
//! inserts a `SWAP`.
|
||||
//! - **Submit**: `POST /jobs` of the circuit job type with the `native`
|
||||
//! gateset (`gpi`, `gpi2`, `zz`), phases and angles in turns, so every dyadic
|
||||
//! angle crosses the wire exactly; debiasing off, so the job runs the
|
||||
//! program as compiled. On the simulator, `noise.model` picks a device's
|
||||
//! noise.
|
||||
//! - **Results**: `GET /jobs/{id}/results/probabilities`: probabilities keyed by
|
||||
//! decimal integers whose bit `p` is physical qubit `p`, read back onto
|
||||
//! logical qubits through the program's final layout, and made counts by
|
||||
//! largest remainders.
|
||||
//!
|
||||
//! The service reports an energy estimate per job (`stats.kwh`); it is kept as
|
||||
//! the service's figure ([`JobInfo::service_kwh`]).
|
||||
|
||||
use crate::transport::{Request, Transport};
|
||||
use crate::{Device, JobInfo, JobStatus, Provider, QpuError, Secret, counts_from_probabilities, field, field_str, json_ok, logical_index};
|
||||
use serde_json::{Value, json};
|
||||
use wai_quantum::quantum_fabric::Provenance;
|
||||
use wai_quantum::quantum_fabric::sampling::Counts;
|
||||
use wai_quantum::quantum_toolchain::native::{Basis, Coupling, Dyadic, NativeGate, NativeProgram};
|
||||
|
||||
/// The service's API base.
|
||||
pub const BASE: &str = "https://api.ionq.co/v0.4"; // allow-name: the service's endpoint
|
||||
/// The job type of a gate-list circuit.
|
||||
pub const JOB_TYPE: &str = "ionq.circuit.v1"; // allow-name: a protocol job type
|
||||
|
||||
/// A client for one backend: `"simulator"` or a processor such as
|
||||
/// a processor's backend name.
|
||||
pub struct NativeTurns<T: Transport> {
|
||||
transport: T,
|
||||
key: Secret,
|
||||
backend: String,
|
||||
noise: Option<String>,
|
||||
base: String,
|
||||
}
|
||||
|
||||
impl<T: Transport> NativeTurns<T> {
|
||||
pub fn new(transport: T, key: Secret, backend: impl Into<String>) -> Self {
|
||||
NativeTurns { transport, key, backend: backend.into(), noise: None, base: BASE.into() }
|
||||
}
|
||||
|
||||
/// From `WAI_QPU_TURNS_KEY` and `WAI_QPU_TURNS_BACKEND` (default
|
||||
/// `simulator`), with `WAI_QPU_TURNS_NOISE` as the simulator's noise model
|
||||
/// when set.
|
||||
pub fn from_env(transport: T) -> Result<Self, QpuError> {
|
||||
let backend = std::env::var("WAI_QPU_TURNS_BACKEND").ok().filter(|b| !b.is_empty()).unwrap_or_else(|| "simulator".into());
|
||||
let mut c = NativeTurns::new(transport, Secret::from_env("WAI_QPU_TURNS_KEY")?, backend);
|
||||
c.noise = std::env::var("WAI_QPU_TURNS_NOISE").ok().filter(|n| !n.is_empty());
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// The simulator with a device's noise model, by the name the service
|
||||
/// gives it.
|
||||
pub fn with_noise(mut self, model: impl Into<String>) -> Self {
|
||||
self.noise = Some(model.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Another endpoint: a test server.
|
||||
pub fn with_base(mut self, base: impl Into<String>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn authed(&self, r: Request) -> Request {
|
||||
r.header("Authorization", format!("apiKey {}", self.key.expose())).header("Accept", "application/json")
|
||||
}
|
||||
|
||||
fn is_simulator(&self) -> bool {
|
||||
self.backend == "simulator"
|
||||
}
|
||||
}
|
||||
|
||||
/// A dyadic angle in turns, `m/2^k`: exact in binary64.
|
||||
pub fn turns(d: Dyadic) -> f64 {
|
||||
let (m, k) = d.turns();
|
||||
m as f64 / (1u64 << k) as f64
|
||||
}
|
||||
|
||||
fn complete(n: u16) -> Coupling {
|
||||
let mut pairs = Vec::new();
|
||||
for a in 0..n {
|
||||
for b in a + 1..n {
|
||||
pairs.push((a, b));
|
||||
}
|
||||
}
|
||||
Coupling::new(n, &pairs).expect("a complete map is valid")
|
||||
}
|
||||
|
||||
impl<T: Transport> Provider for NativeTurns<T> {
|
||||
fn device(&self) -> Result<Device, QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/backends/{}", self.base, self.backend))))?;
|
||||
let v = json_ok(&r)?;
|
||||
let n = field(&v, &["qubits"])?.as_u64().filter(|&n| n > 0 && n <= 64).ok_or_else(|| QpuError::Protocol("qubits".into()))?;
|
||||
let provenance = if self.is_simulator() {
|
||||
Provenance::Simulator(format!("native-turns simulator, noise {}", self.noise.as_deref().unwrap_or("ideal")))
|
||||
} else {
|
||||
Provenance::Silicon
|
||||
};
|
||||
Ok(Device { name: format!("native-turns:{}", self.backend), basis: Basis::GpiZz, coupling: complete(n as u16), provenance })
|
||||
}
|
||||
|
||||
fn submit(&self, program: &NativeProgram, shots: u64) -> Result<String, QpuError> {
|
||||
if program.basis != Basis::GpiZz {
|
||||
return Err(QpuError::Config("this service runs gpi, gpi2 and zz".into()));
|
||||
}
|
||||
let mut circuit: Vec<Value> = Vec::with_capacity(program.gates.len());
|
||||
let mut width = program.final_layout.iter().copied().max().map_or(0, |q| q + 1);
|
||||
for g in &program.gates {
|
||||
circuit.push(match *g {
|
||||
NativeGate::Gpi(q, p) => {
|
||||
width = width.max(q + 1);
|
||||
json!({"gate": "gpi", "target": q, "phase": turns(p)})
|
||||
}
|
||||
NativeGate::Gpi2(q, p) => {
|
||||
width = width.max(q + 1);
|
||||
json!({"gate": "gpi2", "target": q, "phase": turns(p)})
|
||||
}
|
||||
NativeGate::Zz(a, b, t) => {
|
||||
width = width.max(a.max(b) + 1);
|
||||
json!({"gate": "zz", "targets": [a, b], "angle": turns(t)})
|
||||
}
|
||||
other => return Err(QpuError::Config(format!("{other:?} is not one of this service's native gates"))),
|
||||
});
|
||||
}
|
||||
let mut body = json!({
|
||||
"type": JOB_TYPE,
|
||||
"backend": self.backend,
|
||||
"shots": shots,
|
||||
"settings": {"error_mitigation": {"debiasing": false}},
|
||||
"input": {"gateset": "native", "qubits": width, "circuit": circuit}
|
||||
});
|
||||
if self.is_simulator()
|
||||
&& let Some(model) = &self.noise
|
||||
{
|
||||
body["noise"] = json!({"model": model});
|
||||
}
|
||||
let r = self.transport.send(&self.authed(Request::post(format!("{}/jobs", self.base), body.to_string().into_bytes()).header("Content-Type", "application/json")))?;
|
||||
Ok(field_str(&json_ok(&r)?, &["id"])?.to_string())
|
||||
}
|
||||
|
||||
fn status(&self, id: &str) -> Result<JobStatus, QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/jobs/{id}", self.base))))?;
|
||||
let v = json_ok(&r)?;
|
||||
Ok(match field_str(&v, &["status"])? {
|
||||
"submitted" | "ready" | "started" => JobStatus::Pending,
|
||||
"completed" => JobStatus::Done,
|
||||
other => JobStatus::Ended { status: other.to_string(), reason: v.get("failure").map(|f| f.to_string()).unwrap_or_default() },
|
||||
})
|
||||
}
|
||||
|
||||
fn counts(&self, id: &str, program: &NativeProgram, shots: u64) -> Result<(Counts, JobInfo), QpuError> {
|
||||
let job = json_ok(&self.transport.send(&self.authed(Request::get(format!("{}/jobs/{id}", self.base))))?)?;
|
||||
let service_kwh = job.get("stats").and_then(|s| s.get("kwh")).and_then(|k| k.as_f64());
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/jobs/{id}/results/probabilities", self.base))))?;
|
||||
let v = json_ok(&r)?;
|
||||
let map = v.as_object().ok_or_else(|| QpuError::Protocol("probabilities is not a map".into()))?;
|
||||
let mut p = vec![0.0f64; 1 << program.n_qubits];
|
||||
for (key, prob) in map {
|
||||
let physical: u64 = key.parse().map_err(|_| QpuError::Counts(format!("outcome {key:?} is not a decimal integer")))?;
|
||||
let prob = prob.as_f64().filter(|x| x.is_finite() && *x >= 0.0).ok_or_else(|| QpuError::Counts(format!("probability of {key:?}")))?;
|
||||
p[logical_index(physical, &program.final_layout)] += prob;
|
||||
}
|
||||
Ok((counts_from_probabilities(&p, shots), JobInfo { service_kwh }))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn turns_are_exact_binary_fractions() {
|
||||
let d = |m, k| Dyadic::new(m, k).unwrap();
|
||||
assert_eq!(turns(Dyadic::ZERO), 0.0);
|
||||
assert_eq!(turns(d(1, 2)), 0.25);
|
||||
assert_eq!(turns(d(3, 2)), 0.75);
|
||||
assert_eq!(turns(d(1, 32)), 1.0 / 4294967296.0);
|
||||
assert_eq!(json!(turns(d(5, 4))).to_string(), "0.3125");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_pair_of_the_register_interacts() {
|
||||
let c = complete(4);
|
||||
for a in 0..4 {
|
||||
for b in 0..4 {
|
||||
assert_eq!(c.connected(a, b), a != b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
wai/wai-quantum-qpu/src/prx_circuits.rs
Normal file
190
wai/wai-quantum-qpu/src/prx_circuits.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
//! A circuit service of phased-X and CZ instructions on named qubits.
|
||||
//!
|
||||
//! - **Auth**: `Authorization: Bearer <token>`, the token from the service's
|
||||
//! dashboard, sent as it is.
|
||||
//! - **Device**: the dynamic quantum architecture of the default calibration
|
||||
//! (`GET /calibration-sets/{qc}/default/dynamic-quantum-architecture`): its
|
||||
//! `qubits` name the physical qubits in order, its gates must hold `prx`,
|
||||
//! `cz` and `measure`, and the union of the `cz` implementations' loci gives
|
||||
//! the pairs.
|
||||
//! - **Submit**: `POST /jobs/{qc}/circuit` with `circuits` and `shots` only
|
||||
//! (the service takes no fields it does not know): `prx` with `angle` and
|
||||
//! `phase` in radians, `cz`, and one `measure` per classical bit, keyed
|
||||
//! `c<i>`, on the qubit logical qubit `i` ends on.
|
||||
//! - **Results**: `GET /jobs/{id}/artifacts/measurement_counts`: counts keyed
|
||||
//! by bitstrings whose characters follow `measurement_keys` in order.
|
||||
//!
|
||||
//! The angles are radians, so a dyadic angle crosses the wire as the nearest
|
||||
//! binary64 to `π·m/2^(k−1)`, not exactly.
|
||||
|
||||
use crate::transport::{Request, Transport};
|
||||
use crate::{Device, JobInfo, JobStatus, Provider, QpuError, Secret, field, field_str, json_ok};
|
||||
use serde_json::{Value, json};
|
||||
use std::cell::RefCell;
|
||||
use wai_quantum::quantum_fabric::Provenance;
|
||||
use wai_quantum::quantum_fabric::sampling::Counts;
|
||||
use wai_quantum::quantum_toolchain::native::{Basis, Coupling, Dyadic, NativeGate, NativeProgram};
|
||||
|
||||
/// The service's API base.
|
||||
pub const BASE: &str = "https://resonance.iqm.tech/api/v1"; // allow-name: the service's endpoint
|
||||
|
||||
/// A client for one quantum computer.
|
||||
pub struct PrxCircuits<T: Transport> {
|
||||
transport: T,
|
||||
token: Secret,
|
||||
qc: String,
|
||||
base: String,
|
||||
/// The physical qubits' names, by index, once the device is read.
|
||||
names: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<T: Transport> PrxCircuits<T> {
|
||||
/// A client for the quantum computer `qc` (its alias).
|
||||
pub fn new(transport: T, token: Secret, qc: impl Into<String>) -> Self {
|
||||
PrxCircuits { transport, token, qc: qc.into(), base: BASE.into(), names: RefCell::new(Vec::new()) }
|
||||
}
|
||||
|
||||
/// From `WAI_QPU_PRX_TOKEN` and `WAI_QPU_PRX_QC`.
|
||||
pub fn from_env(transport: T) -> Result<Self, QpuError> {
|
||||
let qc = Secret::from_env("WAI_QPU_PRX_QC")?.expose().to_string();
|
||||
Ok(PrxCircuits::new(transport, Secret::from_env("WAI_QPU_PRX_TOKEN")?, qc))
|
||||
}
|
||||
|
||||
/// Another endpoint: a test server.
|
||||
pub fn with_base(mut self, base: impl Into<String>) -> Self {
|
||||
self.base = base.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn authed(&self, r: Request) -> Request {
|
||||
r.header("Authorization", format!("Bearer {}", self.token.expose())).header("Accept", "application/json")
|
||||
}
|
||||
|
||||
fn name(&self, q: u16) -> Result<String, QpuError> {
|
||||
self.names.borrow().get(usize::from(q)).cloned().ok_or_else(|| QpuError::Config(format!("physical qubit {q} has no name; read the device first")))
|
||||
}
|
||||
}
|
||||
|
||||
/// A dyadic angle in radians, in `(−π, π]`.
|
||||
pub fn radians(d: Dyadic) -> f64 {
|
||||
let (m, k) = d.turns();
|
||||
if k == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let full = 1u64 << k;
|
||||
let signed = if m > full / 2 { m as f64 - full as f64 } else { m as f64 };
|
||||
signed / full as f64 * std::f64::consts::TAU
|
||||
}
|
||||
|
||||
impl<T: Transport> Provider for PrxCircuits<T> {
|
||||
fn device(&self) -> Result<Device, QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/calibration-sets/{}/default/dynamic-quantum-architecture", self.base, self.qc))))?;
|
||||
let v = json_ok(&r)?;
|
||||
let names: Vec<String> = field(&v, &["qubits"])?.as_array().ok_or_else(|| QpuError::Protocol("qubits is not a list".into()))?.iter().filter_map(|q| q.as_str().map(String::from)).collect();
|
||||
if names.is_empty() || names.len() > usize::from(u16::MAX) {
|
||||
return Err(QpuError::Protocol(format!("{} qubits", names.len())));
|
||||
}
|
||||
let gates = field(&v, &["gates"])?;
|
||||
for g in ["prx", "cz", "measure"] {
|
||||
if gates.get(g).is_none() {
|
||||
return Err(QpuError::Protocol(format!("{} has no `{g}` gate", self.qc)));
|
||||
}
|
||||
}
|
||||
let index = |name: &str| names.iter().position(|n| n == name).map(|i| i as u16);
|
||||
let mut pairs = Vec::new();
|
||||
if let Some(impls) = gates["cz"].get("implementations").and_then(|i| i.as_object()) {
|
||||
for imp in impls.values() {
|
||||
for locus in imp.get("loci").and_then(|l| l.as_array()).into_iter().flatten() {
|
||||
let pair: Vec<&str> = locus.as_array().map(|a| a.iter().filter_map(|x| x.as_str()).collect()).unwrap_or_default();
|
||||
// A locus on a resonator is not a qubit pair.
|
||||
if let [a, b] = pair.as_slice()
|
||||
&& let (Some(a), Some(b)) = (index(a), index(b))
|
||||
{
|
||||
pairs.push((a, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let coupling = Coupling::new(names.len() as u16, &pairs).map_err(QpuError::Compile)?;
|
||||
*self.names.borrow_mut() = names;
|
||||
// A mock quantum computer returns random bits: a stand-in, never silicon.
|
||||
let provenance = if self.qc.ends_with(":mock") { Provenance::Simulator(format!("prx-circuits {}", self.qc)) } else { Provenance::Silicon };
|
||||
Ok(Device { name: format!("prx-circuits:{}", self.qc), basis: Basis::PrxCz, coupling, provenance })
|
||||
}
|
||||
|
||||
fn submit(&self, program: &NativeProgram, shots: u64) -> Result<String, QpuError> {
|
||||
if program.basis != Basis::PrxCz {
|
||||
return Err(QpuError::Config("this service runs prx and cz".into()));
|
||||
}
|
||||
let mut instructions: Vec<Value> = Vec::with_capacity(program.gates.len() + program.final_layout.len());
|
||||
for g in &program.gates {
|
||||
instructions.push(match *g {
|
||||
NativeGate::Prx(q, theta, phi) => json!({"name": "prx", "locus": [self.name(q)?], "args": {"angle": radians(theta), "phase": radians(phi)}}),
|
||||
NativeGate::Cz(a, b) => json!({"name": "cz", "locus": [self.name(a)?, self.name(b)?], "args": {}}),
|
||||
other => return Err(QpuError::Config(format!("{other:?} is not one of this service's instructions"))),
|
||||
});
|
||||
}
|
||||
for (i, &p) in program.final_layout.iter().enumerate() {
|
||||
instructions.push(json!({"name": "measure", "locus": [self.name(p)?], "args": {"key": format!("c{i}")}}));
|
||||
}
|
||||
let body = json!({"circuits": [{"name": "wai", "instructions": instructions}], "shots": shots});
|
||||
let r = self.transport.send(&self.authed(Request::post(format!("{}/jobs/{}/circuit", self.base, self.qc), body.to_string().into_bytes()).header("Content-Type", "application/json; charset=UTF-8")))?;
|
||||
Ok(field_str(&json_ok(&r)?, &["id"])?.to_string())
|
||||
}
|
||||
|
||||
fn status(&self, id: &str) -> Result<JobStatus, QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/jobs/{id}", self.base))))?;
|
||||
let v = json_ok(&r)?;
|
||||
Ok(match field_str(&v, &["status"])? {
|
||||
"completed" => JobStatus::Done,
|
||||
ended @ ("failed" | "cancelled") => JobStatus::Ended { status: ended.to_string(), reason: v.get("errors").map(|e| e.to_string()).unwrap_or_default() },
|
||||
// `waiting`, `processing`, and any state the contract adds later.
|
||||
_ => JobStatus::Pending,
|
||||
})
|
||||
}
|
||||
|
||||
fn counts(&self, id: &str, program: &NativeProgram, _shots: u64) -> Result<(Counts, JobInfo), QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/jobs/{id}/artifacts/measurement_counts", self.base))).header("Accept-Encoding", "identity"))?;
|
||||
let v = json_ok(&r)?;
|
||||
let first = v.get(0).ok_or_else(|| QpuError::Protocol("no circuit's counts".into()))?;
|
||||
let keys: Vec<&str> = field(first, &["measurement_keys"])?.as_array().map(|a| a.iter().filter_map(|k| k.as_str()).collect()).unwrap_or_default();
|
||||
let n = usize::from(program.n_qubits);
|
||||
// Character position of each classical bit's key.
|
||||
let mut position = vec![usize::MAX; n];
|
||||
for (pos, key) in keys.iter().enumerate() {
|
||||
if let Some(i) = key.strip_prefix('c').and_then(|i| i.parse::<usize>().ok())
|
||||
&& i < n
|
||||
{
|
||||
position[i] = pos;
|
||||
}
|
||||
}
|
||||
if position.contains(&usize::MAX) || keys.len() != n {
|
||||
return Err(QpuError::Protocol(format!("measurement keys {keys:?} for {n} classical bits")));
|
||||
}
|
||||
let mut counts = vec![0u64; 1 << n];
|
||||
for (bits, k) in field(first, &["counts"])?.as_object().ok_or_else(|| QpuError::Protocol("counts is not a map".into()))? {
|
||||
let chars = bits.as_bytes();
|
||||
if chars.len() != n || !chars.iter().all(|c| *c == b'0' || *c == b'1') {
|
||||
return Err(QpuError::Counts(format!("outcome {bits:?} for {n} bits")));
|
||||
}
|
||||
let x = (0..n).fold(0usize, |x, i| x | (usize::from(chars[position[i]] - b'0') << i));
|
||||
counts[x] += k.as_u64().ok_or_else(|| QpuError::Counts(format!("count of {bits:?}")))?;
|
||||
}
|
||||
Ok((counts, JobInfo::default()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dyadic_angles_become_radians_in_the_half_open_turn() {
|
||||
let d = |m, k| Dyadic::new(m, k).unwrap();
|
||||
assert_eq!(radians(Dyadic::ZERO), 0.0);
|
||||
assert_eq!(radians(d(1, 1)), std::f64::consts::PI);
|
||||
assert_eq!(radians(d(1, 2)), std::f64::consts::FRAC_PI_2);
|
||||
assert_eq!(radians(d(3, 2)), -std::f64::consts::FRAC_PI_2);
|
||||
assert_eq!(radians(d(7, 3)), -std::f64::consts::FRAC_PI_4);
|
||||
}
|
||||
}
|
||||
216
wai/wai-quantum-qpu/src/sampler_v2.rs
Normal file
216
wai/wai-quantum-qpu/src/sampler_v2.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
//! A Sampler V2 service: one OpenQASM 3 circuit per job, on physical qubits,
|
||||
//! in the backend's own gates.
|
||||
//!
|
||||
//! - **Auth**: an IAM bearer token minted from the API key
|
||||
//! (the API-key grant), sent with the instance's `Service-CRN` and the
|
||||
//! pinned API version header; the token is
|
||||
//! re-minted a minute before it expires.
|
||||
//! - **Device**: `GET /backends/{backend}/configuration`: its `basis_gates`
|
||||
//! must hold `rz`, `sx`, `x` and `cz`, and the `cz` gate's own
|
||||
//! `coupling_map` (else the backend's) gives the pairs.
|
||||
//! - **Submit**: `POST /jobs` with `program_id: "sampler"`, `params.version: 2`
|
||||
//! and one PUB `[qasm, null, shots]`, dynamical decoupling and twirling off,
|
||||
//! so the job runs the program as compiled.
|
||||
//! - **Results**: `GET /jobs/{id}/results`: `results[0].data.c.samples`, one
|
||||
//! hex string per shot, whose bit `i` is `c[i]`.
|
||||
|
||||
use crate::transport::{Request, Transport};
|
||||
use crate::{Device, JobInfo, JobStatus, Provider, QpuError, Secret, field, field_str, json_ok};
|
||||
use serde_json::json;
|
||||
use std::cell::RefCell;
|
||||
use wai_quantum::quantum_fabric::Provenance;
|
||||
use wai_quantum::quantum_fabric::sampling::Counts;
|
||||
use wai_quantum::quantum_toolchain::device::counts_from_integers;
|
||||
use wai_quantum::quantum_toolchain::native::{Basis, Coupling, NativeProgram};
|
||||
|
||||
/// The service's API base, us-east.
|
||||
pub const BASE: &str = "https://quantum.cloud.ibm.com/api/v1"; // allow-name: the service's endpoint
|
||||
/// The token endpoint.
|
||||
pub const IAM: &str = "https://iam.cloud.ibm.com/identity/token"; // allow-name: the token endpoint
|
||||
/// The API version this client is written against.
|
||||
pub const API_VERSION: &str = "2026-04-15";
|
||||
/// The header that carries it.
|
||||
pub const VERSION_HEADER: &str = "IBM-API-Version"; // allow-name: a protocol header name
|
||||
/// The token grant for an API key, form-encoded.
|
||||
const GRANT: &str = "urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey"; // allow-name: a protocol grant type
|
||||
|
||||
/// A client for one backend.
|
||||
pub struct SamplerV2<T: Transport> {
|
||||
transport: T,
|
||||
api_key: Secret,
|
||||
crn: String,
|
||||
backend: String,
|
||||
base: String,
|
||||
iam: String,
|
||||
token: RefCell<Option<(Secret, u64)>>,
|
||||
now: fn() -> u64,
|
||||
}
|
||||
|
||||
fn unix_now() -> u64 {
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
|
||||
impl<T: Transport> SamplerV2<T> {
|
||||
/// A client for `backend` on the instance `crn`.
|
||||
pub fn new(transport: T, api_key: Secret, crn: impl Into<String>, backend: impl Into<String>) -> Self {
|
||||
SamplerV2 { transport, api_key, crn: crn.into(), backend: backend.into(), base: BASE.into(), iam: IAM.into(), token: RefCell::new(None), now: unix_now }
|
||||
}
|
||||
|
||||
/// From `WAI_QPU_SAMPLER_API_KEY`, `WAI_QPU_SAMPLER_CRN` and `WAI_QPU_SAMPLER_BACKEND`.
|
||||
pub fn from_env(transport: T) -> Result<Self, QpuError> {
|
||||
let crn = Secret::from_env("WAI_QPU_SAMPLER_CRN")?.expose().to_string();
|
||||
let backend = Secret::from_env("WAI_QPU_SAMPLER_BACKEND")?.expose().to_string();
|
||||
Ok(SamplerV2::new(transport, Secret::from_env("WAI_QPU_SAMPLER_API_KEY")?, crn, backend))
|
||||
}
|
||||
|
||||
/// Other endpoints: another region, or a test server.
|
||||
pub fn with_endpoints(mut self, base: impl Into<String>, iam: impl Into<String>) -> Self {
|
||||
self.base = base.into();
|
||||
self.iam = iam.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Another clock, for tests.
|
||||
pub fn with_clock(mut self, now: fn() -> u64) -> Self {
|
||||
self.now = now;
|
||||
self
|
||||
}
|
||||
|
||||
fn bearer(&self) -> Result<String, QpuError> {
|
||||
let now = (self.now)();
|
||||
if let Some((token, expires)) = self.token.borrow().as_ref()
|
||||
&& *expires > now + 60
|
||||
{
|
||||
return Ok(token.expose().to_string());
|
||||
}
|
||||
let body = format!("grant_type={GRANT}&apikey={}", form_encode(self.api_key.expose()));
|
||||
let r = self.transport.send(&Request::post(&self.iam, body.into_bytes()).header("Content-Type", "application/x-www-form-urlencoded").header("Accept", "application/json"))?;
|
||||
let v = json_ok(&r)?;
|
||||
let token = field_str(&v, &["access_token"])?.to_string();
|
||||
let expires = v.get("expiration").and_then(|e| e.as_u64()).unwrap_or(now + v.get("expires_in").and_then(|e| e.as_u64()).unwrap_or(3600));
|
||||
*self.token.borrow_mut() = Some((Secret::new(token.clone()), expires));
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn authed(&self, r: Request) -> Result<Request, QpuError> {
|
||||
Ok(r.header("Authorization", format!("Bearer {}", self.bearer()?)).header("Service-CRN", self.crn.clone()).header(VERSION_HEADER, API_VERSION).header("Accept", "application/json"))
|
||||
}
|
||||
}
|
||||
|
||||
/// `application/x-www-form-urlencoded` for one value.
|
||||
fn form_encode(s: &str) -> String {
|
||||
s.bytes()
|
||||
.map(|b| match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => (b as char).to_string(),
|
||||
_ => format!("%{b:02X}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The backend's two-qubit pairs: the `cz` gate's own map, else the
|
||||
/// backend's.
|
||||
fn pairs(config: &serde_json::Value) -> Result<Vec<(u16, u16)>, QpuError> {
|
||||
let from_gate = config.get("gates").and_then(|g| g.as_array()).and_then(|gates| gates.iter().find(|g| g.get("name").and_then(|n| n.as_str()) == Some("cz"))).and_then(|g| g.get("coupling_map"));
|
||||
let map = from_gate.or_else(|| config.get("coupling_map")).ok_or_else(|| QpuError::Protocol("no coupling_map".into()))?;
|
||||
let list = map.as_array().ok_or_else(|| QpuError::Protocol("coupling_map is not a list".into()))?;
|
||||
list.iter()
|
||||
.map(|p| match p.as_array().map(|a| a.as_slice()) {
|
||||
Some([a, b]) => match (a.as_u64(), b.as_u64()) {
|
||||
(Some(a), Some(b)) if a <= u64::from(u16::MAX) && b <= u64::from(u16::MAX) => Ok((a as u16, b as u16)),
|
||||
_ => Err(QpuError::Protocol(format!("a coupling pair {p}"))),
|
||||
},
|
||||
_ => Err(QpuError::Protocol(format!("a coupling pair {p}"))),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl<T: Transport> Provider for SamplerV2<T> {
|
||||
fn device(&self) -> Result<Device, QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/backends/{}/configuration", self.base, self.backend)))?)?;
|
||||
let v = json_ok(&r)?;
|
||||
let n = field(&v, &["n_qubits"])?.as_u64().filter(|&n| n > 0 && n <= u64::from(u16::MAX)).ok_or_else(|| QpuError::Protocol("n_qubits".into()))?;
|
||||
let basis: Vec<&str> = field(&v, &["basis_gates"])?.as_array().map(|a| a.iter().filter_map(|g| g.as_str()).collect()).unwrap_or_default();
|
||||
for g in ["rz", "sx", "x", "cz"] {
|
||||
if !basis.contains(&g) {
|
||||
return Err(QpuError::Protocol(format!("{} has no `{g}` among its basis gates {basis:?}", self.backend)));
|
||||
}
|
||||
}
|
||||
let coupling = Coupling::new(n as u16, &pairs(&v)?).map_err(QpuError::Compile)?;
|
||||
Ok(Device { name: format!("sampler-v2:{}", self.backend), basis: Basis::RzSxXCz, coupling, provenance: Provenance::Silicon })
|
||||
}
|
||||
|
||||
fn submit(&self, program: &NativeProgram, shots: u64) -> Result<String, QpuError> {
|
||||
if program.basis != Basis::RzSxXCz {
|
||||
return Err(QpuError::Config("this service runs rz, sx, x and cz".into()));
|
||||
}
|
||||
let body = json!({
|
||||
"program_id": "sampler",
|
||||
"backend": self.backend,
|
||||
"params": {
|
||||
"version": 2,
|
||||
"pubs": [[program.qasm3(), null, shots]],
|
||||
"options": {
|
||||
"dynamical_decoupling": {"enable": false},
|
||||
"twirling": {"enable_gates": false, "enable_measure": false}
|
||||
}
|
||||
}
|
||||
});
|
||||
let r = self.transport.send(&self.authed(Request::post(format!("{}/jobs", self.base), body.to_string().into_bytes()).header("Content-Type", "application/json"))?)?;
|
||||
Ok(field_str(&json_ok(&r)?, &["id"])?.to_string())
|
||||
}
|
||||
|
||||
fn status(&self, id: &str) -> Result<JobStatus, QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/jobs/{id}?exclude_params=true", self.base)))?)?;
|
||||
let v = json_ok(&r)?;
|
||||
let status = v.get("state").and_then(|s| s.get("status")).or_else(|| v.get("status")).and_then(|s| s.as_str()).ok_or_else(|| QpuError::Protocol("no job status".into()))?;
|
||||
Ok(match status {
|
||||
"Queued" | "Running" => JobStatus::Pending,
|
||||
"Completed" => JobStatus::Done,
|
||||
other => JobStatus::Ended {
|
||||
status: other.to_string(),
|
||||
reason: v.get("state").and_then(|s| s.get("reason")).and_then(|r| r.as_str()).unwrap_or("").to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn counts(&self, id: &str, program: &NativeProgram, _shots: u64) -> Result<(Counts, JobInfo), QpuError> {
|
||||
let r = self.transport.send(&self.authed(Request::get(format!("{}/jobs/{id}/results", self.base)))?)?;
|
||||
let v = json_ok(&r)?;
|
||||
let samples = field(&v, &["results"])?
|
||||
.get(0)
|
||||
.and_then(|p| p.get("data"))
|
||||
.and_then(|d| d.get("c"))
|
||||
.and_then(|c| c.get("samples"))
|
||||
.and_then(|s| s.as_array())
|
||||
.ok_or_else(|| QpuError::Protocol("no results[0].data.c.samples".into()))?;
|
||||
let mut values = Vec::with_capacity(samples.len());
|
||||
for s in samples {
|
||||
let s = s.as_str().ok_or_else(|| QpuError::Protocol("a sample is not a string".into()))?;
|
||||
let hex = s.strip_prefix("0x").ok_or_else(|| QpuError::Protocol(format!("sample {s:?} is not hex")))?;
|
||||
values.push((u64::from_str_radix(hex, 16).map_err(|_| QpuError::Protocol(format!("sample {s:?} is not hex")))?, 1u64));
|
||||
}
|
||||
let counts = counts_from_integers(program.n_qubits, values).map_err(|e| QpuError::Counts(e.to_string()))?;
|
||||
Ok((counts, JobInfo::default()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_key_is_form_encoded() {
|
||||
assert_eq!(form_encode("a-Z_0.~"), "a-Z_0.~");
|
||||
assert_eq!(form_encode("a+b/c=d&e"), "a%2Bb%2Fc%3Dd%26e");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cz_gates_own_map_is_preferred() {
|
||||
let v = json!({"coupling_map": [[0, 1]], "gates": [{"name": "cz", "coupling_map": [[1, 2], [2, 1]]}]});
|
||||
assert_eq!(pairs(&v).unwrap(), vec![(1, 2), (2, 1)]);
|
||||
let v = json!({"coupling_map": [[0, 1]]});
|
||||
assert_eq!(pairs(&v).unwrap(), vec![(0, 1)]);
|
||||
assert!(pairs(&json!({"coupling_map": [[0]]})).is_err());
|
||||
assert!(pairs(&json!({})).is_err());
|
||||
}
|
||||
}
|
||||
263
wai/wai-quantum-qpu/src/transport.rs
Normal file
263
wai/wai-quantum-qpu/src/transport.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
//! How a client reaches a service: one HTTP request, one response.
|
||||
//!
|
||||
//! [`Curl`] runs `curl` as a program. The request's headers go in a curl
|
||||
//! configuration file that only this user can read, and its body on curl's
|
||||
//! standard input, so a key or token never appears on a command line, where
|
||||
//! other users of the host could read it; the file is removed when the request
|
||||
//! ends. [`Scripted`] answers from a function, for tests.
|
||||
|
||||
use crate::QpuError;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// An HTTP method the clients use.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Method {
|
||||
Get,
|
||||
Post,
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Method::Get => "GET",
|
||||
Method::Post => "POST",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One request.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Request {
|
||||
pub method: Method,
|
||||
pub url: String,
|
||||
pub headers: Vec<(String, String)>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn get(url: impl Into<String>) -> Self {
|
||||
Request { method: Method::Get, url: url.into(), headers: Vec::new(), body: None }
|
||||
}
|
||||
|
||||
pub fn post(url: impl Into<String>, body: Vec<u8>) -> Self {
|
||||
Request { method: Method::Post, url: url.into(), headers: Vec::new(), body: Some(body) }
|
||||
}
|
||||
|
||||
pub fn header(mut self, name: &str, value: impl Into<String>) -> Self {
|
||||
self.headers.push((name.to_string(), value.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// A header's value, by name, ignoring case.
|
||||
pub fn header_value(&self, name: &str) -> Option<&str> {
|
||||
self.headers.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(_, v)| v.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// One response: the status and the body.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub status: u16,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn json(status: u16, value: &serde_json::Value) -> Self {
|
||||
Response { status, body: value.to_string().into_bytes() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A way to send a request.
|
||||
pub trait Transport {
|
||||
fn send(&self, request: &Request) -> Result<Response, QpuError>;
|
||||
}
|
||||
|
||||
impl<T: Transport + ?Sized> Transport for &T {
|
||||
fn send(&self, request: &Request) -> Result<Response, QpuError> {
|
||||
(**self).send(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// `curl` as a program: see the module documentation.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Curl {
|
||||
program: PathBuf,
|
||||
timeout_secs: u32,
|
||||
}
|
||||
|
||||
impl Default for Curl {
|
||||
fn default() -> Self {
|
||||
Curl { program: PathBuf::from("curl"), timeout_secs: 120 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Curl {
|
||||
/// `curl` from the path, each request bounded to `timeout_secs`.
|
||||
pub fn new(timeout_secs: u32) -> Self {
|
||||
Curl { timeout_secs, ..Curl::default() }
|
||||
}
|
||||
|
||||
/// Another `curl`.
|
||||
pub fn with_program(mut self, program: impl Into<PathBuf>) -> Self {
|
||||
self.program = program.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A value quoted for a curl configuration file.
|
||||
fn quoted(value: &str) -> Result<String, QpuError> {
|
||||
if value.contains(['\n', '\r', '\0']) {
|
||||
return Err(QpuError::Config("a request header holds a line break".into()));
|
||||
}
|
||||
Ok(format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")))
|
||||
}
|
||||
|
||||
/// The curl configuration for a request: everything but the body.
|
||||
pub(crate) fn curl_config(request: &Request, timeout_secs: u32) -> Result<String, QpuError> {
|
||||
let mut c = String::new();
|
||||
c.push_str(&format!("url = {}\n", quoted(&request.url)?));
|
||||
c.push_str(&format!("request = {}\n", quoted(request.method.as_str())?));
|
||||
for (name, value) in &request.headers {
|
||||
c.push_str(&format!("header = {}\n", quoted(&format!("{name}: {value}"))?));
|
||||
}
|
||||
c.push_str(&format!("max-time = {timeout_secs}\n"));
|
||||
c.push_str("silent\nshow-error\n");
|
||||
c.push_str("write-out = \"\\n%{http_code}\"\n");
|
||||
if request.body.is_some() {
|
||||
c.push_str("data-binary = \"@-\"\n");
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// A file only this user can read, removed when dropped.
|
||||
struct Private(PathBuf);
|
||||
|
||||
impl Private {
|
||||
fn create(contents: &str) -> Result<Private, QpuError> {
|
||||
static N: AtomicU64 = AtomicU64::new(0);
|
||||
let n = N.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!("wai-qpu-{}-{n}.cfg", std::process::id()));
|
||||
let mut options = std::fs::OpenOptions::new();
|
||||
options.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
let mut f = options.open(&path).map_err(|e| QpuError::Transport(format!("{}: {e}", path.display())))?;
|
||||
let file = Private(path);
|
||||
f.write_all(contents.as_bytes()).map_err(|e| QpuError::Transport(e.to_string()))?;
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Private {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// The body and status of curl's output, which ends with a line holding the
|
||||
/// status (`write-out`).
|
||||
pub(crate) fn split_status(out: &[u8]) -> Result<Response, QpuError> {
|
||||
let at = out.iter().rposition(|&b| b == b'\n').ok_or_else(|| QpuError::Transport("curl wrote no status".into()))?;
|
||||
let status = std::str::from_utf8(&out[at + 1..]).ok().and_then(|s| s.trim().parse::<u16>().ok()).ok_or_else(|| QpuError::Transport("curl wrote no status".into()))?;
|
||||
if status == 0 {
|
||||
return Err(QpuError::Transport("no response".into()));
|
||||
}
|
||||
Ok(Response { status, body: out[..at].to_vec() })
|
||||
}
|
||||
|
||||
impl Transport for Curl {
|
||||
fn send(&self, request: &Request) -> Result<Response, QpuError> {
|
||||
let config = Private::create(&curl_config(request, self.timeout_secs)?)?;
|
||||
let mut child = Command::new(&self.program)
|
||||
.arg("--config")
|
||||
.arg(&config.0)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| QpuError::Transport(format!("{}: {e}", self.program.display())))?;
|
||||
let mut stdin = child.stdin.take().ok_or_else(|| QpuError::Transport("curl has no stdin".into()))?;
|
||||
if let Some(body) = &request.body {
|
||||
stdin.write_all(body).map_err(|e| QpuError::Transport(e.to_string()))?;
|
||||
}
|
||||
drop(stdin);
|
||||
let out = child.wait_with_output().map_err(|e| QpuError::Transport(e.to_string()))?;
|
||||
drop(config);
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
return Err(QpuError::Transport(format!("curl: {}", err.trim())));
|
||||
}
|
||||
split_status(&out.stdout)
|
||||
}
|
||||
}
|
||||
|
||||
/// A transport that answers each request from a function, recording every
|
||||
/// request it is sent: for tests.
|
||||
pub struct Scripted<F: Fn(&Request) -> Response> {
|
||||
answer: F,
|
||||
log: std::sync::Mutex<Vec<Request>>,
|
||||
}
|
||||
|
||||
impl<F: Fn(&Request) -> Response> Scripted<F> {
|
||||
pub fn new(answer: F) -> Self {
|
||||
Scripted { answer, log: std::sync::Mutex::new(Vec::new()) }
|
||||
}
|
||||
|
||||
/// Every request sent so far, in order.
|
||||
pub fn requests(&self) -> Vec<Request> {
|
||||
self.log.lock().unwrap_or_else(|p| p.into_inner()).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Fn(&Request) -> Response> Transport for Scripted<F> {
|
||||
fn send(&self, request: &Request) -> Result<Response, QpuError> {
|
||||
self.log.lock().unwrap_or_else(|p| p.into_inner()).push(request.clone());
|
||||
Ok((self.answer)(request))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_configuration_carries_headers_and_never_the_body() {
|
||||
let r = Request::post("https://example.invalid/a?b=c", b"{\"secret-body\":1}".to_vec()).header("Authorization", "Bearer t\"k\\n");
|
||||
let c = curl_config(&r, 30).unwrap();
|
||||
assert!(c.contains("url = \"https://example.invalid/a?b=c\"\n"));
|
||||
assert!(c.contains("request = \"POST\"\n"));
|
||||
assert!(c.contains("header = \"Authorization: Bearer t\\\"k\\\\n\"\n"), "{c}");
|
||||
assert!(c.contains("data-binary = \"@-\"\n"));
|
||||
assert!(!c.contains("secret-body"));
|
||||
assert!(curl_config(&Request::get("u").header("X", "a\nb"), 30).is_err());
|
||||
assert!(!curl_config(&Request::get("u"), 30).unwrap().contains("data-binary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_status_is_the_last_line() {
|
||||
assert_eq!(split_status(b"{\"a\":1}\n200").unwrap(), Response { status: 200, body: b"{\"a\":1}".to_vec() });
|
||||
assert_eq!(split_status(b"line\nline\n404").unwrap().body, b"line\nline".to_vec());
|
||||
assert_eq!(split_status(b"\n204").unwrap(), Response { status: 204, body: Vec::new() });
|
||||
assert!(split_status(b"no status").is_err());
|
||||
assert!(split_status(b"\n000").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_private_file_is_readable_by_its_owner_only_and_removed_after() {
|
||||
let f = Private::create("x").unwrap();
|
||||
let path = f.0.clone();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
|
||||
}
|
||||
drop(f);
|
||||
assert!(!path.exists());
|
||||
}
|
||||
}
|
||||
501
wai/wai-quantum-qpu/tests/clients.rs
Normal file
501
wai/wai-quantum-qpu/tests/clients.rs
Normal file
|
|
@ -0,0 +1,501 @@
|
|||
//! The clients against services that follow each provider's published
|
||||
//! contract: each fake parses the submitted program in that provider's own
|
||||
//! format, runs it on the crate's simulator, and answers in that provider's
|
||||
//! result shape and bit order. A client whose request, units or bit order
|
||||
//! departs from the contract reads back the wrong distribution, and these
|
||||
//! tests fail.
|
||||
//!
|
||||
//! The fakes encode the contracts as published; the first job on each real
|
||||
//! service is still [`probe_circuit`], which settles its bit order there.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::Mutex;
|
||||
use wai_quantum::quantum::{Circuit, Gateset};
|
||||
use wai_quantum::quantum_fabric::sampling::{QPU_DEFAULT, reference_distribution, sample_kernel};
|
||||
use wai_quantum::quantum_fabric::{FabricClass, Parity, Provenance};
|
||||
use wai_quantum::quantum_toolchain::from_qasm;
|
||||
use wai_quantum_qpu::sampler_v2::SamplerV2;
|
||||
use wai_quantum_qpu::native_turns::NativeTurns;
|
||||
use wai_quantum_qpu::prx_circuits::PrxCircuits;
|
||||
use wai_quantum_qpu::transport::{Method, Request, Response, Scripted, Transport};
|
||||
use wai_quantum_qpu::{Provider, QpuError, QpuSampler, Secret, Wait, check_probe, counts_from_probabilities, probe_circuit};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Running what a fake receives
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A radian angle that is a dyadic multiple of π, as the reader writes it.
|
||||
fn angle_text(rad: f64) -> String {
|
||||
if rad == 0.0 {
|
||||
return "0".into();
|
||||
}
|
||||
for j in 0..=40 {
|
||||
let m = rad / PI * 2f64.powi(j);
|
||||
if (m - m.round()).abs() < 1e-6 {
|
||||
let m = m.round() as i64;
|
||||
return if j == 0 { format!("{m}*pi") } else { format!("{m}*pi/{}", 1u64 << j) };
|
||||
}
|
||||
}
|
||||
panic!("the fake received an angle that is no dyadic multiple of π: {rad}")
|
||||
}
|
||||
|
||||
/// The probabilities of `width` physical qubits after `body`, a program in the
|
||||
/// gates the reader takes on `q[0..width]`.
|
||||
fn physical_distribution(width: usize, body: &str) -> Vec<f64> {
|
||||
let text = format!("OPENQASM 3.0;\nqubit[{width}] q;\n{body}");
|
||||
let c = from_qasm(&text).unwrap_or_else(|e| panic!("the fake cannot read the program: {e}\n{text}"));
|
||||
reference_distribution(&c).expect("the fake runs the program")
|
||||
}
|
||||
|
||||
/// Exact-as-possible counts over the classical register: physical outcome `x`
|
||||
/// gives `c[i] = bit measured[i] of x`.
|
||||
fn register_counts(p: &[f64], measured: &[usize], shots: u64) -> HashMap<u64, u64> {
|
||||
let mut reg = vec![0.0; 1 << measured.len()];
|
||||
for (x, &px) in p.iter().enumerate() {
|
||||
let r = measured.iter().enumerate().fold(0usize, |r, (i, &q)| r | (((x >> q) & 1) << i));
|
||||
reg[r] += px;
|
||||
}
|
||||
counts_from_probabilities(®, shots).into_iter().enumerate().filter(|&(_, k)| k > 0).map(|(r, k)| (r as u64, k)).collect()
|
||||
}
|
||||
|
||||
fn not_found(req: &Request) -> Response {
|
||||
Response::json(404, &json!({"error": format!("no route {} {}", req.method.as_str(), req.url)}))
|
||||
}
|
||||
|
||||
fn body_json(req: &Request) -> Value {
|
||||
serde_json::from_slice(req.body.as_deref().expect("a body")).expect("a JSON body")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sampler V2: an IAM token, then OpenQASM 3 in rz/sx/x/cz on `$p`
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SAMPLER_EDGES: [[u16; 2]; 7] = [[0, 1], [1, 2], [3, 4], [4, 5], [0, 3], [1, 4], [2, 5]];
|
||||
|
||||
struct SamplerV2Service {
|
||||
jobs: Mutex<HashMap<String, (u32, Vec<String>)>>,
|
||||
/// Write `c[0]` as the most significant bit: the wrong convention, for the
|
||||
/// probe's test.
|
||||
flip_bit_order: bool,
|
||||
fail_jobs: bool,
|
||||
}
|
||||
|
||||
impl SamplerV2Service {
|
||||
fn new() -> Self {
|
||||
SamplerV2Service { jobs: Mutex::new(HashMap::new()), flip_bit_order: false, fail_jobs: false }
|
||||
}
|
||||
|
||||
fn answer(&self, req: &Request) -> Response {
|
||||
if req.url == "https://iam.test/identity/token" {
|
||||
let body = String::from_utf8(req.body.clone().unwrap()).unwrap();
|
||||
assert!(body.starts_with("grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey&apikey="), "{body}"); // allow-name: a protocol grant type
|
||||
return Response::json(200, &json!({"access_token": "tok-1", "token_type": "Bearer", "expires_in": 3600, "expiration": 4_000_000_000u64}));
|
||||
}
|
||||
let authed = req.header_value("Authorization") == Some("Bearer tok-1") && req.header_value("Service-CRN") == Some("crn:test") && req.header_value(wai_quantum_qpu::sampler_v2::VERSION_HEADER) == Some("2026-04-15");
|
||||
if !authed {
|
||||
return Response::json(401, &json!({"error": "unauthorised"}));
|
||||
}
|
||||
let path = req.url.strip_prefix("https://sampler.test/api/v1").unwrap_or("");
|
||||
match (req.method, path) {
|
||||
(Method::Get, "/backends/test_device/configuration") => {
|
||||
Response::json(200, &json!({"backend_name": "test_device", "n_qubits": 6, "basis_gates": ["cz", "id", "rz", "sx", "x"], "coupling_map": [[0, 1]], "gates": [{"name": "cz", "coupling_map": SAMPLER_EDGES.iter().flat_map(|e| [[e[0], e[1]], [e[1], e[0]]]).collect::<Vec<_>>()}]}))
|
||||
}
|
||||
(Method::Post, "/jobs") => {
|
||||
let b = body_json(req);
|
||||
assert_eq!(b["program_id"], "sampler");
|
||||
assert_eq!(b["params"]["version"], 2);
|
||||
let pub0 = &b["params"]["pubs"][0];
|
||||
let (qasm, shots) = (pub0[0].as_str().unwrap(), pub0[2].as_u64().unwrap());
|
||||
assert!(pub0[1].is_null());
|
||||
let mut body = String::new();
|
||||
let mut measured: Vec<(usize, usize)> = Vec::new();
|
||||
for line in qasm.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("OPENQASM") || line.starts_with("include") || line.starts_with("bit[") || line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
assert!(!line.starts_with("gate "), "an ISA circuit defines no gate: {line}");
|
||||
if let Some(rest) = line.strip_prefix("c[") {
|
||||
let (i, p) = rest.split_once("] = measure $").expect("c[i] = measure $p;");
|
||||
measured.push((i.parse().unwrap(), p.trim_end_matches(';').parse().unwrap()));
|
||||
continue;
|
||||
}
|
||||
let name: String = line.chars().take_while(|c| c.is_ascii_alphanumeric()).collect();
|
||||
assert!(["rz", "sx", "x", "cz"].contains(&name.as_str()), "only basis gates: {line}");
|
||||
if name == "cz" {
|
||||
let qs: Vec<u16> = line[2..].trim_end_matches(';').split(',').map(|q| q.trim().trim_start_matches('$').parse().unwrap()).collect();
|
||||
assert!(SAMPLER_EDGES.iter().any(|e| (e[0], e[1]) == (qs[0], qs[1]) || (e[1], e[0]) == (qs[0], qs[1])), "cz on an unconnected pair: {line}");
|
||||
}
|
||||
body.push_str(&line.replace('$', "q[").replace(';', "];").replace(", q[", "],q["));
|
||||
body.push('\n');
|
||||
}
|
||||
measured.sort();
|
||||
let order: Vec<usize> = measured.iter().map(|&(_, p)| p).collect();
|
||||
let p = physical_distribution(6, &body);
|
||||
let n = order.len();
|
||||
let mut samples = Vec::new();
|
||||
for (reg, k) in register_counts(&p, &order, shots) {
|
||||
let v = if self.flip_bit_order { (0..n).fold(0u64, |v, i| v | (((reg >> i) & 1) << (n - 1 - i))) } else { reg };
|
||||
for _ in 0..k {
|
||||
samples.push(format!("0x{v:x}"));
|
||||
}
|
||||
}
|
||||
let id = format!("job-{}", self.jobs.lock().unwrap().len());
|
||||
self.jobs.lock().unwrap().insert(id.clone(), (0, samples));
|
||||
Response::json(200, &json!({"id": id, "backend": "test_device"}))
|
||||
}
|
||||
(Method::Get, p) if p.starts_with("/jobs/") && p.ends_with("/results") => {
|
||||
let id = &p["/jobs/".len()..p.len() - "/results".len()];
|
||||
let jobs = self.jobs.lock().unwrap();
|
||||
Response::json(200, &json!({"results": [{"data": {"c": {"samples": jobs[id].1}}, "metadata": {}}], "metadata": {"version": 2}}))
|
||||
}
|
||||
(Method::Get, p) if p.starts_with("/jobs/") => {
|
||||
let id = p["/jobs/".len()..].split('?').next().unwrap();
|
||||
let mut jobs = self.jobs.lock().unwrap();
|
||||
let job = jobs.get_mut(id).unwrap();
|
||||
job.0 += 1;
|
||||
let status = if self.fail_jobs { "Failed" } else if job.0 < 3 { "Queued" } else { "Completed" };
|
||||
Response::json(200, &json!({"id": id, "state": {"status": status, "reason": "the fake failed it"}}))
|
||||
}
|
||||
_ => not_found(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phased-X circuits: prx/cz in radians on named qubits, one measurement key per bit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct PrxService {
|
||||
jobs: Mutex<HashMap<String, (u32, Value)>>,
|
||||
}
|
||||
|
||||
const PRX_LOCI: [[&str; 2]; 5] = [["QB1", "QB2"], ["QB2", "QB3"], ["QB3", "QB4"], ["QB4", "QB5"], ["QB2", "QB5"]];
|
||||
|
||||
impl PrxService {
|
||||
fn answer(&self, req: &Request) -> Response {
|
||||
if req.header_value("Authorization") != Some("Bearer prx-token") {
|
||||
return Response::json(401, &json!({"detail": "unauthorised"}));
|
||||
}
|
||||
let path = req.url.strip_prefix("https://prx.test/api/v1").unwrap_or("");
|
||||
match (req.method, path) {
|
||||
(Method::Get, "/calibration-sets/test-qc/default/dynamic-quantum-architecture") => Response::json(
|
||||
200,
|
||||
&json!({"calibration_set_id": "00000000-0000-0000-0000-000000000000", "qubits": ["QB1", "QB2", "QB3", "QB4", "QB5"], "computational_resonators": [],
|
||||
"gates": {"prx": {"implementations": {"drag_gaussian": {"loci": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"]]}}, "default_implementation": "drag_gaussian"},
|
||||
"cz": {"implementations": {"tgss": {"loci": PRX_LOCI}}, "default_implementation": "tgss", "override_default_implementation": {}},
|
||||
"measure": {"implementations": {"constant": {"loci": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"]]}}}}}),
|
||||
),
|
||||
(Method::Post, "/jobs/test-qc/circuit") => {
|
||||
let b = body_json(req);
|
||||
let keys: Vec<&str> = b.as_object().unwrap().keys().map(|k| k.as_str()).collect();
|
||||
assert!(keys.iter().all(|k| ["circuits", "shots"].contains(k)), "no field the contract does not name: {keys:?}");
|
||||
let shots = b["shots"].as_u64().unwrap();
|
||||
let qb = |name: &Value| -> usize { name.as_str().unwrap().strip_prefix("QB").unwrap().parse::<usize>().unwrap() - 1 };
|
||||
let mut body = String::new();
|
||||
let mut measured: Vec<(usize, usize)> = Vec::new();
|
||||
for ins in b["circuits"][0]["instructions"].as_array().unwrap() {
|
||||
let locus = ins["locus"].as_array().unwrap();
|
||||
match ins["name"].as_str().unwrap() {
|
||||
"prx" => {
|
||||
let (theta, phi, q) = (ins["args"]["angle"].as_f64().unwrap(), ins["args"]["phase"].as_f64().unwrap(), qb(&locus[0]));
|
||||
// PRX(θ, φ) = RZ(φ)·RX(θ)·RZ(−φ).
|
||||
body.push_str(&format!("rz({}) q[{q}];\nrx({}) q[{q}];\nrz({}) q[{q}];\n", angle_text(-phi), angle_text(theta), angle_text(phi)));
|
||||
}
|
||||
"cz" => {
|
||||
let (a, c) = (locus[0].as_str().unwrap(), locus[1].as_str().unwrap());
|
||||
assert!(PRX_LOCI.iter().any(|l| (l[0], l[1]) == (a, c) || (l[1], l[0]) == (a, c)), "cz on an uncalibrated pair {a}, {c}");
|
||||
body.push_str(&format!("cz q[{}],q[{}];\n", qb(&locus[0]), qb(&locus[1])));
|
||||
}
|
||||
"measure" => {
|
||||
let key = ins["args"]["key"].as_str().unwrap();
|
||||
measured.push((key.strip_prefix('c').unwrap().parse().unwrap(), qb(&locus[0])));
|
||||
}
|
||||
other => panic!("not one of the service's instructions: {other}"),
|
||||
}
|
||||
}
|
||||
measured.sort();
|
||||
let order: Vec<usize> = measured.iter().map(|&(_, q)| q).collect();
|
||||
let n = order.len();
|
||||
let p = physical_distribution(5, &body);
|
||||
// Keys listed last bit first, so a client must use the order given.
|
||||
let keys: Vec<String> = (0..n).rev().map(|i| format!("c{i}")).collect();
|
||||
let mut counts = serde_json::Map::new();
|
||||
for (reg, k) in register_counts(&p, &order, shots) {
|
||||
let bits: String = (0..n).rev().map(|i| if (reg >> i) & 1 == 1 { '1' } else { '0' }).collect();
|
||||
counts.insert(bits, json!(k));
|
||||
}
|
||||
let id = format!("prx-{}", self.jobs.lock().unwrap().len());
|
||||
self.jobs.lock().unwrap().insert(id.clone(), (0, json!([{"measurement_keys": keys, "counts": counts}])));
|
||||
Response::json(200, &json!({"id": id, "type": "circuit", "status": "waiting"}))
|
||||
}
|
||||
(Method::Get, p) if p.ends_with("/artifacts/measurement_counts") => {
|
||||
let id = &p["/jobs/".len()..p.len() - "/artifacts/measurement_counts".len()];
|
||||
Response::json(200, &self.jobs.lock().unwrap()[id].1)
|
||||
}
|
||||
(Method::Get, p) if p.starts_with("/jobs/") => {
|
||||
let id = &p["/jobs/".len()..];
|
||||
let mut jobs = self.jobs.lock().unwrap();
|
||||
let job = jobs.get_mut(id).unwrap();
|
||||
job.0 += 1;
|
||||
Response::json(200, &json!({"id": id, "status": if job.0 < 2 { "processing" } else { "completed" }, "new_field": "ignored"}))
|
||||
}
|
||||
_ => not_found(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Native turns: gpi/gpi2/zz in turns, probabilities keyed by decimal integers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct TurnsService {
|
||||
jobs: Mutex<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
impl TurnsService {
|
||||
fn answer(&self, req: &Request) -> Response {
|
||||
if req.header_value("Authorization") != Some("apiKey turns-key") {
|
||||
return Response::json(401, &json!({"error": "unauthorised"}));
|
||||
}
|
||||
let path = req.url.strip_prefix("https://turns.test/v0.4").unwrap_or("");
|
||||
match (req.method, path) {
|
||||
(Method::Get, "/backends/simulator") => Response::json(200, &json!({"backend": "simulator", "status": "available", "qubits": 8, "supported_native_gates": ["gpi", "gpi2", "zz"]})),
|
||||
(Method::Post, "/jobs") => {
|
||||
let b = body_json(req);
|
||||
assert_eq!(b["type"], wai_quantum_qpu::native_turns::JOB_TYPE);
|
||||
assert_eq!(b["input"]["gateset"], "native");
|
||||
assert_eq!(b["settings"]["error_mitigation"]["debiasing"], false);
|
||||
let width = b["input"]["qubits"].as_u64().unwrap() as usize;
|
||||
let turns = |v: &Value| -> f64 {
|
||||
let t = v.as_f64().unwrap();
|
||||
assert!((-1.0..=1.0).contains(&t), "a phase in turns: {t}");
|
||||
t * 2.0 * PI
|
||||
};
|
||||
let mut body = String::new();
|
||||
for g in b["input"]["circuit"].as_array().unwrap() {
|
||||
match g["gate"].as_str().unwrap() {
|
||||
"gpi" | "gpi2" => {
|
||||
let (q, phi) = (g["target"].as_u64().unwrap(), turns(&g["phase"]));
|
||||
let rot = if g["gate"] == "gpi" { "x" } else { "rx(pi/2)" };
|
||||
body.push_str(&format!("rz({}) q[{q}];\n{rot} q[{q}];\nrz({}) q[{q}];\n", angle_text(-phi), angle_text(phi)));
|
||||
}
|
||||
"zz" => {
|
||||
let t = &g["targets"];
|
||||
let (a, c, theta) = (t[0].as_u64().unwrap(), t[1].as_u64().unwrap(), turns(&g["angle"]));
|
||||
// ZZ(θ) = exp(−iπθ·Z⊗Z) = CX·(I⊗RZ(2πθ))·CX.
|
||||
body.push_str(&format!("cx q[{a}],q[{c}];\nrz({}) q[{c}];\ncx q[{a}],q[{c}];\n", angle_text(theta)));
|
||||
}
|
||||
other => panic!("not a native gate: {other}"),
|
||||
}
|
||||
}
|
||||
let p = physical_distribution(width, &body);
|
||||
let probabilities: serde_json::Map<String, Value> = p.iter().enumerate().filter(|&(_, &x)| x > 1e-12).map(|(k, &x)| (k.to_string(), json!(x))).collect();
|
||||
let id = format!("turns-{}", self.jobs.lock().unwrap().len());
|
||||
self.jobs.lock().unwrap().insert(id.clone(), Value::Object(probabilities));
|
||||
Response::json(200, &json!({"id": id, "status": "submitted", "session_id": null}))
|
||||
}
|
||||
(Method::Get, p) if p.ends_with("/results/probabilities") => {
|
||||
let id = &p["/jobs/".len()..p.len() - "/results/probabilities".len()];
|
||||
Response::json(200, &self.jobs.lock().unwrap()[id])
|
||||
}
|
||||
(Method::Get, p) if p.starts_with("/jobs/") => Response::json(200, &json!({"id": &p["/jobs/".len()..], "status": "completed", "stats": {"kwh": 0.0, "qubits": 3}})),
|
||||
_ => not_found(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn no_wait<P: Provider>(s: QpuSampler<P>) -> QpuSampler<P> {
|
||||
s.with_wait(Wait { interval: std::time::Duration::ZERO, polls: 10 }, |_| {})
|
||||
}
|
||||
|
||||
/// Circuits whose counts must match their exact distribution.
|
||||
fn circuits() -> Vec<Circuit> {
|
||||
let mut ghz = Circuit::new(4);
|
||||
ghz.h(0).cx(0, 1).cx(1, 2).cx(2, 3);
|
||||
let mut mixed = Circuit::new(3);
|
||||
mixed.h(0).t(0).h(1).cx(0, 2).s(2).h(2).cz(1, 2).h(1);
|
||||
vec![probe_circuit(3), ghz, Circuit::qft(3), mixed]
|
||||
}
|
||||
|
||||
/// The classical fidelity of counts against the exact distribution.
|
||||
fn fidelity(c: &Circuit, counts: &[u64]) -> f64 {
|
||||
let p = reference_distribution(c).unwrap();
|
||||
let total: u64 = counts.iter().sum();
|
||||
p.iter().zip(counts).map(|(a, &k)| (a * k as f64 / total as f64).sqrt()).sum::<f64>().powi(2)
|
||||
}
|
||||
|
||||
fn exercise<P: Provider>(s: &QpuSampler<P>) {
|
||||
for c in circuits() {
|
||||
let (counts, record) = s.run(&c, 4000).unwrap_or_else(|e| panic!("{}: {e}", s.device().name));
|
||||
assert_eq!(counts.iter().sum::<u64>(), 4000);
|
||||
let f = fidelity(&c, &counts);
|
||||
assert!(f > 0.999, "{}: fidelity {f} on {c:?}", s.device().name);
|
||||
assert_eq!(record.shots, 4000);
|
||||
}
|
||||
let (probe, _) = s.run(&probe_circuit(3), 1000).unwrap();
|
||||
check_probe(&probe, 0.99).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sampler_v2_round_trips_through_its_contract() {
|
||||
let service = SamplerV2Service::new();
|
||||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||||
let client = SamplerV2::new(&transport, Secret::new("sampler-key/+="), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||||
assert_eq!(s.device().coupling.qubits(), 6);
|
||||
assert_eq!(s.device().provenance, Provenance::Silicon);
|
||||
exercise(&s);
|
||||
assert!(s.jobs().iter().all(|j| j.polls == 2), "{:?}", s.jobs());
|
||||
// The key goes to the token endpoint only, form-encoded, and the token is
|
||||
// minted once.
|
||||
let requests = transport.requests();
|
||||
let iam: Vec<&Request> = requests.iter().filter(|r| r.url.contains("iam.test")).collect();
|
||||
assert_eq!(iam.len(), 1);
|
||||
assert!(String::from_utf8_lossy(iam[0].body.as_ref().unwrap()).ends_with("apikey=sampler-key%2F%2B%3D"));
|
||||
for r in requests.iter().filter(|r| !r.url.contains("iam.test")) {
|
||||
let all = format!("{} {:?} {}", r.url, r.headers, String::from_utf8_lossy(r.body.as_deref().unwrap_or_default()));
|
||||
assert!(!all.contains("sampler-key"), "the key leaked into {}", r.url);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prx_circuits_round_trip_through_their_contract() {
|
||||
let service = PrxService { jobs: Mutex::new(HashMap::new()) };
|
||||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||||
let s = no_wait(QpuSampler::new(PrxCircuits::new(&transport, Secret::new("prx-token"), "test-qc").with_base("https://prx.test/api/v1")).unwrap());
|
||||
assert_eq!(s.device().coupling.qubits(), 5);
|
||||
exercise(&s);
|
||||
// A mock quantum computer is never silicon.
|
||||
let mock = PrxCircuits::new(&transport, Secret::new("prx-token"), "test-qc:mock").with_base("https://prx.test/api/v1");
|
||||
assert!(mock.device().is_err() || matches!(mock.device().unwrap().provenance, Provenance::Simulator(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_turns_round_trip_through_their_contract_exactly() {
|
||||
let service = TurnsService { jobs: Mutex::new(HashMap::new()) };
|
||||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||||
let s = no_wait(QpuSampler::new(NativeTurns::new(&transport, Secret::new("turns-key"), "simulator").with_base("https://turns.test/v0.4")).unwrap());
|
||||
assert!(matches!(&s.device().provenance, Provenance::Simulator(name) if name.contains("native-turns simulator")));
|
||||
exercise(&s);
|
||||
assert!(s.jobs().iter().all(|j| j.swaps == 0 && j.service_kwh == Some(0.0)));
|
||||
// Every phase on the wire is an exact binary fraction of a turn.
|
||||
for r in transport.requests().iter().filter(|r| r.method == Method::Post) {
|
||||
for g in body_json(r)["input"]["circuit"].as_array().unwrap() {
|
||||
let t = g.get("phase").or_else(|| g.get("angle")).unwrap().as_f64().unwrap();
|
||||
assert_eq!((t * 4294967296.0).fract(), 0.0, "{g}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_device_certified_through_the_fabric_carries_its_provenance() {
|
||||
let service = TurnsService { jobs: Mutex::new(HashMap::new()) };
|
||||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||||
let s = no_wait(QpuSampler::new(NativeTurns::new(&transport, Secret::new("turns-key"), "simulator").with_base("https://turns.test/v0.4")).unwrap());
|
||||
let member = s.member(QPU_DEFAULT, 2000).unwrap();
|
||||
assert_eq!((member.class, member.kernels.len()), (FabricClass::Qpu, 2));
|
||||
for (entry, gs) in member.kernels.iter().zip(Gateset::ALL) {
|
||||
assert_eq!(entry.kernel, sample_kernel(gs));
|
||||
assert_eq!(entry.certificate.provenance, s.device().provenance);
|
||||
let Parity::Statistical { fidelity, shots } = entry.certificate.parity else { panic!("{:?}", entry.certificate.parity) };
|
||||
assert_eq!(shots, 2000);
|
||||
assert!(fidelity > 0.999, "{gs:?}: {fidelity}");
|
||||
}
|
||||
assert!(member.id.starts_with("remote/qpu-native-turns-simulator-"), "{}", member.id);
|
||||
// The report names every certificate by its hash, and every job.
|
||||
let r = wai_quantum_qpu::report(&s, &member);
|
||||
assert_eq!(r["certificates"].as_array().unwrap().len(), 2);
|
||||
for (c, entry) in r["certificates"].as_array().unwrap().iter().zip(&member.kernels) {
|
||||
let hash: String = entry.certificate.hash().iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(c["hash"], hash.as_str());
|
||||
assert_eq!(c["parity"]["statistical"]["shots"], 2000);
|
||||
}
|
||||
assert_eq!(r["jobs"].as_array().unwrap().len(), 2 * QPU_DEFAULT.cases);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_probe_catches_a_service_with_the_other_bit_order() {
|
||||
let service = SamplerV2Service { flip_bit_order: true, ..SamplerV2Service::new() };
|
||||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||||
let client = SamplerV2::new(&transport, Secret::new("k"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||||
let (counts, _) = s.run(&probe_circuit(3), 500).unwrap();
|
||||
let e = check_probe(&counts, 0.99).unwrap_err();
|
||||
assert!(e.contains("the most on index 4"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_and_unfinished_jobs_are_errors() {
|
||||
let service = SamplerV2Service { fail_jobs: true, ..SamplerV2Service::new() };
|
||||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||||
let client = SamplerV2::new(&transport, Secret::new("k"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||||
assert!(matches!(s.run(&probe_circuit(2), 10), Err(QpuError::Job { status, .. }) if status == "Failed"));
|
||||
let service = SamplerV2Service::new();
|
||||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||||
let client = SamplerV2::new(&transport, Secret::new("k"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||||
let s = QpuSampler::new(client).unwrap().with_wait(Wait { interval: std::time::Duration::ZERO, polls: 1 }, |_| {});
|
||||
assert!(matches!(s.run(&probe_circuit(2), 10), Err(QpuError::Timeout { .. })));
|
||||
// A circuit wider than the device has no layout.
|
||||
assert!(matches!(s.run(&Circuit::new(7), 10), Err(QpuError::Compile(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rejected_request_is_an_http_error_with_the_body() {
|
||||
let transport = Scripted::new(|_: &Request| Response::json(403, &json!({"detail": "this token is not allowed here"})));
|
||||
let client = PrxCircuits::new(&transport, Secret::new("x"), "qc").with_base("https://prx.test/api/v1");
|
||||
match client.device() {
|
||||
Err(QpuError::Http { status: 403, body }) => assert!(body.contains("not allowed")),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
assert!(matches!(client.status("j"), Err(QpuError::Http { .. })));
|
||||
}
|
||||
|
||||
/// `curl` end to end against a local server: the headers arrive, the body
|
||||
/// arrives on its own, and the status comes back.
|
||||
#[test]
|
||||
fn curl_carries_a_request_to_a_server_and_back() {
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
if std::process::Command::new("curl").arg("--version").output().is_err() {
|
||||
println!("no curl on this host: the transport test runs where curl is");
|
||||
return;
|
||||
}
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let server = std::thread::spawn(move || {
|
||||
let (stream, _) = listener.accept().unwrap();
|
||||
let mut reader = BufReader::new(stream.try_clone().unwrap());
|
||||
let mut head = Vec::new();
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).unwrap();
|
||||
if line == "\r\n" || line.is_empty() {
|
||||
break;
|
||||
}
|
||||
head.push(line.trim_end().to_string());
|
||||
}
|
||||
let len: usize = head.iter().find_map(|h| h.to_ascii_lowercase().strip_prefix("content-length: ").map(|v| v.parse().unwrap())).unwrap_or(0);
|
||||
let mut body = vec![0u8; len];
|
||||
reader.read_exact(&mut body).unwrap();
|
||||
let reply = b"{\"id\":\"job-7\"}";
|
||||
let mut stream = stream;
|
||||
write!(stream, "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", reply.len()).unwrap();
|
||||
stream.write_all(reply).unwrap();
|
||||
(head, body)
|
||||
});
|
||||
let curl = wai_quantum_qpu::transport::Curl::new(20);
|
||||
let r = curl.send(&Request::post(format!("http://127.0.0.1:{port}/jobs"), b"{\"shots\":5}".to_vec()).header("Authorization", "Bearer t-1").header("Content-Type", "application/json")).unwrap();
|
||||
let (head, body) = server.join().unwrap();
|
||||
assert_eq!(r.status, 201);
|
||||
assert_eq!(r.body, b"{\"id\":\"job-7\"}");
|
||||
assert_eq!(head[0], "POST /jobs HTTP/1.1");
|
||||
assert!(head.iter().any(|h| h == "Authorization: Bearer t-1"), "{head:?}");
|
||||
assert_eq!(body, b"{\"shots\":5}");
|
||||
}
|
||||
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -589,7 +589,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
79
wai/wai-quantum/Cargo.lock
generated
79
wai/wai-quantum/Cargo.lock
generated
|
|
@ -144,6 +144,18 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "cl3"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ad5f4170f520224684c5c6e86e8d5474c2a10696e553b1db8389970cd82ece7"
|
||||
dependencies = [
|
||||
"dlopen2",
|
||||
"libc",
|
||||
"opencl-sys",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.13.1"
|
||||
|
|
@ -267,6 +279,29 @@ dependencies = [
|
|||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlopen2"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
|
||||
dependencies = [
|
||||
"dlopen2_derive",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlopen2_derive"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
|
|
@ -641,6 +676,25 @@ version = "1.21.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "opencl-sys"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c295aff010162eb6b1a59c758f0547b886c1edcbf324a41f48ee8f64dc48c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opencl3"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecb3473c4f01afd0eea3ee5b31649fec47dbc23353a63e1b916aa67cd810fb73"
|
||||
dependencies = [
|
||||
"cl3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "5.5.0"
|
||||
|
|
@ -1014,11 +1068,12 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"cudarc",
|
||||
"ed25519-dalek",
|
||||
"opencl3",
|
||||
"serde_json",
|
||||
"wgpu",
|
||||
]
|
||||
|
|
@ -1248,6 +1303,22 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
|
|
@ -1257,6 +1328,12 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -38,6 +38,9 @@ wgpu = { version = "30.0.1", optional = true, default-features = false, features
|
|||
# so a build with the feature needs no CUDA installed and finds no member where
|
||||
# there is none.
|
||||
cudarc = { version = "0.19.10", optional = true, default-features = false, features = ["std", "driver", "nvrtc", "dynamic-loading", "cuda-12080"] }
|
||||
# The OpenCL member: the ICD loader is opened when first used, so a build with the
|
||||
# feature needs no OpenCL installed and finds no member where there is none.
|
||||
opencl3 = { version = "0.12.3", optional = true, default-features = false, features = ["dynamic"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
wgpu = { version = "30.0.1", optional = true, default-features = false, features = ["std", "wgsl", "webgpu"] }
|
||||
|
|
@ -139,13 +142,16 @@ quantum_qbom = ["quantum_ops"]
|
|||
# The fabric: members, parity against the CPU reference, certification,
|
||||
# placement by joules, and a signed receipt naming where a result ran. With the
|
||||
# fixed-point core in 32-bit lanes, the kernel an integer-only fabric runs.
|
||||
quantum_fabric = ["quantum", "quantum_ops"]
|
||||
quantum_fabric = ["quantum", "quantum_ops", "quantum_toolchain"]
|
||||
# The WebGPU member: one WGSL kernel on Metal, Vulkan, DX12 or a browser's
|
||||
# WebGPU, certified bit-exact against the CPU reference on each adapter. Opt-in,
|
||||
# and not in `full`: it is the one feature that reaches a platform interface.
|
||||
quantum_fabric_webgpu = ["quantum_fabric", "dep:wgpu"]
|
||||
# The CUDA member of the fabric (an NVIDIA GPU, native targets only).
|
||||
quantum_fabric_cuda = ["quantum_fabric", "dep:cudarc"]
|
||||
quantum_fabric_opencl = ["quantum_fabric", "dep:opencl3"]
|
||||
# The simulated FPGA member: Verilator, invoked at run time, compiles the datapath.
|
||||
quantum_fabric_fpga = ["quantum_fabric"]
|
||||
|
||||
# --- interference / ML layer ----------------------------------------------
|
||||
quantum_vml = []
|
||||
|
|
@ -172,6 +178,22 @@ required-features = ["quantum_fabric_webgpu"]
|
|||
name = "cuda_member"
|
||||
required-features = ["quantum_fabric_cuda"]
|
||||
|
||||
[[test]]
|
||||
name = "cuda_sv"
|
||||
required-features = ["quantum_fabric_cuda", "quantum_sv"]
|
||||
|
||||
[[test]]
|
||||
name = "opencl_member"
|
||||
required-features = ["quantum_fabric_opencl"]
|
||||
|
||||
[[test]]
|
||||
name = "opencl_sv"
|
||||
required-features = ["quantum_fabric_opencl", "quantum_sv"]
|
||||
|
||||
[[test]]
|
||||
name = "fpga_member"
|
||||
required-features = ["quantum_fabric_fpga"]
|
||||
|
||||
[[test]]
|
||||
name = "fabric_expected"
|
||||
required-features = ["quantum_fabric"]
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ every stage.
|
|||
|
||||
No QPU, no cloud service, no vendor SDK, and **no system libraries** — so the
|
||||
same source runs natively, in the browser via wasm, and as a WASI component at
|
||||
the edge, producing **byte-identical results and receipts on all three**. Two
|
||||
opt-in features reach a platform interface: `quantum_fabric_webgpu` (the
|
||||
platform's GPU interface) and `quantum_fabric_cuda` (the CUDA driver and runtime
|
||||
compiler, loaded when first used); no default feature reaches a system library.
|
||||
the edge, producing **byte-identical results and receipts on all three**. Four
|
||||
opt-in features reach outside the crate: `quantum_fabric_webgpu` (the
|
||||
platform's GPU interface), `quantum_fabric_cuda` (the CUDA driver and runtime
|
||||
compiler) and `quantum_fabric_opencl` (the OpenCL ICD loader), the last two
|
||||
loaded when first used, and `quantum_fabric_fpga` (Verilator, run as a program
|
||||
when first used); no default feature reaches a system library.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
|
|
@ -58,7 +60,7 @@ wai-quantum qutrit # d = 3, not just qubits
|
|||
never creates information.
|
||||
|
||||
| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), as `wai.quantum.circuit2` by default or as the frozen `wai.quantum.circuit` |
|
||||
| `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded) — Bloch / entanglement / purity analysis |
|
||||
| `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded); **device-ready OpenQASM 2/3** (`device::device_qasm`): every gate lowered exactly to its dialect's standard library, multi-controlled gates included, every qubit measured into `c[i]`, and the service's counts read back onto the crate's basis; **native programs** (`native::compile`): a processor's own basis (`rz`/`sx`/`x`/`cz`; `prx`/`cz`; or `gpi`/`gpi2`/`zz` with angles in turns, written exactly; the Z rotations carried in the phases of the last two) on its own qubits, routed over its coupling map, and checked against the circuit by simulation before it is sent — Bloch / entanglement / purity analysis |
|
||||
| `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector |
|
||||
| `quantum_mps` | tensor-network (matrix-product-state) backend |
|
||||
| `quantum_pauli` | sparse-Pauli / Heisenberg observable propagation over the dyadic gate set (up to 64 qubits) |
|
||||
|
|
@ -95,8 +97,11 @@ never creates information.
|
|||
| `quantum_channel` | **transport itself**: Kraus channels (bit flip, dephasing, depolarizing, amplitude damping), and linear-inversion tomography to see what actually arrived — with the law that a channel can only *lose* information, never create it |
|
||||
| `quantum_qbom` | Quantum Bill of Materials — stage receipts into one manifest |
|
||||
| `quantum_vml`, `quantum_phasor`, `quantum_qfhrr`, `quantum_kernel`, `quantum_qdata`, `quantum_phasor_meter` | the interference / phasor ML layer |
|
||||
| `quantum_fabric_webgpu` | **the fabric's WebGPU member**, opt-in and not in `full`: the 32-bit-lane gate kernel in WGSL, one dispatch per gate, through Metal, Vulkan or DX12 natively and through a browser's WebGPU on `wasm32-unknown-unknown`; a second implementation applies runs of gates tile by tile, one dispatch per run, and is certified on its own. Each adapter is probed against the CPU's arithmetic in the process that uses it and certified bit-exact by reproducing the CPU reference's pinned corpus digests; a result that would leave 32 bits is refused, never returned. One of the two features that reach a platform interface |
|
||||
| `quantum_fabric_cuda` | **the fabric's CUDA member**, opt-in and not in `full`, native targets only: the gate kernel in CUDA C with exact 64-bit integers, compiled at run time for each device's own architecture, one launch per gate. The CUDA driver and runtime compiler are loaded when first used, so a build with the feature runs on a host without CUDA and finds no member there, saying why. Each device is probed against the CPU's arithmetic in the process that uses it and certified bit-exact by reproducing the CPU reference's pinned corpus digests; a result that would leave 32 bits is refused, never returned |
|
||||
| `quantum_fabric` | **the compute fabric**: members (the CPU and accelerators, quantum processors among them), each kernel implementation certified against the CPU reference in the process that uses it (bit-exact, within a stated tolerance, or statistical by classical fidelity over stated shots), with the certificate saying where it was earned (silicon, an emulator, or a simulator); placement by parity, provenance and metered energy; a signed receipt naming the member and implementation behind a result. Two kernels: apply (a circuit's state) and sample (its measurement counts), the latter with the CPU reference sampler, a corpus shaped for quantum processors, and a virtual quantum processor whose stated noise model lets every step of a processor's path run and be tested without one; its certificates say they were earned in a simulator |
|
||||
| `quantum_fabric_webgpu` | **the fabric's WebGPU member**, opt-in and not in `full`: the 32-bit-lane gate kernel in WGSL, one dispatch per gate, through Metal, Vulkan or DX12 natively and through a browser's WebGPU on `wasm32-unknown-unknown`; a second implementation applies runs of gates tile by tile, one dispatch per run, and is certified on its own. Each adapter is probed against the CPU's arithmetic in the process that uses it and certified bit-exact by reproducing the CPU reference's pinned corpus digests; a result that would leave 32 bits is refused, never returned. One of the features that reach outside the crate |
|
||||
| `quantum_fabric_cuda` | **the fabric's CUDA member**, opt-in and not in `full`, native targets only: the gate kernel in CUDA C with exact 64-bit integers, compiled at run time for each device's own architecture, one launch per gate. The CUDA driver and runtime compiler are loaded when first used, so a build with the feature runs on a host without CUDA and finds no member there, saying why. Each device is probed against the CPU's arithmetic in the process that uses it and certified bit-exact by reproducing the CPU reference's pinned corpus digests; a result that would leave 32 bits is refused, never returned. With `quantum_sv` too, the **dense f64 state vector** runs on the device bit for bit as on the CPU: the CPU's own launches (diagonal, one-qubit and grouped formulas), each an IEEE binary64 operation with no fused multiply-add, certified against a pinned corpus digest |
|
||||
| `quantum_fabric_opencl` | **the fabric's OpenCL member**, opt-in and not in `full`, native targets only: the same exact 64-bit kernel in OpenCL C, built at run time by each device's own compiler, one launch per gate, on any OpenCL GPU or CPU (an OpenCL accelerator is refused: no fabric class names it). The ICD loader is opened when first used, so a build with the feature runs on a host without OpenCL and finds no member there, saying why. Each device is probed and certified as the CUDA member's are. With `quantum_sv` too, the dense f64 state vector runs bit for bit as on the CPU on every device with binary64 (contraction off); a device without it refuses, saying so |
|
||||
| `quantum_fabric_fpga` | **the fabric's FPGA member, simulated**, opt-in and not in `full`, native targets only: the gate datapath in Verilog (each 32×32 product from four 16-bit partial products, the size of an FPGA's multiplier blocks; a state RAM; one pair per two cycles), compiled at run time by Verilator into a cycle-accurate simulation driven over a pipe. Probed and certified as the other members are, and every certificate says it was earned in a simulator, so placement ranks it after silicon. A host without Verilator finds no member and says why |
|
||||
| `full` | everything |
|
||||
|
||||
Default features are `quantum`, `quantum_toolchain`, `quantum_receipt`.
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@
|
|||
//! Rust, so the same source runs natively, in the browser via wasm, and as a
|
||||
//! WASI component at the edge, producing **byte-identical** results and receipts
|
||||
//! on all three. A fabric receipt is the exception by design: it names the
|
||||
//! member that ran the work. Two opt-in features reach a platform interface:
|
||||
//! `quantum_fabric_webgpu` (the platform's GPU interface) and
|
||||
//! `quantum_fabric_cuda` (the CUDA driver and runtime compiler, loaded when
|
||||
//! member that ran the work. Four opt-in features reach outside the crate:
|
||||
//! `quantum_fabric_webgpu` (the platform's GPU interface),
|
||||
//! `quantum_fabric_cuda` (the CUDA driver and runtime compiler) and
|
||||
//! `quantum_fabric_opencl` (the OpenCL ICD loader), the last two loaded when
|
||||
//! first used, and `quantum_fabric_fpga` (Verilator, run as a program when
|
||||
//! first used); no default feature reaches a system library.
|
||||
//!
|
||||
//! Everything is behind a feature; enable only what you use.
|
||||
|
|
@ -79,6 +81,10 @@ pub mod quantum_fabric;
|
|||
pub mod quantum_fabric_webgpu;
|
||||
#[cfg(all(feature = "quantum_fabric_cuda", not(target_arch = "wasm32")))]
|
||||
pub mod quantum_fabric_cuda;
|
||||
#[cfg(all(feature = "quantum_fabric_opencl", not(target_arch = "wasm32")))]
|
||||
pub mod quantum_fabric_opencl;
|
||||
#[cfg(all(feature = "quantum_fabric_fpga", not(target_arch = "wasm32")))]
|
||||
pub mod quantum_fabric_fpga;
|
||||
|
||||
#[cfg(feature = "quantum_control")]
|
||||
pub mod quantum_control;
|
||||
|
|
|
|||
|
|
@ -237,8 +237,9 @@ fn put(o: &mut Vec<u8>, bytes: &[u8]) {
|
|||
// Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A class of compute silicon.
|
||||
/// A class of compute silicon. More classes may come in a minor release.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum FabricClass {
|
||||
/// General-purpose cores and their vector units.
|
||||
Cpu,
|
||||
|
|
@ -544,7 +545,7 @@ fn ranked(members: &[Member], kernel: &str, required: &Parity) -> Vec<(usize, us
|
|||
}
|
||||
let key = |&(i, j): &(usize, usize)| {
|
||||
let (m, k) = (&members[i], &members[i].kernels[j]);
|
||||
(k.energy.is_none(), k.energy.as_ref().map_or(0, |e| e.0), m.id.as_str(), k.implementation != REFERENCE, k.implementation.as_str())
|
||||
(k.certificate.provenance.rank(), k.energy.is_none(), k.energy.as_ref().map_or(0, |e| e.0), m.id.as_str(), k.implementation != REFERENCE, k.implementation.as_str())
|
||||
};
|
||||
order.sort_by(|a, b| key(a).cmp(&key(b)));
|
||||
order
|
||||
|
|
@ -559,7 +560,7 @@ fn ranked_evenly(members: &[Member], kernel: &str, required: &Parity) -> Vec<(us
|
|||
if figured != 0 && figured != order.len() {
|
||||
let key = |&(i, j): &(usize, usize)| {
|
||||
let (m, k) = (&members[i], &members[i].kernels[j]);
|
||||
(m.id.as_str(), k.implementation != REFERENCE, k.implementation.as_str())
|
||||
(k.certificate.provenance.rank(), m.id.as_str(), k.implementation != REFERENCE, k.implementation.as_str())
|
||||
};
|
||||
order.sort_by(|a, b| key(a).cmp(&key(b)));
|
||||
}
|
||||
|
|
@ -574,7 +575,8 @@ fn ranked_for(members: &[Member], kernel: &str, required: &Parity, predict_pj: &
|
|||
match predicted {
|
||||
Some(pj) => {
|
||||
let mut by_energy: Vec<(u64, (usize, usize))> = pj.into_iter().zip(order).collect();
|
||||
by_energy.sort_by_key(|(pj, _)| *pj);
|
||||
// A prediction never lifts an emulated or simulated candidate above silicon.
|
||||
by_energy.sort_by_key(|&(pj, (i, j))| (members[i].kernels[j].certificate.provenance.rank(), pj));
|
||||
by_energy.into_iter().map(|(_, at)| at).collect()
|
||||
}
|
||||
None => order,
|
||||
|
|
@ -916,6 +918,64 @@ pub fn statistical(reference: &[f64], counts: &[u64]) -> Parity {
|
|||
Parity::Statistical { fidelity: (overlap * overlap).min(1.0), shots }
|
||||
}
|
||||
|
||||
/// Where a certificate was earned: on the silicon its member names, under an
|
||||
/// emulator that runs the member's own binary (user-mode QEMU, an
|
||||
/// instruction-set emulator, a binary translator), or in a simulator of the
|
||||
/// member (an RTL simulator, a vendor's device simulator, a noise model of a
|
||||
/// quantum processor).
|
||||
///
|
||||
/// An emulated or simulated certificate proves what that emulator or simulator
|
||||
/// computed, never what the silicon would. [`place`] ranks every silicon
|
||||
/// candidate before any emulated one, and every emulated one before any
|
||||
/// simulated one, so a stand-in never displaces the thing it stands in for.
|
||||
///
|
||||
/// A silicon certificate's hash is the hash it had before provenance existed:
|
||||
/// only an emulator or simulator is written into it.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum Provenance {
|
||||
#[default]
|
||||
Silicon,
|
||||
/// The emulator and its version, for instance `"qemu-user 8.2.2"`.
|
||||
Emulator(String),
|
||||
/// The simulator and its version, for instance `"verilator 5.052"`.
|
||||
Simulator(String),
|
||||
}
|
||||
|
||||
impl Provenance {
|
||||
/// `"silicon"`, `"emulator:<name>"` or `"simulator:<name>"`.
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Provenance::Silicon => "silicon".into(),
|
||||
Provenance::Emulator(n) => format!("emulator:{n}"),
|
||||
Provenance::Simulator(n) => format!("simulator:{n}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Placement's order: silicon, then emulators, then simulators.
|
||||
fn rank(&self) -> u8 {
|
||||
match self {
|
||||
Provenance::Silicon => 0,
|
||||
Provenance::Emulator(_) => 1,
|
||||
Provenance::Simulator(_) => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the certificate hash appends: nothing for silicon.
|
||||
fn write(&self, o: &mut Vec<u8>) {
|
||||
match self {
|
||||
Provenance::Silicon => {}
|
||||
Provenance::Emulator(n) => {
|
||||
o.push(1);
|
||||
put(o, n.as_bytes());
|
||||
}
|
||||
Provenance::Simulator(n) => {
|
||||
o.push(2);
|
||||
put(o, n.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a kernel implementation on a member has earned against the
|
||||
/// reference, and the corpus it earned it on.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
@ -935,12 +995,28 @@ pub struct Certificate {
|
|||
/// BLAKE3 over each case's input and both outputs, each length-prefixed,
|
||||
/// in order; zero for the reference.
|
||||
pub corpus: [u8; 32],
|
||||
/// Where it was earned ([`Provenance`]).
|
||||
pub provenance: Provenance,
|
||||
}
|
||||
|
||||
impl Certificate {
|
||||
/// A CPU reference's own standing: bit-exact by definition, on no corpus.
|
||||
pub fn reference(kernel: &str, member: &str) -> Certificate {
|
||||
Certificate { kernel: kernel.into(), implementation: REFERENCE.into(), member: member.into(), generator: "definition".into(), parity: Parity::BitExact, cases: 0, corpus: [0; 32] }
|
||||
Certificate {
|
||||
kernel: kernel.into(),
|
||||
implementation: REFERENCE.into(),
|
||||
member: member.into(),
|
||||
generator: "definition".into(),
|
||||
parity: Parity::BitExact,
|
||||
cases: 0,
|
||||
corpus: [0; 32],
|
||||
provenance: Provenance::Silicon,
|
||||
}
|
||||
}
|
||||
|
||||
/// The same certificate, earned under `provenance`.
|
||||
pub fn with_provenance(self, provenance: Provenance) -> Certificate {
|
||||
Certificate { provenance, ..self }
|
||||
}
|
||||
|
||||
/// The content hash a receipt binds.
|
||||
|
|
@ -952,6 +1028,7 @@ impl Certificate {
|
|||
self.parity.write_signed(&mut o);
|
||||
o.extend_from_slice(&self.cases.to_be_bytes());
|
||||
o.extend_from_slice(&self.corpus);
|
||||
self.provenance.write(&mut o);
|
||||
content_hash(&o)
|
||||
}
|
||||
}
|
||||
|
|
@ -1004,6 +1081,7 @@ impl Certification {
|
|||
parity: self.parity.unwrap_or(Parity::Uncertified),
|
||||
cases: self.cases,
|
||||
corpus: *self.hasher.finalize().as_bytes(),
|
||||
provenance: Provenance::Silicon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1110,6 +1188,7 @@ pub fn certify_sampling(kernel: &str, implementation: &str, member: &str, genera
|
|||
parity: parity.unwrap_or(Parity::Uncertified),
|
||||
cases: n,
|
||||
corpus: *h.finalize().as_bytes(),
|
||||
provenance: Provenance::Silicon,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1956,7 +2035,13 @@ pub mod lanes {
|
|||
}
|
||||
|
||||
pub mod cuda;
|
||||
pub mod fpga;
|
||||
pub mod opencl;
|
||||
pub mod plan;
|
||||
pub mod sampling;
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
pub mod sv64;
|
||||
pub mod vqpu;
|
||||
pub mod wgsl;
|
||||
|
||||
/// The fixed-point one is a 32-bit lane value.
|
||||
|
|
@ -1968,6 +2053,40 @@ mod tests {
|
|||
use crate::quantum::{BaseGate, Gate, Gateset, fxmul};
|
||||
use crate::quantum_energy::{CounterFiltering, Uncertainty};
|
||||
|
||||
/// A silicon certificate hashes as every certificate did before
|
||||
/// provenance existed, so no receipt sealed before it changes; an emulator
|
||||
/// or a simulator is written into the hash, and the two differ.
|
||||
#[test]
|
||||
fn provenance_leaves_a_silicon_certificates_hash_as_it_was() {
|
||||
let c = Certificate {
|
||||
kernel: KERNEL_CIRCUIT2_APPLY.into(),
|
||||
implementation: "impl".into(),
|
||||
member: "local/x".into(),
|
||||
generator: "g".into(),
|
||||
parity: Parity::BitExact,
|
||||
cases: 3,
|
||||
corpus: [9; 32],
|
||||
provenance: Provenance::Silicon,
|
||||
};
|
||||
let mut o = DOMAIN_CERT.to_vec();
|
||||
for s in [&c.kernel, &c.implementation, &c.member, &c.generator] {
|
||||
put(&mut o, s.as_bytes());
|
||||
}
|
||||
c.parity.write_signed(&mut o);
|
||||
o.extend_from_slice(&c.cases.to_be_bytes());
|
||||
o.extend_from_slice(&c.corpus);
|
||||
assert_eq!(c.hash(), content_hash(&o), "the pre-provenance encoding");
|
||||
let emulated = c.clone().with_provenance(Provenance::Emulator("qemu-user 8.2".into()));
|
||||
let simulated = c.clone().with_provenance(Provenance::Simulator("qemu-user 8.2".into()));
|
||||
assert_ne!(emulated.hash(), c.hash());
|
||||
assert_ne!(simulated.hash(), c.hash());
|
||||
assert_ne!(emulated.hash(), simulated.hash());
|
||||
assert_ne!(emulated.hash(), c.clone().with_provenance(Provenance::Emulator("qemu-user 8.3".into())).hash());
|
||||
assert_eq!(Certificate::reference("k", "m").provenance, Provenance::Silicon);
|
||||
assert_eq!(Provenance::default(), Provenance::Silicon);
|
||||
assert_eq!((Provenance::Silicon.rank(), emulated.provenance.rank(), simulated.provenance.rank()), (0, 1, 2));
|
||||
}
|
||||
|
||||
fn next(s: &mut u64) -> u64 {
|
||||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = *s;
|
||||
|
|
@ -2194,7 +2313,7 @@ mod tests {
|
|||
KernelEntry {
|
||||
kernel: KERNEL_CIRCUIT_APPLY.into(),
|
||||
implementation: implementation.into(),
|
||||
certificate: Certificate { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: implementation.into(), member: member.into(), generator: "g".into(), parity, cases: 1, corpus: [1; 32] },
|
||||
certificate: Certificate { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: implementation.into(), member: member.into(), generator: "g".into(), parity, cases: 1, corpus: [1; 32], provenance: Provenance::Silicon },
|
||||
energy: nj.map(|e| (e, EnergyClass::ModelBased)),
|
||||
}
|
||||
}
|
||||
|
|
@ -2837,7 +2956,7 @@ mod tests {
|
|||
.into_iter()
|
||||
.map(|gs| {
|
||||
let kernel = apply_kernel(gs);
|
||||
let certificate = Certificate { kernel: kernel.into(), implementation: implementation.into(), member: id.into(), generator: FABRIC_DEFAULT.generator(gs), parity: Parity::BitExact, cases: 224, corpus: [7; 32] };
|
||||
let certificate = Certificate { kernel: kernel.into(), implementation: implementation.into(), member: id.into(), generator: FABRIC_DEFAULT.generator(gs), parity: Parity::BitExact, cases: 224, corpus: [7; 32], provenance: Provenance::Silicon };
|
||||
KernelEntry { kernel: kernel.into(), implementation: implementation.into(), certificate, energy: nj.map(|e| (e, counter(50_000, 1_000))) }
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
209
wai/wai-quantum/src/quantum_fabric/fpga.rs
Normal file
209
wai/wai-quantum/src/quantum_fabric/fpga.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
//! The gate datapath for an FPGA, and what names its simulated member: the
|
||||
//! datapath's Verilog (`lanes64.v`), the harness that drives its simulation
|
||||
//! (`lanes64_harness.cpp`), the implementation id, the simulator's identity
|
||||
//! and the mutants a conforming build must be caught running. Nothing is built
|
||||
//! or run here, so all of it is testable on any host; the member itself is
|
||||
//! `quantum_fabric_fpga`.
|
||||
//!
|
||||
//! **Why the datapath is bit-exact.** It is the same transcription of
|
||||
//! [`lanes::exact_row`] and [`lanes::apply_words`] as the CUDA and OpenCL
|
||||
//! kernels, in logic: each 32×32 product is assembled from four 16-bit partial
|
||||
//! products (the size of an FPGA's multiplier blocks), exactly, in 64 bits;
|
||||
//! `fx(a, b) = (a·b + 2^29) >>> 30` and each row's sum of four are exact in 64
|
||||
//! bits; the lane keeps each sum's low 32 bits, and a sum outside 32 bits
|
||||
//! records the gate's op-log index, the lowest winning. A gate visits every
|
||||
//! pair in order, two cycles a pair, each pair's two addresses its own.
|
||||
//!
|
||||
//! **What the member is.** A cycle-accurate simulation of that datapath,
|
||||
//! compiled by Verilator: its certificates are earned in a simulator
|
||||
//! ([`Provenance::Simulator`]) and say so, and placement ranks them after
|
||||
//! silicon. The day the datapath runs on a board, the same corpus certifies it
|
||||
//! as silicon.
|
||||
//!
|
||||
//! [`lanes::exact_row`]: super::lanes
|
||||
//! [`lanes::apply_words`]: super::lanes::apply_words
|
||||
|
||||
use super::Provenance;
|
||||
use super::wgsl::{Detector, Mutant};
|
||||
|
||||
/// The datapath, `lanes64.v`, exactly as it is compiled.
|
||||
pub const RTL: &str = include_str!("lanes64.v");
|
||||
|
||||
/// The simulation's harness, `lanes64_harness.cpp`: it moves bytes and toggles
|
||||
/// the clock, and computes nothing.
|
||||
pub const HARNESS: &str = include_str!("lanes64_harness.cpp");
|
||||
|
||||
/// The implementation name the id starts with.
|
||||
pub const IMPLEMENTATION: &str = "fpga-lanes64";
|
||||
|
||||
/// Version of the host-side layout: the harness's protocol (probe and run
|
||||
/// requests, little-endian), the lane layout `{imaginary, real}`, the report
|
||||
/// word (`u32::MAX` for none) and the RAM's address width
|
||||
/// ([`ADDRESS_BITS`]). Bumped on any change to them; it is part of the
|
||||
/// implementation id.
|
||||
pub const HOST_LAYOUT: u32 = 1;
|
||||
|
||||
/// The state RAM's address width: `2^ADDRESS_BITS` lanes, so at most that many
|
||||
/// qubits.
|
||||
pub const ADDRESS_BITS: u8 = 20;
|
||||
|
||||
/// The options Verilator compiles the datapath and the harness with, besides
|
||||
/// the output paths.
|
||||
pub const VERILATOR_OPTIONS: [&str; 10] = ["--cc", "--exe", "--build", "-O3", "-Wno-fatal", "-Wno-lint", "-Wno-style", "-GAW=20", "-CFLAGS", "-O2 -DWQ_AW=20"];
|
||||
|
||||
const DOMAIN_IMPLEMENTATION: &[u8] = b"wai:quantum-fabric-fpga\x01";
|
||||
const DOMAIN_MEMBER: &[u8] = b"wai:quantum-fabric-fpga-member\x01";
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
/// A length-prefixed byte string, so that no field reads as part of another.
|
||||
fn put(o: &mut Vec<u8>, bytes: &[u8]) {
|
||||
o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
|
||||
o.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
/// `"fpga-lanes64/<16 hex>"`: the first 8 bytes of
|
||||
/// BLAKE3(`"wai:quantum-fabric-fpga\x01"` ‖ put([`RTL`]) ‖ put([`HARNESS`]) ‖
|
||||
/// put of each of [`VERILATOR_OPTIONS`] ‖ [`HOST_LAYOUT`] as a big-endian
|
||||
/// `u32`), in hex. Any edit to the datapath, the harness, the options or the
|
||||
/// host layout changes it. The simulator and its compiler are part of the
|
||||
/// member id instead.
|
||||
pub fn implementation_id() -> String {
|
||||
implementation_id_of(RTL)
|
||||
}
|
||||
|
||||
/// [`implementation_id`] for another datapath text, such as a mutant's.
|
||||
pub fn implementation_id_of(rtl: &str) -> String {
|
||||
let mut o = DOMAIN_IMPLEMENTATION.to_vec();
|
||||
put(&mut o, rtl.as_bytes());
|
||||
put(&mut o, HARNESS.as_bytes());
|
||||
for option in VERILATOR_OPTIONS {
|
||||
put(&mut o, option.as_bytes());
|
||||
}
|
||||
o.extend_from_slice(&HOST_LAYOUT.to_be_bytes());
|
||||
format!("{IMPLEMENTATION}/{}", hex(&blake3::hash(&o).as_bytes()[..8]))
|
||||
}
|
||||
|
||||
/// What a simulated FPGA member is, as its id hashes it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SimIdentity {
|
||||
/// The simulator's version line, as `verilator --version` prints it.
|
||||
pub simulator: String,
|
||||
/// The C++ compiler's version line.
|
||||
pub compiler: String,
|
||||
/// The host's architecture and operating system.
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
impl SimIdentity {
|
||||
/// `"local/fpga-sim-<d12>"`. `d12` is the first 12 hex digits of
|
||||
/// BLAKE3(`"wai:quantum-fabric-fpga-member\x01"` ‖ put(simulator) ‖
|
||||
/// put(compiler) ‖ put(host) ‖ [`ADDRESS_BITS`]). It sorts after every
|
||||
/// `local/cpu-<arch>`.
|
||||
pub fn member_id(&self) -> String {
|
||||
let mut o = DOMAIN_MEMBER.to_vec();
|
||||
put(&mut o, self.simulator.as_bytes());
|
||||
put(&mut o, self.compiler.as_bytes());
|
||||
put(&mut o, self.host.as_bytes());
|
||||
o.push(ADDRESS_BITS);
|
||||
format!("local/fpga-sim-{}", hex(&blake3::hash(&o).as_bytes()[..6]))
|
||||
}
|
||||
|
||||
/// The key a runner declares in CI: `"fpga-sim-verilator"`.
|
||||
pub fn key(&self) -> String {
|
||||
"fpga-sim-verilator".into()
|
||||
}
|
||||
|
||||
/// The simulator's version number: the second word of its version line,
|
||||
/// `"5.052"` from `"Verilator 5.052 2026-09-05 rev …"`.
|
||||
pub fn version(&self) -> &str {
|
||||
self.simulator.split_whitespace().nth(1).unwrap_or("")
|
||||
}
|
||||
|
||||
/// `Simulator("verilator <version>")`: what every certificate this member
|
||||
/// earns says it was earned in.
|
||||
pub fn provenance(&self) -> Provenance {
|
||||
Provenance::Simulator(format!("verilator {}", self.version()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The mutants: textual edits of [`RTL`] that a conforming build must be
|
||||
/// caught running, each by the detector named. Each `find` occurs exactly once
|
||||
/// in [`RTL`].
|
||||
pub const MUTANTS: [Mutant; 6] = [
|
||||
// The rounding constant is half what it should be.
|
||||
Mutant { name: "F1", find: "assign fx = (prod + 64'sd536870912) >>> 30;", replace: "assign fx = (prod + 64'sd268435456) >>> 30;", detector: Detector::Probe },
|
||||
// The product is shifted by one bit too few.
|
||||
Mutant { name: "F2", find: "assign fx = (prod + 64'sd536870912) >>> 30;", replace: "assign fx = (prod + 64'sd536870912) >>> 29;", detector: Detector::Probe },
|
||||
// A low half is taken as signed in its partial product.
|
||||
Mutant { name: "F3", find: "wire signed [32:0] hl = ah * $signed({1'b0, bl});", replace: "wire signed [32:0] hl = ah * $signed(bl);", detector: Detector::Probe },
|
||||
// The control test is inverted.
|
||||
Mutant { name: "F4", find: "wire selected = ((i_ix & ctrl[AW:0]) == ctrl[AW:0]);", replace: "wire selected = ((i_ix & ctrl[AW:0]) != ctrl[AW:0]);", detector: Detector::Certificate },
|
||||
// A sign of row 0's real part is wrong, on the imaginary part of the
|
||||
// top-right entry, which `Y` has.
|
||||
Mutant { name: "F5", find: "wire signed [63:0] s0r = p0r0 - p1i0 + p2r1 - p3i1;", replace: "wire signed [63:0] s0r = p0r0 - p1i0 + p2r1 + p3i1;", detector: Detector::Certificate },
|
||||
// The range guard never fires.
|
||||
Mutant { name: "F6", find: "if (oor && index < report) begin", replace: "if (1'b0) begin", detector: Detector::Range },
|
||||
];
|
||||
|
||||
/// [`RTL`] with `m` applied. Panics unless `m.find` occurs exactly once.
|
||||
pub fn mutate(m: &Mutant) -> String {
|
||||
let n = RTL.matches(m.find).count();
|
||||
assert_eq!(n, 1, "mutant {}: `{}` occurs {n} times in the datapath", m.name, m.find);
|
||||
RTL.replacen(m.find, m.replace, 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn identity() -> SimIdentity {
|
||||
SimIdentity { simulator: "Verilator 5.052 2026-09-05 rev v5.052".into(), compiler: "a compiler 21.0".into(), host: "aarch64-macos".into() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_field_of_the_identity_is_in_the_member_id() {
|
||||
let base = identity();
|
||||
let id = base.member_id();
|
||||
assert!(id.starts_with("local/fpga-sim-") && id.len() == "local/fpga-sim-".len() + 12, "{id}");
|
||||
assert!(id.as_str() > "local/cpu-x86_64" && id.as_str() > "local/cpu-aarch64");
|
||||
for v in [
|
||||
SimIdentity { simulator: "Verilator 5.054".into(), ..identity() },
|
||||
SimIdentity { compiler: "another".into(), ..identity() },
|
||||
SimIdentity { host: "x86_64-linux".into(), ..identity() },
|
||||
] {
|
||||
assert_ne!(v.member_id(), id, "{v:?}");
|
||||
}
|
||||
assert_eq!(base.key(), "fpga-sim-verilator");
|
||||
assert_eq!(base.version(), "5.052");
|
||||
assert_eq!(base.provenance(), Provenance::Simulator("verilator 5.052".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_implementation_id_follows_the_datapath_and_the_harness() {
|
||||
let id = implementation_id();
|
||||
assert!(id.starts_with("fpga-lanes64/") && id.len() == "fpga-lanes64/".len() + 16, "{id}");
|
||||
for m in &MUTANTS {
|
||||
assert_ne!(implementation_id_of(&mutate(m)), id, "mutant {}", m.name);
|
||||
}
|
||||
// The address width is one number in two places: the RTL parameter and
|
||||
// the harness's bound.
|
||||
assert!(VERILATOR_OPTIONS.contains(&"-GAW=20") && VERILATOR_OPTIONS.contains(&"-O2 -DWQ_AW=20"));
|
||||
assert_eq!(ADDRESS_BITS, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_mutant_edits_the_datapath_once_and_names_distinct_edits() {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for m in &MUTANTS {
|
||||
let edited = mutate(m);
|
||||
assert_ne!(edited, RTL, "mutant {} changes nothing", m.name);
|
||||
assert!(seen.insert(edited), "mutant {} duplicates another", m.name);
|
||||
}
|
||||
for d in [Detector::Probe, Detector::Certificate, Detector::Range] {
|
||||
assert!(MUTANTS.iter().any(|m| m.detector == d), "no mutant for {d:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
95
wai/wai-quantum/src/quantum_fabric/lanes64.cl
Normal file
95
wai/wai-quantum/src/quantum_fabric/lanes64.cl
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// wai-quantum fabric: the gate kernel for OpenCL devices.
|
||||
//
|
||||
// A transcription of `lanes::exact_row` and `lanes::apply_words` in 64-bit
|
||||
// integers, which OpenCL C's `long` is on every full-profile device. Amplitudes
|
||||
// are pairs of 32-bit fixed-point values (30 fractional bits). Each product
|
||||
// `fx(a, b) = (a·b + 2^29) >> 30` is exact in 64 bits (a 32×32 product is at
|
||||
// most 2^62 in size), and so is each row's sum of four products. The lane
|
||||
// stores the low 32 bits of each sum; a sum outside 32 bits records the
|
||||
// gate's op-log index in `report[0]`, the lowest such index winning, so the
|
||||
// host refuses the run with the gate the CPU reference names.
|
||||
//
|
||||
// Nothing here is floating point, depends on the order work-items run in, or
|
||||
// reads an amplitude another work-item of the same launch writes: each
|
||||
// work-item owns one pair (i, j), and a launch is one gate.
|
||||
//
|
||||
// OpenCL C fills the vacated bits of `>>` on a negative signed value with ones,
|
||||
// as Rust's `>>` on i64 does; the host's probe checks this, among the rest of
|
||||
// the arithmetic, before a device is a member.
|
||||
|
||||
typedef long i64;
|
||||
typedef ulong u64;
|
||||
|
||||
// The exact fixed-point product.
|
||||
inline i64 fx(int a, int b) {
|
||||
return ((i64)a * (i64)b + ((i64)1 << 29)) >> 30;
|
||||
}
|
||||
|
||||
// The low 32 bits of `v`, as the lane keeps them.
|
||||
inline int low32(i64 v) {
|
||||
return (int)(uint)(u64)v;
|
||||
}
|
||||
|
||||
// Whether `v` fits a 32-bit lane.
|
||||
inline int fits32(i64 v) {
|
||||
return v >= -(i64)2147483648L && v <= (i64)2147483647L;
|
||||
}
|
||||
|
||||
// One gate on a state of `2 * pairs` lanes, one work-item per pair.
|
||||
//
|
||||
// `m0..m7` are the gate's words: row 0 is (m0 + i·m1)·a0 + (m2 + i·m3)·a1,
|
||||
// row 1 is (m4 + i·m5)·a0 + (m6 + i·m7)·a1. `tbit` is 1 << target; `ctrl`
|
||||
// the OR of the controls' bits. `index` is the gate's position in the op
|
||||
// log. The host checks the words before a launch (`apply_words` refuses the
|
||||
// same malformed words), so the kernel trusts them.
|
||||
__kernel void apply_gate(
|
||||
__global int2* restrict amps,
|
||||
ulong pairs,
|
||||
uint tbit,
|
||||
uint ctrl,
|
||||
int m0, int m1, int m2, int m3, int m4, int m5, int m6, int m7,
|
||||
uint index,
|
||||
__global uint* restrict report)
|
||||
{
|
||||
u64 p = (u64)get_global_id(0);
|
||||
u64 stride = (u64)get_global_size(0);
|
||||
for (; p < pairs; p += stride) {
|
||||
// The p-th index whose target bit is clear: p with a zero inserted at
|
||||
// the target's position.
|
||||
u64 low = p & ((u64)tbit - 1);
|
||||
u64 i = ((p - low) << 1) | low;
|
||||
if ((i & (u64)ctrl) != (u64)ctrl) {
|
||||
continue;
|
||||
}
|
||||
u64 j = i | (u64)tbit;
|
||||
int2 a0 = amps[i];
|
||||
int2 a1 = amps[j];
|
||||
i64 o0r = fx(m0, a0.x) - fx(m1, a0.y) + fx(m2, a1.x) - fx(m3, a1.y);
|
||||
i64 o0i = fx(m0, a0.y) + fx(m1, a0.x) + fx(m2, a1.y) + fx(m3, a1.x);
|
||||
i64 o1r = fx(m4, a0.x) - fx(m5, a0.y) + fx(m6, a1.x) - fx(m7, a1.y);
|
||||
i64 o1i = fx(m4, a0.y) + fx(m5, a0.x) + fx(m6, a1.y) + fx(m7, a1.x);
|
||||
if (!(fits32(o0r) && fits32(o0i) && fits32(o1r) && fits32(o1i))) {
|
||||
atomic_min(&report[0], index);
|
||||
}
|
||||
amps[i] = (int2)(low32(o0r), low32(o0i));
|
||||
amps[j] = (int2)(low32(o1r), low32(o1i));
|
||||
}
|
||||
}
|
||||
|
||||
// The arithmetic probe: for each case (a, b), the 64-bit two's-complement
|
||||
// product a·b and the exact fx(a, b), each as (high, low) words, the outputs
|
||||
// of `lanes::mul_wide` and `lanes::fxmul_wide`.
|
||||
__kernel void probe(
|
||||
__global const int2* restrict cases,
|
||||
__global uint4* restrict out,
|
||||
ulong count)
|
||||
{
|
||||
u64 k = (u64)get_global_id(0);
|
||||
u64 stride = (u64)get_global_size(0);
|
||||
for (; k < count; k += stride) {
|
||||
int2 c = cases[k];
|
||||
u64 prod = (u64)((i64)c.x * (i64)c.y);
|
||||
u64 f = (u64)fx(c.x, c.y);
|
||||
out[k] = (uint4)((uint)(prod >> 32), (uint)prod, (uint)(f >> 32), (uint)f);
|
||||
}
|
||||
}
|
||||
186
wai/wai-quantum/src/quantum_fabric/lanes64.v
Normal file
186
wai/wai-quantum/src/quantum_fabric/lanes64.v
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
// wai-quantum fabric: the gate datapath for an FPGA.
|
||||
//
|
||||
// A transcription of `lanes::exact_row` and `lanes::apply_words` into logic.
|
||||
// Amplitudes are pairs of 32-bit fixed-point values (30 fractional bits), held
|
||||
// in a dual-port RAM, one 64-bit lane per address: {imaginary, real}.
|
||||
//
|
||||
// Each 32x32 product is built from four 16-bit partial products, the size of
|
||||
// an FPGA's multiplier blocks: with a = ah*2^16 + al (ah signed, al unsigned)
|
||||
// and b likewise,
|
||||
//
|
||||
// a*b = ah*bh*2^32 + (ah*bl + al*bh)*2^16 + al*bl
|
||||
//
|
||||
// exact in 64 bits, as is fx(a, b) = (a*b + 2^29) >>> 30 and each row's sum
|
||||
// of four such products. The lane keeps each sum's low 32 bits; a sum outside
|
||||
// 32 bits records the gate's op-log index in `report`, the lowest winning.
|
||||
//
|
||||
// One pair per two cycles: the first reads both amplitudes of the pair, the
|
||||
// second writes both results. A gate visits every pair in order; nothing
|
||||
// depends on timing beyond that order, and each pair's two addresses are its
|
||||
// own.
|
||||
|
||||
`default_nettype none
|
||||
|
||||
// The exact product and the exact fixed-point product.
|
||||
module wq_fx_mul (
|
||||
input wire signed [31:0] a,
|
||||
input wire signed [31:0] b,
|
||||
output wire signed [63:0] prod,
|
||||
output wire signed [63:0] fx
|
||||
);
|
||||
wire signed [15:0] ah = a[31:16];
|
||||
wire [15:0] al = a[15:0];
|
||||
wire signed [15:0] bh = b[31:16];
|
||||
wire [15:0] bl = b[15:0];
|
||||
wire signed [31:0] hh = ah * bh;
|
||||
wire signed [32:0] hl = ah * $signed({1'b0, bl});
|
||||
wire signed [32:0] lh = $signed({1'b0, al}) * bh;
|
||||
wire [31:0] ll = al * bl;
|
||||
assign prod = ($signed({{32{hh[31]}}, hh}) <<< 32)
|
||||
+ ($signed({{31{hl[32]}}, hl}) <<< 16)
|
||||
+ ($signed({{31{lh[32]}}, lh}) <<< 16)
|
||||
+ $signed({32'd0, ll});
|
||||
assign fx = (prod + 64'sd536870912) >>> 30;
|
||||
endmodule
|
||||
|
||||
// One gate on one pair: row 0 is (m0 + i*m1)*a0 + (m2 + i*m3)*a1, row 1 is
|
||||
// (m4 + i*m5)*a0 + (m6 + i*m7)*a1.
|
||||
module wq_pair (
|
||||
input wire signed [31:0] m0, m1, m2, m3, m4, m5, m6, m7,
|
||||
input wire signed [31:0] a0r, a0i, a1r, a1i,
|
||||
output wire [31:0] o0r, o0i, o1r, o1i,
|
||||
output wire out_of_range
|
||||
);
|
||||
wire signed [63:0] p0r0, p0i0, p1r0, p1i0, p2r1, p2i1, p3r1, p3i1;
|
||||
wire signed [63:0] p4r0, p4i0, p5r0, p5i0, p6r1, p6i1, p7r1, p7i1;
|
||||
wire signed [63:0] unused [0:15];
|
||||
wq_fx_mul u0 (.a(m0), .b(a0r), .prod(unused[0]), .fx(p0r0));
|
||||
wq_fx_mul u1 (.a(m0), .b(a0i), .prod(unused[1]), .fx(p0i0));
|
||||
wq_fx_mul u2 (.a(m1), .b(a0r), .prod(unused[2]), .fx(p1r0));
|
||||
wq_fx_mul u3 (.a(m1), .b(a0i), .prod(unused[3]), .fx(p1i0));
|
||||
wq_fx_mul u4 (.a(m2), .b(a1r), .prod(unused[4]), .fx(p2r1));
|
||||
wq_fx_mul u5 (.a(m2), .b(a1i), .prod(unused[5]), .fx(p2i1));
|
||||
wq_fx_mul u6 (.a(m3), .b(a1r), .prod(unused[6]), .fx(p3r1));
|
||||
wq_fx_mul u7 (.a(m3), .b(a1i), .prod(unused[7]), .fx(p3i1));
|
||||
wq_fx_mul u8 (.a(m4), .b(a0r), .prod(unused[8]), .fx(p4r0));
|
||||
wq_fx_mul u9 (.a(m4), .b(a0i), .prod(unused[9]), .fx(p4i0));
|
||||
wq_fx_mul u10 (.a(m5), .b(a0r), .prod(unused[10]), .fx(p5r0));
|
||||
wq_fx_mul u11 (.a(m5), .b(a0i), .prod(unused[11]), .fx(p5i0));
|
||||
wq_fx_mul u12 (.a(m6), .b(a1r), .prod(unused[12]), .fx(p6r1));
|
||||
wq_fx_mul u13 (.a(m6), .b(a1i), .prod(unused[13]), .fx(p6i1));
|
||||
wq_fx_mul u14 (.a(m7), .b(a1r), .prod(unused[14]), .fx(p7r1));
|
||||
wq_fx_mul u15 (.a(m7), .b(a1i), .prod(unused[15]), .fx(p7i1));
|
||||
wire signed [63:0] s0r = p0r0 - p1i0 + p2r1 - p3i1;
|
||||
wire signed [63:0] s0i = p0i0 + p1r0 + p2i1 + p3r1;
|
||||
wire signed [63:0] s1r = p4r0 - p5i0 + p6r1 - p7i1;
|
||||
wire signed [63:0] s1i = p4i0 + p5r0 + p6i1 + p7r1;
|
||||
// A sum fits 32 bits when its top 33 bits all equal its sign.
|
||||
function fits32(input [63:0] v);
|
||||
fits32 = (v[63:31] == {33{1'b0}}) || (v[63:31] == {33{1'b1}});
|
||||
endfunction
|
||||
assign out_of_range = !(fits32(s0r) && fits32(s0i) && fits32(s1r) && fits32(s1i));
|
||||
assign o0r = s0r[31:0];
|
||||
assign o0i = s0i[31:0];
|
||||
assign o1r = s1r[31:0];
|
||||
assign o1i = s1i[31:0];
|
||||
endmodule
|
||||
|
||||
// The datapath: the state RAM, its host port, the probe, and the gate engine.
|
||||
module wq_lanes64 #(
|
||||
parameter AW = 20
|
||||
) (
|
||||
input wire clk,
|
||||
// The host port: write or read one lane of the state while idle.
|
||||
input wire host_we,
|
||||
input wire [AW-1:0] host_addr,
|
||||
input wire [63:0] host_wdata,
|
||||
output wire [63:0] host_rdata,
|
||||
// The probe: the exact product and fixed-point product of one pair.
|
||||
input wire [31:0] probe_a,
|
||||
input wire [31:0] probe_b,
|
||||
output wire [63:0] probe_prod,
|
||||
output wire [63:0] probe_fx,
|
||||
// One gate: its words, the state's pair count, and its op-log index.
|
||||
input wire start,
|
||||
input wire [31:0] m0, m1, m2, m3, m4, m5, m6, m7,
|
||||
input wire [31:0] tbit,
|
||||
input wire [31:0] ctrl,
|
||||
input wire [31:0] index,
|
||||
input wire [AW:0] pairs,
|
||||
input wire report_reset,
|
||||
output reg busy,
|
||||
output reg [31:0] report
|
||||
);
|
||||
reg [63:0] mem [0:(1 << AW) - 1];
|
||||
|
||||
assign host_rdata = mem[host_addr];
|
||||
|
||||
wq_fx_mul probe (.a(probe_a), .b(probe_b), .prod(probe_prod), .fx(probe_fx));
|
||||
|
||||
reg [AW:0] p;
|
||||
reg phase;
|
||||
reg [63:0] lane_i, lane_j;
|
||||
reg [AW-1:0] addr_i, addr_j;
|
||||
reg take;
|
||||
|
||||
// The p-th index whose target bit is clear: p with a zero inserted at the
|
||||
// target's position.
|
||||
wire [AW:0] tmask = tbit[AW:0] - 1'b1;
|
||||
wire [AW:0] low = p & tmask;
|
||||
wire [AW:0] i_ix = ((p - low) << 1) | low;
|
||||
wire [AW:0] j_ix = i_ix | tbit[AW:0];
|
||||
wire selected = ((i_ix & ctrl[AW:0]) == ctrl[AW:0]);
|
||||
|
||||
wire [31:0] o0r, o0i, o1r, o1i;
|
||||
wire oor;
|
||||
wq_pair pair (
|
||||
.m0(m0), .m1(m1), .m2(m2), .m3(m3), .m4(m4), .m5(m5), .m6(m6), .m7(m7),
|
||||
.a0r(lane_i[31:0]), .a0i(lane_i[63:32]), .a1r(lane_j[31:0]), .a1i(lane_j[63:32]),
|
||||
.o0r(o0r), .o0i(o0i), .o1r(o1r), .o1i(o1i), .out_of_range(oor)
|
||||
);
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (report_reset) begin
|
||||
report <= 32'hFFFF_FFFF;
|
||||
end
|
||||
if (!busy) begin
|
||||
if (host_we) begin
|
||||
mem[host_addr] <= host_wdata;
|
||||
end
|
||||
if (start) begin
|
||||
busy <= (pairs != 0);
|
||||
p <= 0;
|
||||
phase <= 1'b0;
|
||||
end
|
||||
end else if (phase == 1'b0) begin
|
||||
// Read the pair's two lanes.
|
||||
addr_i <= i_ix[AW-1:0];
|
||||
addr_j <= j_ix[AW-1:0];
|
||||
lane_i <= mem[i_ix[AW-1:0]];
|
||||
lane_j <= mem[j_ix[AW-1:0]];
|
||||
take <= selected;
|
||||
phase <= 1'b1;
|
||||
end else begin
|
||||
// Write both results, and report a sum outside 32 bits.
|
||||
if (take) begin
|
||||
mem[addr_i] <= {o0i, o0r};
|
||||
mem[addr_j] <= {o1i, o1r};
|
||||
if (oor && index < report) begin
|
||||
report <= index;
|
||||
end
|
||||
end
|
||||
phase <= 1'b0;
|
||||
if (p + 1 == pairs) begin
|
||||
busy <= 1'b0;
|
||||
end
|
||||
p <= p + 1;
|
||||
end
|
||||
end
|
||||
|
||||
initial begin
|
||||
busy = 1'b0;
|
||||
report = 32'hFFFF_FFFF;
|
||||
end
|
||||
endmodule
|
||||
|
||||
`default_nettype wire
|
||||
149
wai/wai-quantum/src/quantum_fabric/lanes64_harness.cpp
Normal file
149
wai/wai-quantum/src/quantum_fabric/lanes64_harness.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// wai-quantum fabric: the host harness for the gate datapath's simulation.
|
||||
//
|
||||
// Drives the Verilator model of `wq_lanes64` (lanes64.v) over a binary
|
||||
// protocol on stdin and stdout, little-endian, one request at a time:
|
||||
//
|
||||
// op 0 quit
|
||||
// op 1, u64 count, count x (i32 a, i32 b)
|
||||
// probe: count x (u32 prod_hi, prod_lo, fx_hi, fx_lo)
|
||||
// op 2, u32 n, u32 gates, 2^n x (i32 re, i32 im),
|
||||
// gates x (u32 m0..m7, tbit, ctrl, index)
|
||||
// run: u32 report, 2^n x (i32 re, i32 im), u64 cycles
|
||||
//
|
||||
// The harness only moves bytes and toggles the clock: the arithmetic, the pair
|
||||
// addressing, the control test and the report are the model's. A request it
|
||||
// cannot serve (a state wider than the model's RAM, a short read) ends the
|
||||
// process with a nonzero status, which the member reports.
|
||||
|
||||
#include "Vwq_lanes64.h"
|
||||
#include "verilated.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
static Vwq_lanes64* top = nullptr;
|
||||
static uint64_t cycles = 0;
|
||||
|
||||
static void tick() {
|
||||
top->clk = 0;
|
||||
top->eval();
|
||||
top->clk = 1;
|
||||
top->eval();
|
||||
cycles++;
|
||||
}
|
||||
|
||||
static void need(void* buf, size_t n) {
|
||||
if (fread(buf, 1, n, stdin) != n) {
|
||||
std::exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
template <class T> static T get() {
|
||||
T v;
|
||||
need(&v, sizeof v);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void put(const void* buf, size_t n) {
|
||||
if (fwrite(buf, 1, n, stdout) != n) {
|
||||
std::exit(4);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Verilated::commandArgs(argc, argv);
|
||||
top = new Vwq_lanes64;
|
||||
top->clk = 0;
|
||||
top->host_we = 0;
|
||||
top->start = 0;
|
||||
top->report_reset = 0;
|
||||
top->eval();
|
||||
for (;;) {
|
||||
uint8_t op;
|
||||
if (fread(&op, 1, 1, stdin) != 1 || op == 0) {
|
||||
break;
|
||||
}
|
||||
if (op == 1) {
|
||||
uint64_t count = get<uint64_t>();
|
||||
std::vector<uint32_t> out;
|
||||
out.reserve(count * 4);
|
||||
for (uint64_t k = 0; k < count; k++) {
|
||||
int32_t a = get<int32_t>();
|
||||
int32_t b = get<int32_t>();
|
||||
top->probe_a = (uint32_t)a;
|
||||
top->probe_b = (uint32_t)b;
|
||||
top->eval();
|
||||
uint64_t prod = top->probe_prod;
|
||||
uint64_t fx = top->probe_fx;
|
||||
out.push_back((uint32_t)(prod >> 32));
|
||||
out.push_back((uint32_t)prod);
|
||||
out.push_back((uint32_t)(fx >> 32));
|
||||
out.push_back((uint32_t)fx);
|
||||
}
|
||||
put(out.data(), out.size() * 4);
|
||||
} else if (op == 2) {
|
||||
uint32_t n = get<uint32_t>();
|
||||
uint32_t gates = get<uint32_t>();
|
||||
if (n < 1 || n > WQ_AW) {
|
||||
std::exit(5);
|
||||
}
|
||||
uint64_t len = 1ull << n;
|
||||
cycles = 0;
|
||||
for (uint64_t x = 0; x < len; x++) {
|
||||
uint32_t re = (uint32_t)get<int32_t>();
|
||||
uint32_t im = (uint32_t)get<int32_t>();
|
||||
top->host_addr = (uint32_t)x;
|
||||
top->host_wdata = ((uint64_t)im << 32) | re;
|
||||
top->host_we = 1;
|
||||
tick();
|
||||
}
|
||||
top->host_we = 0;
|
||||
top->report_reset = 1;
|
||||
tick();
|
||||
top->report_reset = 0;
|
||||
for (uint32_t g = 0; g < gates; g++) {
|
||||
uint32_t w[11];
|
||||
need(w, sizeof w);
|
||||
top->m0 = w[0];
|
||||
top->m1 = w[1];
|
||||
top->m2 = w[2];
|
||||
top->m3 = w[3];
|
||||
top->m4 = w[4];
|
||||
top->m5 = w[5];
|
||||
top->m6 = w[6];
|
||||
top->m7 = w[7];
|
||||
top->tbit = w[8];
|
||||
top->ctrl = w[9];
|
||||
top->index = w[10];
|
||||
top->pairs = (uint32_t)(len / 2);
|
||||
top->start = 1;
|
||||
tick();
|
||||
top->start = 0;
|
||||
while (top->busy) {
|
||||
tick();
|
||||
}
|
||||
}
|
||||
uint32_t report = top->report;
|
||||
put(&report, 4);
|
||||
std::vector<int32_t> state;
|
||||
state.reserve(len * 2);
|
||||
for (uint64_t x = 0; x < len; x++) {
|
||||
top->host_addr = (uint32_t)x;
|
||||
top->eval();
|
||||
uint64_t v = top->host_rdata;
|
||||
state.push_back((int32_t)(uint32_t)v);
|
||||
state.push_back((int32_t)(uint32_t)(v >> 32));
|
||||
}
|
||||
put(state.data(), state.size() * 4);
|
||||
put(&cycles, 8);
|
||||
} else {
|
||||
std::exit(2);
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
top->final();
|
||||
delete top;
|
||||
return 0;
|
||||
}
|
||||
271
wai/wai-quantum/src/quantum_fabric/opencl.rs
Normal file
271
wai/wai-quantum/src/quantum_fabric/opencl.rs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
//! The gate kernel for OpenCL devices, and what names an OpenCL member: the
|
||||
//! kernel's text (`lanes64.cl`), its implementation id, a device's identity
|
||||
//! and the mutants a conforming device must be caught running. No device is
|
||||
//! touched here, so all of it is testable on any host; the OpenCL member itself
|
||||
//! is `quantum_fabric_opencl`.
|
||||
//!
|
||||
//! **Why the kernel is bit-exact.** OpenCL C's `long` is a 64-bit integer on
|
||||
//! every full-profile device, so the kernel is the same transcription of
|
||||
//! [`lanes::exact_row`] and [`lanes::apply_words`] as the CUDA kernel
|
||||
//! ([`super::cuda`]): each `fx(a, b) = (a·b + 2^29) >> 30` and each row's sum
|
||||
//! of four is exact in 64 bits, the lane keeps the sum's low 32 bits, and a sum
|
||||
//! outside 32 bits records the gate's op-log index, the lowest winning.
|
||||
//!
|
||||
//! Each work-item owns one amplitude pair and a launch is one gate, so no
|
||||
//! work-item reads what another writes within a launch, and neither the
|
||||
//! work-group size the runtime picks nor the order work-items run in can change
|
||||
//! a bit. The member's probe checks the device's products and shifts against
|
||||
//! [`lanes::mul_wide`] and [`lanes::fxmul_wide`] before the device is a member,
|
||||
//! and its certificates check whole runs against the CPU's pinned corpus
|
||||
//! digests.
|
||||
//!
|
||||
//! [`lanes::exact_row`]: super::lanes
|
||||
//! [`lanes::apply_words`]: super::lanes::apply_words
|
||||
//! [`lanes::mul_wide`]: super::lanes::mul_wide
|
||||
//! [`lanes::fxmul_wide`]: super::lanes::fxmul_wide
|
||||
|
||||
use super::FabricClass;
|
||||
use super::wgsl::{Detector, Mutant};
|
||||
|
||||
/// The kernel, `lanes64.cl`, exactly as it is built.
|
||||
pub const SOURCE: &str = include_str!("lanes64.cl");
|
||||
|
||||
/// The implementation name the id starts with.
|
||||
pub const IMPLEMENTATION: &str = "opencl-lanes64";
|
||||
|
||||
/// Version of the host-side layout: interleaved `int2` lanes, one launch per
|
||||
/// gate with the gate's words as kernel arguments, the report word (the
|
||||
/// lowest out-of-range op index, `u32::MAX` for none), the probe's `int2`
|
||||
/// cases and `uint4` outputs, and the launch shape ([`GLOBAL_MAX`], the
|
||||
/// work-group size left to the runtime). Bumped on any change to them; it is
|
||||
/// part of the implementation id.
|
||||
pub const HOST_LAYOUT: u32 = 1;
|
||||
|
||||
/// Work-items per launch, at most; the kernel strides over the rest.
|
||||
pub const GLOBAL_MAX: u64 = 1 << 20;
|
||||
|
||||
/// The options the kernel is built with: OpenCL C 1.2, and nothing that
|
||||
/// relaxes arithmetic, though the kernel has no floating point for it to
|
||||
/// touch.
|
||||
pub const BUILD_OPTIONS: &str = "-cl-std=CL1.2";
|
||||
|
||||
const DOMAIN_IMPLEMENTATION: &[u8] = b"wai:quantum-fabric-opencl\x01";
|
||||
const DOMAIN_MEMBER: &[u8] = b"wai:quantum-fabric-opencl-member\x01";
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
/// A length-prefixed byte string, so that no field reads as part of another.
|
||||
fn put(o: &mut Vec<u8>, bytes: &[u8]) {
|
||||
o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
|
||||
o.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
/// `"opencl-lanes64/<16 hex>"`: the first 8 bytes of
|
||||
/// BLAKE3(`"wai:quantum-fabric-opencl\x01"` ‖ put([`SOURCE`]) ‖
|
||||
/// put([`BUILD_OPTIONS`]) ‖ [`HOST_LAYOUT`] as a big-endian `u32`), in hex. Any
|
||||
/// edit to the kernel's text, its build options or the host layout changes it.
|
||||
/// The device and its compiler are part of the member id instead.
|
||||
pub fn implementation_id() -> String {
|
||||
implementation_id_of(SOURCE)
|
||||
}
|
||||
|
||||
/// [`implementation_id`] for another kernel text, such as a mutant's.
|
||||
pub fn implementation_id_of(source: &str) -> String {
|
||||
let mut o = DOMAIN_IMPLEMENTATION.to_vec();
|
||||
put(&mut o, source.as_bytes());
|
||||
put(&mut o, BUILD_OPTIONS.as_bytes());
|
||||
o.extend_from_slice(&HOST_LAYOUT.to_be_bytes());
|
||||
format!("{IMPLEMENTATION}/{}", hex(&blake3::hash(&o).as_bytes()[..8]))
|
||||
}
|
||||
|
||||
/// The kind of device an OpenCL platform reports.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum DeviceKind {
|
||||
Gpu,
|
||||
Cpu,
|
||||
/// An accelerator, or any type the platform reports that is neither.
|
||||
Accelerator,
|
||||
}
|
||||
|
||||
impl DeviceKind {
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
DeviceKind::Gpu => "gpu",
|
||||
DeviceKind::Cpu => "cpu",
|
||||
DeviceKind::Accelerator => "accelerator",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What an OpenCL member is, as its id hashes it. The member fills it from the
|
||||
/// runtime; the fields are plain values here so that the id is testable
|
||||
/// without a device.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DeviceIdentity {
|
||||
/// The platform's name, version and vendor, as it reports them.
|
||||
pub platform: String,
|
||||
pub platform_version: String,
|
||||
pub vendor: String,
|
||||
/// The device's name, its OpenCL version string and its driver's version.
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub driver: String,
|
||||
pub kind: DeviceKind,
|
||||
/// The device's UUID in hex where the platform reports one
|
||||
/// (`cl_khr_device_uuid`), else empty.
|
||||
pub uuid: String,
|
||||
pub compute_units: u32,
|
||||
/// The device's global memory, bytes.
|
||||
pub memory: u64,
|
||||
}
|
||||
|
||||
impl DeviceIdentity {
|
||||
/// [`FabricClass::Gpu`] for a GPU and [`FabricClass::Cpu`] for a CPU. An
|
||||
/// OpenCL accelerator may be an FPGA, a DSP or anything else, so it has no
|
||||
/// class, and discovery refuses it.
|
||||
pub fn class(&self) -> Option<FabricClass> {
|
||||
match self.kind {
|
||||
DeviceKind::Gpu => Some(FabricClass::Gpu),
|
||||
DeviceKind::Cpu => Some(FabricClass::Cpu),
|
||||
DeviceKind::Accelerator => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The leading letters and digits of the vendor's first word other than
|
||||
/// "the", lowercase: `"Maker Corporation"` is `"maker"`,
|
||||
/// `"Maker(R) Corporation"` is `"maker"`, `"The pocl project"` is `"pocl"`.
|
||||
pub fn vendor_slug(&self) -> String {
|
||||
let word = self.vendor.split_whitespace().find(|w| !w.eq_ignore_ascii_case("the")).unwrap_or("");
|
||||
let slug: String = word.chars().take_while(|c| c.is_ascii_alphanumeric()).map(|c| c.to_ascii_lowercase()).collect();
|
||||
if slug.is_empty() { "unknown".into() } else { slug }
|
||||
}
|
||||
|
||||
/// `"local/opencl-<kind>-<d12>"`. `d12` is the first 12 hex digits of
|
||||
/// BLAKE3(`"wai:quantum-fabric-opencl-member\x01"` ‖ put of the platform,
|
||||
/// its version, the vendor, the name, the version, the driver, the kind and
|
||||
/// the UUID ‖ compute units ‖ memory), the numbers big-endian. It sorts
|
||||
/// after every `local/cpu-<arch>` and `local/gpu-…`, so with no energy
|
||||
/// figures [`place`](super::place) keeps the CPU reference.
|
||||
pub fn member_id(&self) -> String {
|
||||
let mut o = DOMAIN_MEMBER.to_vec();
|
||||
for s in [&self.platform, &self.platform_version, &self.vendor, &self.name, &self.version, &self.driver] {
|
||||
put(&mut o, s.as_bytes());
|
||||
}
|
||||
put(&mut o, self.kind.name().as_bytes());
|
||||
put(&mut o, self.uuid.as_bytes());
|
||||
o.extend_from_slice(&self.compute_units.to_be_bytes());
|
||||
o.extend_from_slice(&self.memory.to_be_bytes());
|
||||
format!("local/opencl-{}-{}", self.kind.name(), hex(&blake3::hash(&o).as_bytes()[..6]))
|
||||
}
|
||||
|
||||
/// The key a runner declares in CI: `"opencl-<kind>-<vendor>"`, for
|
||||
/// instance `"opencl-gpu-maker"`.
|
||||
pub fn key(&self) -> String {
|
||||
format!("opencl-{}-{}", self.kind.name(), self.vendor_slug())
|
||||
}
|
||||
}
|
||||
|
||||
/// The mutants: textual edits of [`SOURCE`] that a conforming device must be
|
||||
/// caught running, each by the detector named. Each `find` occurs exactly
|
||||
/// once in [`SOURCE`].
|
||||
pub const MUTANTS: [Mutant; 6] = [
|
||||
// The rounding constant is half what it should be.
|
||||
Mutant { name: "O1", find: "((i64)a * (i64)b + ((i64)1 << 29)) >> 30;", replace: "((i64)a * (i64)b + ((i64)1 << 28)) >> 30;", detector: Detector::Probe },
|
||||
// The product is shifted by one bit too few.
|
||||
Mutant { name: "O2", find: "((i64)a * (i64)b + ((i64)1 << 29)) >> 30;", replace: "((i64)a * (i64)b + ((i64)1 << 29)) >> 29;", detector: Detector::Probe },
|
||||
// The probe's product is taken in 32 bits.
|
||||
Mutant { name: "O3", find: "u64 prod = (u64)((i64)c.x * (i64)c.y);", replace: "u64 prod = (u64)(uint)(c.x * c.y);", detector: Detector::Probe },
|
||||
// The control test is inverted.
|
||||
Mutant { name: "O4", find: "if ((i & (u64)ctrl) != (u64)ctrl) {", replace: "if ((i & (u64)ctrl) == (u64)ctrl) {", detector: Detector::Certificate },
|
||||
// A sign of row 0's real part is wrong, on the imaginary part of the
|
||||
// top-right entry, which `Y` has.
|
||||
Mutant { name: "O5", find: "fx(m2, a1.x) - fx(m3, a1.y);", replace: "fx(m2, a1.x) + fx(m3, a1.y);", detector: Detector::Certificate },
|
||||
// The range guard never fires.
|
||||
Mutant { name: "O6", find: "if (!(fits32(o0r) && fits32(o0i) && fits32(o1r) && fits32(o1i))) {", replace: "if (0) {", detector: Detector::Range },
|
||||
];
|
||||
|
||||
/// [`SOURCE`] with `m` applied. Panics unless `m.find` occurs exactly once.
|
||||
pub fn mutate(m: &Mutant) -> String {
|
||||
let n = SOURCE.matches(m.find).count();
|
||||
assert_eq!(n, 1, "mutant {}: `{}` occurs {n} times in the kernel", m.name, m.find);
|
||||
SOURCE.replacen(m.find, m.replace, 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn identity() -> DeviceIdentity {
|
||||
DeviceIdentity {
|
||||
platform: "a platform".into(),
|
||||
platform_version: "OpenCL 3.0".into(),
|
||||
vendor: "Graphics Corporation".into(),
|
||||
name: "a device".into(),
|
||||
version: "OpenCL 3.0 CUDA".into(),
|
||||
driver: "595.84".into(),
|
||||
kind: DeviceKind::Gpu,
|
||||
uuid: "00112233445566778899aabbccddeeff".into(),
|
||||
compute_units: 20,
|
||||
memory: 6 << 30,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_field_of_the_identity_is_in_the_member_id() {
|
||||
let base = identity();
|
||||
let id = base.member_id();
|
||||
assert!(id.starts_with("local/opencl-gpu-") && id.len() == "local/opencl-gpu-".len() + 12, "{id}");
|
||||
for other in ["local/cpu-x86_64", "local/cpu-aarch64", "local/gpu-cuda-ffffffffffff"] {
|
||||
assert!(id.as_str() > other, "{id} sorts after {other}");
|
||||
}
|
||||
let cpu = DeviceIdentity { kind: DeviceKind::Cpu, ..identity() }.member_id();
|
||||
assert!(cpu.as_str() > "local/cpu-x86_64", "a CPU device sorts after the CPU reference: {cpu}");
|
||||
let variants = [
|
||||
DeviceIdentity { platform: "another".into(), ..identity() },
|
||||
DeviceIdentity { platform_version: "OpenCL 1.2".into(), ..identity() },
|
||||
DeviceIdentity { vendor: "Cores(R) Corporation".into(), ..identity() },
|
||||
DeviceIdentity { name: "another".into(), ..identity() },
|
||||
DeviceIdentity { version: "OpenCL 1.2".into(), ..identity() },
|
||||
DeviceIdentity { driver: "595.85".into(), ..identity() },
|
||||
DeviceIdentity { kind: DeviceKind::Cpu, ..identity() },
|
||||
DeviceIdentity { kind: DeviceKind::Accelerator, ..identity() },
|
||||
DeviceIdentity { uuid: String::new(), ..identity() },
|
||||
DeviceIdentity { compute_units: 21, ..identity() },
|
||||
DeviceIdentity { memory: 8 << 30, ..identity() },
|
||||
];
|
||||
for v in variants {
|
||||
assert_ne!(v.member_id(), id, "{v:?}");
|
||||
}
|
||||
assert_eq!(base.key(), "opencl-gpu-graphics");
|
||||
assert_eq!(DeviceIdentity { vendor: "Cores(R) Corporation".into(), kind: DeviceKind::Cpu, ..identity() }.key(), "opencl-cpu-cores");
|
||||
assert_eq!(DeviceIdentity { vendor: "The pocl project".into(), kind: DeviceKind::Cpu, ..identity() }.key(), "opencl-cpu-pocl");
|
||||
assert_eq!(DeviceIdentity { vendor: String::new(), ..identity() }.key(), "opencl-gpu-unknown");
|
||||
assert_eq!(base.class(), Some(FabricClass::Gpu));
|
||||
assert_eq!(DeviceIdentity { kind: DeviceKind::Cpu, ..identity() }.class(), Some(FabricClass::Cpu));
|
||||
assert_eq!(DeviceIdentity { kind: DeviceKind::Accelerator, ..identity() }.class(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_implementation_id_follows_the_kernel_text() {
|
||||
let id = implementation_id();
|
||||
assert!(id.starts_with("opencl-lanes64/") && id.len() == "opencl-lanes64/".len() + 16, "{id}");
|
||||
for m in &MUTANTS {
|
||||
assert_ne!(implementation_id_of(&mutate(m)), id, "mutant {}", m.name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_mutant_edits_the_kernel_once_and_names_distinct_edits() {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for m in &MUTANTS {
|
||||
let edited = mutate(m);
|
||||
assert_ne!(edited, SOURCE, "mutant {} changes nothing", m.name);
|
||||
assert!(seen.insert(edited), "mutant {} duplicates another", m.name);
|
||||
}
|
||||
for d in [Detector::Probe, Detector::Certificate, Detector::Range] {
|
||||
assert!(MUTANTS.iter().any(|m| m.detector == d), "no mutant for {d:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
395
wai/wai-quantum/src/quantum_fabric/sampling.rs
Normal file
395
wai/wai-quantum/src/quantum_fabric/sampling.rs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
//! Sampling on the fabric: the kernel a quantum processor offers, and how a
|
||||
//! member that returns measurement counts rather than a state is certified,
|
||||
//! placed and receipted.
|
||||
//!
|
||||
//! - **The kernel** ([`sample_kernel`]): run a circuit from `|0…0⟩`, measure
|
||||
//! every qubit in the computational basis, `shots` times, and return the
|
||||
//! count of each basis state. Its reference is the exact distribution of the
|
||||
//! CPU's integer simulation ([`reference_distribution`]); the CPU reference
|
||||
//! sampler ([`CpuSampler`]) draws from it with the core's seeded integer
|
||||
//! sampler, so its counts are the same on every machine.
|
||||
//! - **A member that samples** implements [`Sampler`]. Its certificate is
|
||||
//! statistical ([`Parity::Statistical`](super::Parity::Statistical)): over a corpus of circuits shaped
|
||||
//! for quantum processors ([`QpuCorpus`]), the worst classical fidelity
|
||||
//! between its counts and the reference distribution, and the shots each
|
||||
//! case took ([`certify_sampler`]).
|
||||
//! - **Placement** is [`place`](super::place) with a statistical requirement:
|
||||
//! a bit-exact reference meets every requirement, so the CPU reference is
|
||||
//! placed unless the caller offers only other members; a simulated
|
||||
//! processor's certificate ([`Provenance::Simulator`]) ranks after every
|
||||
//! silicon one.
|
||||
//! - **A receipt** binds [`counts_hash`]: the circuit's hash, the shots and the
|
||||
//! counts.
|
||||
|
||||
use super::{Certificate, Member, MemberError, Presence, Provenance, REFERENCE, RunMeter, Window, certify_sampling, detect_local, lanes};
|
||||
use crate::quantum::{BaseGate, Circuit, Gate, Gateset, QuantumError, circuit_hash};
|
||||
use crate::quantum_energy::EnergyClass;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// The sampling kernel of the frozen gateset.
|
||||
pub const KERNEL_CIRCUIT_SAMPLE: &str = "wai.quantum.circuit/sample";
|
||||
|
||||
/// The sampling kernel of circuit2.
|
||||
pub const KERNEL_CIRCUIT2_SAMPLE: &str = "wai.quantum.circuit2/sample";
|
||||
|
||||
/// The sampling kernel of `gs`.
|
||||
pub const fn sample_kernel(gs: Gateset) -> &'static str {
|
||||
match gs {
|
||||
Gateset::V1 => KERNEL_CIRCUIT_SAMPLE,
|
||||
_ => KERNEL_CIRCUIT2_SAMPLE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts over the `2^n` basis states, by index: qubit `q` is bit `q` of the
|
||||
/// index, as in a state vector.
|
||||
pub type Counts = Vec<u64>;
|
||||
|
||||
/// What a [`Sampler`] returns: the counts, and the microjoules and class its
|
||||
/// meter read (zero and `None` when unmetered).
|
||||
pub type SampleFuture<'r> = Pin<Box<dyn Future<Output = Result<(Counts, u64, Option<EnergyClass>), MemberError>> + 'r>>;
|
||||
|
||||
/// A member that measures: a quantum processor, a simulator of one, or the
|
||||
/// CPU reference sampler.
|
||||
pub trait Sampler {
|
||||
/// The id of the member this sampler is.
|
||||
fn member_id(&self) -> String;
|
||||
|
||||
/// `shots` measurements of every qubit of `c`, run from `|0…0⟩` with
|
||||
/// `implementation`, inside a window of `meter`: one count per basis state,
|
||||
/// summing to `shots`.
|
||||
fn sample<'r>(&'r self, implementation: &'r str, c: &'r Circuit, shots: u64, meter: &'r mut dyn RunMeter) -> SampleFuture<'r>;
|
||||
}
|
||||
|
||||
impl<S: Sampler + ?Sized> Sampler for &S {
|
||||
fn member_id(&self) -> String {
|
||||
(**self).member_id()
|
||||
}
|
||||
|
||||
fn sample<'r>(&'r self, implementation: &'r str, c: &'r Circuit, shots: u64, meter: &'r mut dyn RunMeter) -> SampleFuture<'r> {
|
||||
(**self).sample(implementation, c, shots, meter)
|
||||
}
|
||||
}
|
||||
|
||||
/// The probability of each basis state of `c` run from `|0…0⟩`: the CPU's
|
||||
/// integer state, its weights `|amp|²` divided by their sum. Refuses what the
|
||||
/// simulator refuses.
|
||||
pub fn reference_distribution(c: &Circuit) -> Result<Vec<f64>, QuantumError> {
|
||||
let weights = c.simulate()?.prob_weights();
|
||||
let total: i128 = weights.iter().sum();
|
||||
if total <= 0 {
|
||||
return Ok(vec![0.0; weights.len()]);
|
||||
}
|
||||
let t = total as f64;
|
||||
Ok(weights.iter().map(|&w| w as f64 / t).collect())
|
||||
}
|
||||
|
||||
/// The CPU reference as a [`Sampler`]: the exact integer state, sampled with
|
||||
/// the core's seeded sampler (`StateVector::sample_shots`), so the same
|
||||
/// circuit, seed and shots give the same counts on every machine.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct CpuSampler {
|
||||
pub seed: u64,
|
||||
}
|
||||
|
||||
impl Sampler for CpuSampler {
|
||||
fn member_id(&self) -> String {
|
||||
detect_local().id
|
||||
}
|
||||
|
||||
fn sample<'r>(&'r self, implementation: &'r str, c: &'r Circuit, shots: u64, meter: &'r mut dyn RunMeter) -> SampleFuture<'r> {
|
||||
Box::pin(async move {
|
||||
if implementation != REFERENCE {
|
||||
return Err(MemberError::Other(format!("the CPU reference sampler does not run implementation {implementation:?}")));
|
||||
}
|
||||
let mut window = Window::new(meter);
|
||||
window.open();
|
||||
let counts = c.simulate().map_err(MemberError::Circuit)?.sample_shots(self.seed, shots);
|
||||
let (joules_micro, class) = window.close();
|
||||
Ok((counts, joules_micro, class))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The CPU reference's sampling entries: one per gateset, bit-exact by
|
||||
/// definition ([`Certificate::reference`]).
|
||||
pub fn local_sampling_member() -> Member {
|
||||
let local = detect_local();
|
||||
let kernels = Gateset::ALL
|
||||
.into_iter()
|
||||
.map(|gs| super::KernelEntry {
|
||||
kernel: sample_kernel(gs).into(),
|
||||
implementation: REFERENCE.into(),
|
||||
certificate: Certificate::reference(sample_kernel(gs), &local.id),
|
||||
energy: None,
|
||||
})
|
||||
.collect();
|
||||
Member { kernels, ..local }
|
||||
}
|
||||
|
||||
/// A certification corpus for samplers, shaped for quantum processors: few
|
||||
/// qubits, shallow, at most one control per gate and phases no finer than
|
||||
/// `P(4)`, so a processor's compiler takes it as written in either gateset.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct QpuCorpus {
|
||||
pub seed: u64,
|
||||
/// Circuits in all: a Bell pair, GHZ on three qubits, GHZ on
|
||||
/// `max_qubits`, then random circuits.
|
||||
pub cases: usize,
|
||||
/// The widest circuit, at least 3.
|
||||
pub max_qubits: u8,
|
||||
/// Ops per random circuit.
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
/// The default sampler corpus: qpu-v1, seed 7, 16 circuits of up to 5 qubits,
|
||||
/// 12 ops each.
|
||||
pub const QPU_DEFAULT: QpuCorpus = QpuCorpus { seed: 7, cases: 16, max_qubits: 5, depth: 12 };
|
||||
|
||||
fn next(s: &mut u64) -> u64 {
|
||||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = *s;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
impl QpuCorpus {
|
||||
/// `"qpu-v1 seed=<seed> cases=<cases> qubits=<max_qubits> depth=<depth>"`,
|
||||
/// enough to draw the corpus again.
|
||||
pub fn generator(&self) -> String {
|
||||
format!("qpu-v1 seed={} cases={} qubits={} depth={}", self.seed, self.cases, self.max_qubits, self.depth)
|
||||
}
|
||||
|
||||
/// The circuits, in order, in `gs`. Both gatesets draw the same ops.
|
||||
pub fn circuits(&self, gs: Gateset) -> Vec<Circuit> {
|
||||
let one = |base, target| Gate { base, controls: Vec::new(), target, param: 0 };
|
||||
let cx = |control, target| Gate { base: BaseGate::X, controls: vec![control], target, param: 0 };
|
||||
let ghz = |n: u8| {
|
||||
let mut c = Circuit::with_gateset(n, gs);
|
||||
c.ops.push(one(BaseGate::H, 0));
|
||||
(1..n).for_each(|q| c.ops.push(cx(q - 1, q)));
|
||||
c
|
||||
};
|
||||
let mut out = vec![ghz(2), ghz(3), ghz(self.max_qubits.max(3))];
|
||||
let bases = [BaseGate::H, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P];
|
||||
let mut s = self.seed;
|
||||
while out.len() < self.cases {
|
||||
let n = 2 + (next(&mut s) % u64::from(self.max_qubits.max(3) - 1)) as u8;
|
||||
let mut c = Circuit::with_gateset(n, gs);
|
||||
for _ in 0..self.depth {
|
||||
let target = (next(&mut s) % u64::from(n)) as u8;
|
||||
if next(&mut s).is_multiple_of(3) {
|
||||
let control = (target + 1 + (next(&mut s) % u64::from(n - 1)) as u8) % n;
|
||||
let base = if next(&mut s).is_multiple_of(2) { BaseGate::X } else { BaseGate::Z };
|
||||
c.ops.push(Gate { base, controls: vec![control], target, param: 0 });
|
||||
} else {
|
||||
let base = bases[(next(&mut s) % bases.len() as u64) as usize];
|
||||
let param = if base == BaseGate::P { 1 + (next(&mut s) % 4) as u16 } else { 0 };
|
||||
c.ops.push(Gate { base, controls: Vec::new(), target, param });
|
||||
}
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out.truncate(self.cases);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// A case's input bytes for a sampling certificate: the circuit's bytes
|
||||
/// ([`lanes::circuit_bytes`]) and the shots, little-endian.
|
||||
pub fn case_input(c: &Circuit, shots: u64) -> Vec<u8> {
|
||||
let mut o = lanes::circuit_bytes(c);
|
||||
o.extend_from_slice(&shots.to_le_bytes());
|
||||
o
|
||||
}
|
||||
|
||||
/// Checks `counts` for a run of `c` with `shots`: one count per basis state,
|
||||
/// summing to `shots`.
|
||||
pub fn check_counts(c: &Circuit, shots: u64, counts: &[u64]) -> Result<(), MemberError> {
|
||||
let want = 1usize.checked_shl(u32::from(c.n_qubits)).unwrap_or(0);
|
||||
if counts.len() != want {
|
||||
return Err(MemberError::Other(format!("{} counts for a circuit of {} qubits; {want} expected", counts.len(), c.n_qubits)));
|
||||
}
|
||||
let total = counts.iter().try_fold(0u64, |a, &b| a.checked_add(b));
|
||||
if total != Some(shots) {
|
||||
return Err(MemberError::Other(format!("the counts sum to {total:?}, not {shots}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The statistical certificate of `sampler` running `implementation` for
|
||||
/// [`sample_kernel`]`(gs)`, over `corpus` with `shots` per circuit: each case
|
||||
/// folds in the circuit and shots, the reference distribution and the counts
|
||||
/// ([`certify_sampling`]); the parity is the worst case's classical fidelity.
|
||||
/// The certificate is earned under `provenance`. Counts of the wrong shape
|
||||
/// stop the certification.
|
||||
pub async fn certify_sampler<S: Sampler + ?Sized>(sampler: &S, implementation: &str, gs: Gateset, corpus: QpuCorpus, shots: u64, provenance: Provenance) -> Result<Certificate, MemberError> {
|
||||
let mut cases = Vec::new();
|
||||
for c in corpus.circuits(gs) {
|
||||
let reference = reference_distribution(&c).map_err(MemberError::Circuit)?;
|
||||
let (counts, _, _) = sampler.sample(implementation, &c, shots, &mut super::Unmetered).await?;
|
||||
check_counts(&c, shots, &counts)?;
|
||||
cases.push((case_input(&c, shots), reference, counts));
|
||||
}
|
||||
Ok(certify_sampling(sample_kernel(gs), implementation, &sampler.member_id(), &corpus.generator(), cases).with_provenance(provenance))
|
||||
}
|
||||
|
||||
const DOMAIN_COUNTS: &[u8] = b"wai:quantum-fabric-counts\x01";
|
||||
|
||||
/// What a receipt over a sampled result binds: BLAKE3 of the domain, the
|
||||
/// circuit's hash, the shots and every count, little-endian. `None` for a
|
||||
/// circuit with no hash (one its gateset does not allow).
|
||||
pub fn counts_hash(c: &Circuit, shots: u64, counts: &[u64]) -> Option<[u8; 32]> {
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(DOMAIN_COUNTS);
|
||||
h.update(&circuit_hash(c).ok()?);
|
||||
h.update(&shots.to_le_bytes());
|
||||
h.update(&(counts.len() as u64).to_le_bytes());
|
||||
for k in counts {
|
||||
h.update(&k.to_le_bytes());
|
||||
}
|
||||
Some(*h.finalize().as_bytes())
|
||||
}
|
||||
|
||||
/// A member's sampling entries are present: its id and presence, for
|
||||
/// [`place`](super::place).
|
||||
pub fn sampling_member(id: String, class: super::FabricClass, detail: String, entries: Vec<super::KernelEntry>) -> Member {
|
||||
Member { id, class, presence: Presence::Detected, kernels: entries, detail }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::quantum_fabric::{FabricClass, KernelEntry, Parity, place, ready};
|
||||
|
||||
#[test]
|
||||
fn the_sample_kernels_name_their_gateset() {
|
||||
assert_eq!(sample_kernel(Gateset::V1), KERNEL_CIRCUIT_SAMPLE);
|
||||
assert_eq!(sample_kernel(Gateset::V2), KERNEL_CIRCUIT2_SAMPLE);
|
||||
assert_ne!(KERNEL_CIRCUIT_SAMPLE, crate::quantum_fabric::KERNEL_CIRCUIT_APPLY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reference_distribution_is_the_states_weights_normalised() {
|
||||
let mut bell = Circuit::new(2);
|
||||
bell.h(0).cx(0, 1);
|
||||
let p = reference_distribution(&bell).unwrap();
|
||||
assert_eq!(p.len(), 4);
|
||||
assert!((p[0] - 0.5).abs() < 1e-9 && (p[3] - 0.5).abs() < 1e-9 && p[1] == 0.0 && p[2] == 0.0, "{p:?}");
|
||||
let qft = Circuit::qft(4);
|
||||
let p = reference_distribution(&qft).unwrap();
|
||||
assert!((p.iter().sum::<f64>() - 1.0).abs() < 1e-12);
|
||||
assert!(p.iter().all(|&x| (x - 1.0 / 16.0).abs() < 1e-6), "the QFT of |0⟩ is uniform");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_qpu_corpus_is_small_shallow_singly_controlled_and_the_same_in_both_gatesets() {
|
||||
let v1 = QPU_DEFAULT.circuits(Gateset::V1);
|
||||
let v2 = QPU_DEFAULT.circuits(Gateset::V2);
|
||||
assert_eq!(v1.len(), QPU_DEFAULT.cases);
|
||||
assert_eq!(v1.iter().map(|c| (&c.ops, c.n_qubits)).collect::<Vec<_>>(), v2.iter().map(|c| (&c.ops, c.n_qubits)).collect::<Vec<_>>());
|
||||
for c in v1.iter().chain(&v2) {
|
||||
assert!(c.validate().is_ok(), "{c:?}");
|
||||
assert!((2..=QPU_DEFAULT.max_qubits).contains(&c.n_qubits));
|
||||
assert!(c.ops.iter().all(|g| g.controls.len() <= 1 && g.param <= 4));
|
||||
}
|
||||
assert_eq!(QPU_DEFAULT.circuits(Gateset::V2), v2, "deterministic");
|
||||
assert_eq!(QPU_DEFAULT.generator(), "qpu-v1 seed=7 cases=16 qubits=5 depth=12");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cpu_sampler_is_deterministic_and_certifies_near_one() {
|
||||
let s = CpuSampler { seed: 0x5a };
|
||||
let qft = Circuit::qft(3);
|
||||
let a = ready(s.sample(REFERENCE, &qft, 1000, &mut crate::quantum_fabric::Unmetered)).unwrap().unwrap().0;
|
||||
let b = ready(s.sample(REFERENCE, &qft, 1000, &mut crate::quantum_fabric::Unmetered)).unwrap().unwrap().0;
|
||||
assert_eq!(a, b);
|
||||
assert_eq!(a.iter().sum::<u64>(), 1000);
|
||||
assert!(ready(s.sample("other", &qft, 10, &mut crate::quantum_fabric::Unmetered)).unwrap().is_err());
|
||||
for gs in Gateset::ALL {
|
||||
let cert = ready(certify_sampler(&s, REFERENCE, gs, QPU_DEFAULT, 4000, Provenance::Silicon)).unwrap().unwrap();
|
||||
let Parity::Statistical { fidelity, shots } = cert.parity else { panic!("{:?}", cert.parity) };
|
||||
assert_eq!(shots, 4000);
|
||||
assert!(fidelity > 0.99, "{gs:?}: {fidelity}");
|
||||
assert_eq!((cert.cases, cert.kernel.as_str()), (QPU_DEFAULT.cases as u64, sample_kernel(gs)));
|
||||
}
|
||||
}
|
||||
|
||||
/// A sampler that returns what it is told to.
|
||||
struct Fixed(Counts);
|
||||
|
||||
impl Sampler for Fixed {
|
||||
fn member_id(&self) -> String {
|
||||
"local/test".into()
|
||||
}
|
||||
|
||||
fn sample<'r>(&'r self, _: &'r str, _: &'r Circuit, _: u64, _: &'r mut dyn RunMeter) -> SampleFuture<'r> {
|
||||
let counts = self.0.clone();
|
||||
Box::pin(async move { Ok((counts, 0, None)) })
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_of_the_wrong_shape_stop_a_certification() {
|
||||
let short = ready(certify_sampler(&Fixed(vec![1, 1]), "x", Gateset::V2, QPU_DEFAULT, 2, Provenance::Silicon)).unwrap();
|
||||
assert!(matches!(short, Err(MemberError::Other(_))), "{short:?}");
|
||||
let mut bell = Circuit::new(2);
|
||||
bell.h(0).cx(0, 1);
|
||||
assert!(check_counts(&bell, 10, &[5, 0, 0, 5]).is_ok());
|
||||
assert!(check_counts(&bell, 10, &[5, 0, 0, 4]).is_err());
|
||||
assert!(check_counts(&bell, 10, &[10, 0, 0]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_counts_hash_binds_circuit_shots_and_every_count() {
|
||||
let mut bell = Circuit::new(2);
|
||||
bell.h(0).cx(0, 1);
|
||||
let h = counts_hash(&bell, 10, &[5, 0, 0, 5]).unwrap();
|
||||
assert_ne!(counts_hash(&bell, 10, &[4, 0, 0, 6]), Some(h));
|
||||
assert_ne!(counts_hash(&bell, 11, &[5, 0, 0, 5]), Some(h));
|
||||
let mut other = bell.clone();
|
||||
other.gateset = Gateset::V1;
|
||||
assert_ne!(counts_hash(&other, 10, &[5, 0, 0, 5]), Some(h));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placement_keeps_the_reference_and_puts_silicon_before_a_simulator() {
|
||||
let local = local_sampling_member();
|
||||
let simulated = |id: &str, fidelity| {
|
||||
let mut entries = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
let cert = Certificate {
|
||||
kernel: sample_kernel(gs).into(),
|
||||
implementation: "q".into(),
|
||||
member: id.into(),
|
||||
generator: QPU_DEFAULT.generator(),
|
||||
parity: Parity::Statistical { fidelity, shots: 1000 },
|
||||
cases: 16,
|
||||
corpus: [3; 32],
|
||||
provenance: Provenance::Silicon,
|
||||
};
|
||||
entries.push(KernelEntry { kernel: sample_kernel(gs).into(), implementation: "q".into(), certificate: cert, energy: None });
|
||||
}
|
||||
sampling_member(id.into(), FabricClass::Qpu, String::new(), entries)
|
||||
};
|
||||
let need = Parity::Statistical { fidelity: 0.9, shots: 1000 };
|
||||
// A silicon QPU whose id sorts first, and a simulator.
|
||||
let qpu = simulated("a/qpu", 0.95);
|
||||
let mut sim = simulated("a/aaa-sim", 0.999);
|
||||
sim.kernels.iter_mut().for_each(|k| k.certificate.provenance = Provenance::Simulator("test 1".into()));
|
||||
for gs in Gateset::ALL {
|
||||
let all = [local.clone(), qpu.clone(), sim.clone()];
|
||||
let (placed, _) = place(&all, sample_kernel(gs), &need).unwrap();
|
||||
assert_eq!(placed.id, qpu.id, "silicon first, whatever the ids");
|
||||
let pair = [sim.clone(), local.clone()];
|
||||
let (placed, _) = place(&pair, sample_kernel(gs), &need).unwrap();
|
||||
assert_eq!(placed.id, local.id, "the reference before a simulator");
|
||||
let (placed, _) = place(std::slice::from_ref(&sim), sample_kernel(gs), &need).unwrap();
|
||||
assert_eq!(placed.id, sim.id, "a simulator alone is placed");
|
||||
// A requirement above a member's fidelity leaves it out.
|
||||
let strict = Parity::Statistical { fidelity: 0.97, shots: 1000 };
|
||||
assert!(place(std::slice::from_ref(&qpu), sample_kernel(gs), &strict).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
116
wai/wai-quantum/src/quantum_fabric/sv64.cl
Normal file
116
wai/wai-quantum/src/quantum_fabric/sv64.cl
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// wai-quantum fabric: the dense f64 state vector's gate kernels for OpenCL.
|
||||
//
|
||||
// A transcription of `quantum_sv`'s three formulas, with contraction into
|
||||
// fused multiply-add off, so every operation is one IEEE binary64 operation rounded to
|
||||
// nearest, in the order the CPU performs it:
|
||||
//
|
||||
// - a diagonal gate multiplies amplitude x by its diagonal entry, unless the
|
||||
// entry is exactly 1 (then the amplitude is left as it is: multiplying by 1
|
||||
// could flip the sign of a zero);
|
||||
// - a gate on one qubit applies its 2x2 matrix to each pair, left to right:
|
||||
// m00*a - m00i*ai + m01*b - m01i*bi;
|
||||
// - a gate on k >= 2 qubits computes each output of a group as a row of the
|
||||
// matrix against the group's members in local-index order, accumulated from
|
||||
// zero in that order.
|
||||
//
|
||||
// (`local` is an address space in OpenCL C, so the local index is `li`.)
|
||||
//
|
||||
// Each thread owns one amplitude, pair or group, and a launch is one gate, so
|
||||
// no thread reads what another writes within a launch.
|
||||
|
||||
#pragma OPENCL EXTENSION cl_khr_fp64 : enable
|
||||
#pragma OPENCL FP_CONTRACT OFF
|
||||
|
||||
typedef ulong u64;
|
||||
|
||||
// The maximum group size of a gate on k qubits: 2^k <= SV_MAX_D.
|
||||
#define SV_MAX_D 64
|
||||
|
||||
__kernel void sv_diag(
|
||||
__global double* restrict re,
|
||||
__global double* restrict im,
|
||||
u64 len,
|
||||
__global const uint* restrict bitpos,
|
||||
uint k,
|
||||
__global const double* restrict diag)
|
||||
{
|
||||
u64 x = (u64)get_global_id(0);
|
||||
u64 stride = (u64)get_global_size(0);
|
||||
for (; x < len; x += stride) {
|
||||
u64 li = 0;
|
||||
for (uint b = 0; b < k; b++) {
|
||||
li = (li << 1) | ((x >> bitpos[b]) & 1);
|
||||
}
|
||||
double cr = diag[2 * li];
|
||||
double ci = diag[2 * li + 1];
|
||||
if (cr == 1.0 && ci == 0.0) {
|
||||
continue;
|
||||
}
|
||||
double ar = re[x];
|
||||
double ai = im[x];
|
||||
re[x] = cr * ar - ci * ai;
|
||||
im[x] = cr * ai + ci * ar;
|
||||
}
|
||||
}
|
||||
|
||||
__kernel void sv_one(
|
||||
__global double* restrict re,
|
||||
__global double* restrict im,
|
||||
u64 pairs,
|
||||
u64 s,
|
||||
double m00r, double m00i, double m01r, double m01i,
|
||||
double m10r, double m10i, double m11r, double m11i)
|
||||
{
|
||||
u64 p = (u64)get_global_id(0);
|
||||
u64 stride = (u64)get_global_size(0);
|
||||
for (; p < pairs; p += stride) {
|
||||
u64 low = p & (s - 1);
|
||||
u64 i = ((p - low) << 1) | low;
|
||||
u64 j = i + s;
|
||||
double ar = re[i], ai = im[i], br = re[j], bi = im[j];
|
||||
re[i] = m00r * ar - m00i * ai + m01r * br - m01i * bi;
|
||||
im[i] = m00r * ai + m00i * ar + m01r * bi + m01i * br;
|
||||
re[j] = m10r * ar - m10i * ai + m11r * br - m11i * bi;
|
||||
im[j] = m10r * ai + m10i * ar + m11r * bi + m11i * br;
|
||||
}
|
||||
}
|
||||
|
||||
__kernel void sv_many(
|
||||
__global double* restrict re,
|
||||
__global double* restrict im,
|
||||
u64 groups,
|
||||
__global const uint* restrict sorted,
|
||||
uint k,
|
||||
__global const u64* restrict offsets,
|
||||
__global const double* restrict m)
|
||||
{
|
||||
u64 t = (u64)get_global_id(0);
|
||||
u64 stride = (u64)get_global_size(0);
|
||||
uint d = 1u << k;
|
||||
double vr[SV_MAX_D];
|
||||
double vi[SV_MAX_D];
|
||||
for (; t < groups; t += stride) {
|
||||
// t with a zero inserted at each target position, ascending.
|
||||
u64 base = t;
|
||||
for (uint b = 0; b < k; b++) {
|
||||
u64 pos = sorted[b];
|
||||
u64 low = base & (((u64)1 << pos) - 1);
|
||||
base = ((base >> pos) << (pos + 1)) | low;
|
||||
}
|
||||
for (uint j = 0; j < d; j++) {
|
||||
vr[j] = re[base | offsets[j]];
|
||||
vi[j] = im[base | offsets[j]];
|
||||
}
|
||||
for (uint r = 0; r < d; r++) {
|
||||
double sr = 0.0, si = 0.0;
|
||||
for (uint c = 0; c < d; c++) {
|
||||
double er = m[2 * (r * d + c)];
|
||||
double ei = m[2 * (r * d + c) + 1];
|
||||
sr += er * vr[c] - ei * vi[c];
|
||||
si += er * vi[c] + ei * vr[c];
|
||||
}
|
||||
re[base | offsets[r]] = sr;
|
||||
im[base | offsets[r]] = si;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
wai/wai-quantum/src/quantum_fabric/sv64.cu
Normal file
111
wai/wai-quantum/src/quantum_fabric/sv64.cu
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// wai-quantum fabric: the dense f64 state vector's gate kernels for CUDA.
|
||||
//
|
||||
// A transcription of `quantum_sv`'s three formulas, built without fused
|
||||
// multiply-add, so every operation is one IEEE binary64 operation rounded to
|
||||
// nearest, in the order the CPU performs it:
|
||||
//
|
||||
// - a diagonal gate multiplies amplitude x by its diagonal entry, unless the
|
||||
// entry is exactly 1 (then the amplitude is left as it is: multiplying by 1
|
||||
// could flip the sign of a zero);
|
||||
// - a gate on one qubit applies its 2x2 matrix to each pair, left to right:
|
||||
// m00*a - m00i*ai + m01*b - m01i*bi;
|
||||
// - a gate on k >= 2 qubits computes each output of a group as a row of the
|
||||
// matrix against the group's members in local-index order, accumulated from
|
||||
// zero in that order.
|
||||
//
|
||||
// Each thread owns one amplitude, pair or group, and a launch is one gate, so
|
||||
// no thread reads what another writes within a launch.
|
||||
|
||||
typedef unsigned long long u64;
|
||||
|
||||
// The maximum group size of a gate on k qubits: 2^k <= SV_MAX_D.
|
||||
#define SV_MAX_D 64
|
||||
|
||||
extern "C" __global__ void sv_diag(
|
||||
double* __restrict__ re,
|
||||
double* __restrict__ im,
|
||||
u64 len,
|
||||
const unsigned int* __restrict__ bitpos,
|
||||
unsigned int k,
|
||||
const double* __restrict__ diag)
|
||||
{
|
||||
u64 x = (u64)blockIdx.x * (u64)blockDim.x + (u64)threadIdx.x;
|
||||
u64 stride = (u64)gridDim.x * (u64)blockDim.x;
|
||||
for (; x < len; x += stride) {
|
||||
u64 local = 0;
|
||||
for (unsigned int b = 0; b < k; b++) {
|
||||
local = (local << 1) | ((x >> bitpos[b]) & 1);
|
||||
}
|
||||
double cr = diag[2 * local];
|
||||
double ci = diag[2 * local + 1];
|
||||
if (cr == 1.0 && ci == 0.0) {
|
||||
continue;
|
||||
}
|
||||
double ar = re[x];
|
||||
double ai = im[x];
|
||||
re[x] = cr * ar - ci * ai;
|
||||
im[x] = cr * ai + ci * ar;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" __global__ void sv_one(
|
||||
double* __restrict__ re,
|
||||
double* __restrict__ im,
|
||||
u64 pairs,
|
||||
u64 s,
|
||||
double m00r, double m00i, double m01r, double m01i,
|
||||
double m10r, double m10i, double m11r, double m11i)
|
||||
{
|
||||
u64 p = (u64)blockIdx.x * (u64)blockDim.x + (u64)threadIdx.x;
|
||||
u64 stride = (u64)gridDim.x * (u64)blockDim.x;
|
||||
for (; p < pairs; p += stride) {
|
||||
u64 low = p & (s - 1);
|
||||
u64 i = ((p - low) << 1) | low;
|
||||
u64 j = i + s;
|
||||
double ar = re[i], ai = im[i], br = re[j], bi = im[j];
|
||||
re[i] = m00r * ar - m00i * ai + m01r * br - m01i * bi;
|
||||
im[i] = m00r * ai + m00i * ar + m01r * bi + m01i * br;
|
||||
re[j] = m10r * ar - m10i * ai + m11r * br - m11i * bi;
|
||||
im[j] = m10r * ai + m10i * ar + m11r * bi + m11i * br;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" __global__ void sv_many(
|
||||
double* __restrict__ re,
|
||||
double* __restrict__ im,
|
||||
u64 groups,
|
||||
const unsigned int* __restrict__ sorted,
|
||||
unsigned int k,
|
||||
const u64* __restrict__ offsets,
|
||||
const double* __restrict__ m)
|
||||
{
|
||||
u64 t = (u64)blockIdx.x * (u64)blockDim.x + (u64)threadIdx.x;
|
||||
u64 stride = (u64)gridDim.x * (u64)blockDim.x;
|
||||
unsigned int d = 1u << k;
|
||||
double vr[SV_MAX_D];
|
||||
double vi[SV_MAX_D];
|
||||
for (; t < groups; t += stride) {
|
||||
// t with a zero inserted at each target position, ascending.
|
||||
u64 base = t;
|
||||
for (unsigned int b = 0; b < k; b++) {
|
||||
u64 pos = sorted[b];
|
||||
u64 low = base & ((1ull << pos) - 1);
|
||||
base = ((base >> pos) << (pos + 1)) | low;
|
||||
}
|
||||
for (unsigned int j = 0; j < d; j++) {
|
||||
vr[j] = re[base | offsets[j]];
|
||||
vi[j] = im[base | offsets[j]];
|
||||
}
|
||||
for (unsigned int r = 0; r < d; r++) {
|
||||
double sr = 0.0, si = 0.0;
|
||||
for (unsigned int c = 0; c < d; c++) {
|
||||
double er = m[2 * (r * d + c)];
|
||||
double ei = m[2 * (r * d + c) + 1];
|
||||
sr += er * vr[c] - ei * vi[c];
|
||||
si += er * vi[c] + ei * vr[c];
|
||||
}
|
||||
re[base | offsets[r]] = sr;
|
||||
im[base | offsets[r]] = si;
|
||||
}
|
||||
}
|
||||
}
|
||||
452
wai/wai-quantum/src/quantum_fabric/sv64.rs
Normal file
452
wai/wai-quantum/src/quantum_fabric/sv64.rs
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
//! The dense f64 state vector on an accelerator, bit for bit: what
|
||||
//! [`crate::quantum_sv`] computes, planned as launches an accelerator runs.
|
||||
//!
|
||||
//! [`crate::quantum_sv::StateVector::run`] applies each gate with one of three
|
||||
//! formulas, whichever thread count or memory layout computes it:
|
||||
//!
|
||||
//! - **a diagonal gate** multiplies each amplitude by its diagonal entry,
|
||||
//! leaving it as it is where the entry is exactly 1 (multiplying by 1 can
|
||||
//! flip the sign of a zero);
|
||||
//! - **a gate on one qubit** applies the explicit 2×2 formula
|
||||
//! `m00·a − m00ᵢ·aᵢ + m01·b − m01ᵢ·bᵢ`, left to right;
|
||||
//! - **a gate on two or more qubits** computes each output as a matrix row
|
||||
//! against the group's members in local-index order, accumulated from zero.
|
||||
//!
|
||||
//! [`plan`] turns a gate list into those launches ([`Launch`]), exactly as the
|
||||
//! CPU decides them; [`replay`] runs them on the CPU, and must equal
|
||||
//! [`StateVector::run`](crate::quantum_sv::StateVector::run) bit for bit, which
|
||||
//! the tests check over a corpus. An accelerator kernel that performs the same
|
||||
//! IEEE binary64 operations in the same order, with no fused multiply-add,
|
||||
//! returns the same bits; its certificate says whether it did.
|
||||
//!
|
||||
//! The kernel for CUDA devices is [`SOURCE`] (`sv64.cu`).
|
||||
|
||||
use crate::gates::Gate;
|
||||
use crate::linalg::C;
|
||||
use crate::quantum_fabric::Output;
|
||||
use crate::quantum_sv::{StateVector, SvError, fuse};
|
||||
|
||||
/// The kernel this module certifies: a gate list run on a dense f64 state.
|
||||
pub const KERNEL_SV_RUN: &str = "wai.quantum.sv/run";
|
||||
|
||||
/// The CUDA kernels, `sv64.cu`, exactly as they are compiled.
|
||||
pub const SOURCE: &str = include_str!("sv64.cu");
|
||||
|
||||
/// The implementation name the CUDA id starts with.
|
||||
pub const IMPLEMENTATION: &str = "cuda-sv64";
|
||||
|
||||
/// Version of the host-side layout: separate real and imaginary arrays, one
|
||||
/// launch per planned gate, the launch kinds and their parameters. Part of the
|
||||
/// implementation id.
|
||||
pub const HOST_LAYOUT: u32 = 1;
|
||||
|
||||
/// The largest gate a launch takes: `2^k ≤ MAX_D` (the kernel's `SV_MAX_D`).
|
||||
pub const MAX_D: usize = 64;
|
||||
|
||||
/// The compile options: no fused multiply-add, so each operation rounds once.
|
||||
pub const COMPILE_OPTIONS: [&str; 2] = ["--std=c++17", "--fmad=false"];
|
||||
|
||||
const DOMAIN_IMPLEMENTATION: &[u8] = b"wai:quantum-fabric-sv64\x01";
|
||||
|
||||
fn put(o: &mut Vec<u8>, bytes: &[u8]) {
|
||||
o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
|
||||
o.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
/// `"cuda-sv64/<16 hex>"` over the kernel text, the options and the layout.
|
||||
pub fn implementation_id_of(source: &str) -> String {
|
||||
let mut o = DOMAIN_IMPLEMENTATION.to_vec();
|
||||
put(&mut o, source.as_bytes());
|
||||
for option in COMPILE_OPTIONS {
|
||||
put(&mut o, option.as_bytes());
|
||||
}
|
||||
o.extend_from_slice(&HOST_LAYOUT.to_be_bytes());
|
||||
let h: String = blake3::hash(&o).as_bytes()[..8].iter().map(|b| format!("{b:02x}")).collect();
|
||||
format!("{IMPLEMENTATION}/{h}")
|
||||
}
|
||||
|
||||
pub fn implementation_id() -> String {
|
||||
implementation_id_of(SOURCE)
|
||||
}
|
||||
|
||||
/// The OpenCL kernels, `sv64.cl`: the same formulas, with floating-point
|
||||
/// contraction off (`#pragma OPENCL FP_CONTRACT OFF`), for devices with
|
||||
/// `cl_khr_fp64`.
|
||||
pub const CL_SOURCE: &str = include_str!("sv64.cl");
|
||||
|
||||
/// The implementation name the OpenCL id starts with.
|
||||
pub const CL_IMPLEMENTATION: &str = "opencl-sv64";
|
||||
|
||||
/// The OpenCL build options.
|
||||
pub const CL_BUILD_OPTIONS: &str = "-cl-std=CL1.2";
|
||||
|
||||
const DOMAIN_CL_IMPLEMENTATION: &[u8] = b"wai:quantum-fabric-sv64-opencl\x01";
|
||||
|
||||
/// `"opencl-sv64/<16 hex>"` over the kernel text, the options and the layout.
|
||||
pub fn cl_implementation_id_of(source: &str) -> String {
|
||||
let mut o = DOMAIN_CL_IMPLEMENTATION.to_vec();
|
||||
put(&mut o, source.as_bytes());
|
||||
put(&mut o, CL_BUILD_OPTIONS.as_bytes());
|
||||
o.extend_from_slice(&HOST_LAYOUT.to_be_bytes());
|
||||
let h: String = blake3::hash(&o).as_bytes()[..8].iter().map(|b| format!("{b:02x}")).collect();
|
||||
format!("{CL_IMPLEMENTATION}/{h}")
|
||||
}
|
||||
|
||||
pub fn cl_implementation_id() -> String {
|
||||
cl_implementation_id_of(CL_SOURCE)
|
||||
}
|
||||
|
||||
/// One planned gate.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Launch {
|
||||
/// A diagonal gate: the index bit of each gate qubit (most significant
|
||||
/// first) and the diagonal.
|
||||
Diag { bitpos: Vec<u32>, diag: Vec<C> },
|
||||
/// A gate on one qubit at index bit `s.trailing_zeros()`: its matrix
|
||||
/// `[m00, m01, m10, m11]`.
|
||||
One { s: u64, m: [C; 4] },
|
||||
/// A gate on `k ≥ 2` qubits: the target bits ascending, each local index's
|
||||
/// offset, and the `2^k × 2^k` matrix, row-major.
|
||||
Many { sorted: Vec<u32>, offsets: Vec<u64>, m: Vec<C> },
|
||||
}
|
||||
|
||||
/// The launches [`StateVector::run`](crate::quantum_sv::StateVector::run) on
|
||||
/// `n` qubits with `width` performs for `gates`: the same checks, the same
|
||||
/// fusion, and each gate's formula. A gate on more than `log2(MAX_D)` qubits
|
||||
/// is refused.
|
||||
pub fn plan(n: u32, gates: &[Gate], width: usize) -> Result<Vec<Launch>, SvError> {
|
||||
for (i, g) in gates.iter().enumerate() {
|
||||
let k = g.qubits.len();
|
||||
let mut q = g.qubits.clone();
|
||||
q.sort_unstable();
|
||||
q.dedup();
|
||||
if k == 0 || q.len() != k || g.qubits.iter().any(|&x| x >= n) || g.matrix.len() != 1 << (2 * k) {
|
||||
return Err(SvError::BadGate(i));
|
||||
}
|
||||
}
|
||||
let fused;
|
||||
let list = if width >= 2 {
|
||||
fused = fuse(gates, width);
|
||||
&fused
|
||||
} else {
|
||||
gates
|
||||
};
|
||||
let mut out = Vec::with_capacity(list.len());
|
||||
for (i, g) in list.iter().enumerate() {
|
||||
let k = g.qubits.len();
|
||||
if 1usize << k > MAX_D {
|
||||
return Err(SvError::BadGate(i));
|
||||
}
|
||||
let bitpos: Vec<u32> = g.qubits.iter().map(|&q| n - 1 - q).collect();
|
||||
let d = 1usize << k;
|
||||
if g.is_diagonal() {
|
||||
out.push(Launch::Diag { diag: (0..d).map(|i| g.matrix[i * d + i]).collect(), bitpos });
|
||||
continue;
|
||||
}
|
||||
let offsets: Vec<u64> = (0..d).map(|local| bitpos.iter().enumerate().fold(0u64, |o, (i, &p)| o | ((((local >> (k - 1 - i)) & 1) as u64) << p))).collect();
|
||||
if k == 1 {
|
||||
out.push(Launch::One { s: offsets[1], m: [g.matrix[0], g.matrix[1], g.matrix[2], g.matrix[3]] });
|
||||
} else {
|
||||
let mut sorted = bitpos;
|
||||
sorted.sort_unstable();
|
||||
out.push(Launch::Many { sorted, offsets, m: g.matrix.clone() });
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `t` with a zero inserted at each position of `pos`, ascending.
|
||||
fn spread(mut t: u64, pos: &[u32]) -> u64 {
|
||||
for &p in pos {
|
||||
let low = t & ((1u64 << p) - 1);
|
||||
t = ((t >> p) << (p + 1)) | low;
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// Runs `launches` on `(re, im)` on the CPU, as a kernel does: each formula
|
||||
/// exactly as [`SOURCE`] writes it.
|
||||
pub fn replay(re: &mut [f64], im: &mut [f64], launches: &[Launch]) {
|
||||
let len = re.len() as u64;
|
||||
for l in launches {
|
||||
match l {
|
||||
Launch::Diag { bitpos, diag } => {
|
||||
for x in 0..len {
|
||||
let local = bitpos.iter().fold(0u64, |a, &p| (a << 1) | ((x >> p) & 1));
|
||||
let c = diag[local as usize];
|
||||
if c.re == 1.0 && c.im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let (ar, ai) = (re[x as usize], im[x as usize]);
|
||||
re[x as usize] = c.re * ar - c.im * ai;
|
||||
im[x as usize] = c.re * ai + c.im * ar;
|
||||
}
|
||||
}
|
||||
Launch::One { s, m } => {
|
||||
let [m00, m01, m10, m11] = *m;
|
||||
for p in 0..len / 2 {
|
||||
let low = p & (s - 1);
|
||||
let i = (((p - low) << 1) | low) as usize;
|
||||
let j = i + *s as usize;
|
||||
let (ar, ai, br, bi) = (re[i], im[i], re[j], im[j]);
|
||||
re[i] = m00.re * ar - m00.im * ai + m01.re * br - m01.im * bi;
|
||||
im[i] = m00.re * ai + m00.im * ar + m01.re * bi + m01.im * br;
|
||||
re[j] = m10.re * ar - m10.im * ai + m11.re * br - m11.im * bi;
|
||||
im[j] = m10.re * ai + m10.im * ar + m11.re * bi + m11.im * br;
|
||||
}
|
||||
}
|
||||
Launch::Many { sorted, offsets, m } => {
|
||||
let d = offsets.len();
|
||||
let (mut vr, mut vi) = (vec![0.0; d], vec![0.0; d]);
|
||||
for t in 0..len >> sorted.len() {
|
||||
let base = spread(t, sorted);
|
||||
for (j, &o) in offsets.iter().enumerate() {
|
||||
vr[j] = re[(base | o) as usize];
|
||||
vi[j] = im[(base | o) as usize];
|
||||
}
|
||||
for (r, &o) in offsets.iter().enumerate() {
|
||||
let (mut sr, mut si) = (0.0, 0.0);
|
||||
for c in 0..d {
|
||||
let e = m[r * d + c];
|
||||
sr += e.re * vr[c] - e.im * vi[c];
|
||||
si += e.re * vi[c] + e.im * vr[c];
|
||||
}
|
||||
re[(base | o) as usize] = sr;
|
||||
im[(base | o) as usize] = si;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A corpus of gate lists: the state vector's named gates at angles drawn
|
||||
/// from a seed, run at fusion widths 0, 2 and 3 in turn.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SvCorpus {
|
||||
pub seed: u64,
|
||||
pub cases: usize,
|
||||
pub max_qubits: u32,
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
/// The default: 48 cases of 2 to 10 qubits, 30 gates each.
|
||||
pub const SV_DEFAULT: SvCorpus = SvCorpus { seed: 7, cases: 48, max_qubits: 10, depth: 30 };
|
||||
|
||||
/// The corpus digest of [`SV_DEFAULT`] when the device's state is the CPU's on
|
||||
/// every case: what a bit-exact certificate reproduces.
|
||||
pub const SV_DEFAULT_PIN: &str = "b7d129d0593adf70dc164477664970c5c0b8c09f79f1a0573036bd60888dbcd1";
|
||||
|
||||
/// One case: the width, the fusion width and the gates.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SvCase {
|
||||
pub n: u32,
|
||||
pub width: usize,
|
||||
pub gates: Vec<Gate>,
|
||||
}
|
||||
|
||||
fn next(s: &mut u64) -> u64 {
|
||||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = *s;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
impl SvCorpus {
|
||||
/// `"sv64-v1 seed=… cases=… qubits=… depth=…"`.
|
||||
pub fn generator(&self) -> String {
|
||||
format!("sv64-v1 seed={} cases={} qubits={} depth={}", self.seed, self.cases, self.max_qubits, self.depth)
|
||||
}
|
||||
|
||||
pub fn cases(&self) -> Vec<SvCase> {
|
||||
let mut s = self.seed;
|
||||
(0..self.cases)
|
||||
.map(|i| {
|
||||
let n = 2 + (next(&mut s) % u64::from(self.max_qubits.max(2) - 1)) as u32;
|
||||
let width = [0usize, 2, 3][i % 3];
|
||||
let mut gates = Vec::with_capacity(self.depth);
|
||||
for _ in 0..self.depth {
|
||||
let a = (next(&mut s) % u64::from(n)) as u32;
|
||||
let mut b = (next(&mut s) % u64::from(n)) as u32;
|
||||
if b == a {
|
||||
b = (a + 1) % n;
|
||||
}
|
||||
let angle = (next(&mut s) >> 11) as f64 / (1u64 << 53) as f64 * 6.5 - 0.1;
|
||||
gates.push(match next(&mut s) % 9 {
|
||||
0 => Gate::rx(a, angle),
|
||||
1 => Gate::ry(a, angle),
|
||||
2 => Gate::rz(a, angle),
|
||||
3 => Gate::sqrt_w(a),
|
||||
4 => Gate::t(a),
|
||||
5 => Gate::h(a),
|
||||
6 => Gate::fsim(a, b, angle, 0.5 * angle),
|
||||
7 => Gate::cz(a, b),
|
||||
_ => Gate::iswap(a, b),
|
||||
});
|
||||
}
|
||||
SvCase { n, width, gates }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl SvCase {
|
||||
/// The case as bytes: its width, fusion width, and each gate's qubits and
|
||||
/// matrix bits.
|
||||
pub fn bytes(&self) -> Vec<u8> {
|
||||
let mut o = Vec::new();
|
||||
o.extend_from_slice(&self.n.to_le_bytes());
|
||||
o.extend_from_slice(&(self.width as u64).to_le_bytes());
|
||||
for g in &self.gates {
|
||||
o.extend_from_slice(&(g.qubits.len() as u64).to_le_bytes());
|
||||
for q in &g.qubits {
|
||||
o.extend_from_slice(&q.to_le_bytes());
|
||||
}
|
||||
for c in &g.matrix {
|
||||
o.extend_from_slice(&c.re.to_bits().to_le_bytes());
|
||||
o.extend_from_slice(&c.im.to_bits().to_le_bytes());
|
||||
}
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// The CPU's state after the case, as an output: real then imaginary
|
||||
/// parts, interleaved by amplitude.
|
||||
pub fn reference(&self) -> Result<Output, SvError> {
|
||||
let mut sv = StateVector::zero(self.n)?;
|
||||
sv.run(&self.gates, self.width)?;
|
||||
Ok(state_output(&sv.re, &sv.im))
|
||||
}
|
||||
}
|
||||
|
||||
/// A state as a certificate output.
|
||||
pub fn state_output(re: &[f64], im: &[f64]) -> Output {
|
||||
Output::Float(re.iter().zip(im).flat_map(|(&a, &b)| [a, b]).collect())
|
||||
}
|
||||
|
||||
/// The mutants a conforming device must be caught running: each changes one
|
||||
/// operation's order or one formula, so its bits differ from the CPU's.
|
||||
pub const MUTANTS: [crate::quantum_fabric::wgsl::Mutant; 4] = [
|
||||
// The identity skip is lost: a zero's sign may flip.
|
||||
crate::quantum_fabric::wgsl::Mutant { name: "S1", find: " if (cr == 1.0 && ci == 0.0) {\n continue;\n }\n", replace: "", detector: crate::quantum_fabric::wgsl::Detector::Certificate },
|
||||
// The one-qubit formula is reassociated.
|
||||
crate::quantum_fabric::wgsl::Mutant { name: "S2", find: "re[i] = m00r * ar - m00i * ai + m01r * br - m01i * bi;", replace: "re[i] = (m00r * ar - m00i * ai) + (m01r * br - m01i * bi);", detector: crate::quantum_fabric::wgsl::Detector::Certificate },
|
||||
// The row's real part is reassociated: (sr + a) − b, not sr + (a − b).
|
||||
// (Swapping an addition's operands would not do: IEEE addition commutes.)
|
||||
crate::quantum_fabric::wgsl::Mutant { name: "S3", find: "sr += er * vr[c] - ei * vi[c];", replace: "sr += er * vr[c];\n sr -= ei * vi[c];", detector: crate::quantum_fabric::wgsl::Detector::Certificate },
|
||||
// The imaginary row term's sign is wrong.
|
||||
crate::quantum_fabric::wgsl::Mutant { name: "S4", find: "si += er * vi[c] + ei * vr[c];", replace: "si += er * vi[c] - ei * vr[c];", detector: crate::quantum_fabric::wgsl::Detector::Certificate },
|
||||
];
|
||||
|
||||
/// [`SOURCE`] with a mutant applied. Panics unless its `find` occurs exactly
|
||||
/// once.
|
||||
pub fn mutate(m: &crate::quantum_fabric::wgsl::Mutant) -> String {
|
||||
assert_eq!(SOURCE.matches(m.find).count(), 1, "mutant {}", m.name);
|
||||
SOURCE.replacen(m.find, m.replace, 1)
|
||||
}
|
||||
|
||||
/// [`CL_SOURCE`] with a mutant applied: the same edits, which the two texts
|
||||
/// share.
|
||||
pub fn mutate_cl(m: &crate::quantum_fabric::wgsl::Mutant) -> String {
|
||||
assert_eq!(CL_SOURCE.matches(m.find).count(), 1, "mutant {}", m.name);
|
||||
CL_SOURCE.replacen(m.find, m.replace, 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn bits(o: &Output) -> Vec<u64> {
|
||||
match o {
|
||||
Output::Float(v) => v.iter().map(|x| x.to_bits()).collect(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_replayed_plan_is_the_cpu_run_bit_for_bit() {
|
||||
let mut kinds = [0usize; 3];
|
||||
for case in SV_DEFAULT.cases() {
|
||||
let launches = plan(case.n, &case.gates, case.width).unwrap();
|
||||
for l in &launches {
|
||||
kinds[match l {
|
||||
Launch::Diag { .. } => 0,
|
||||
Launch::One { .. } => 1,
|
||||
Launch::Many { .. } => 2,
|
||||
}] += 1;
|
||||
}
|
||||
let dim = 1usize << case.n;
|
||||
let (mut re, mut im) = (vec![0.0; dim], vec![0.0; dim]);
|
||||
re[0] = 1.0;
|
||||
replay(&mut re, &mut im, &launches);
|
||||
assert_eq!(bits(&state_output(&re, &im)), bits(&case.reference().unwrap()), "n={} width={}", case.n, case.width);
|
||||
}
|
||||
assert!(kinds.iter().all(|&k| k > 10), "every launch kind is exercised: {kinds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cpu_reproduces_the_pinned_digest() {
|
||||
let mut cert = crate::quantum_fabric::Certification::new();
|
||||
for case in SV_DEFAULT.cases() {
|
||||
let r = case.reference().unwrap();
|
||||
cert.add(&case.bytes(), &r, &r);
|
||||
}
|
||||
let c = cert.finish(KERNEL_SV_RUN, "cpu", "cpu", &SV_DEFAULT.generator());
|
||||
let hex: String = c.corpus.iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(hex, SV_DEFAULT_PIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_replay_with_a_formula_changed_is_caught() {
|
||||
// The one-qubit formula, reassociated: the corpus sees the difference.
|
||||
let differs = SV_DEFAULT.cases().iter().any(|case| {
|
||||
let launches = plan(case.n, &case.gates, case.width).unwrap();
|
||||
let dim = 1usize << case.n;
|
||||
let (mut re, mut im) = (vec![0.0; dim], vec![0.0; dim]);
|
||||
re[0] = 1.0;
|
||||
for l in &launches {
|
||||
if let Launch::One { s, m } = l {
|
||||
let [m00, m01, m10, m11] = *m;
|
||||
for p in 0..dim as u64 / 2 {
|
||||
let low = p & (s - 1);
|
||||
let i = (((p - low) << 1) | low) as usize;
|
||||
let j = i + *s as usize;
|
||||
let (ar, ai, br, bi) = (re[i], im[i], re[j], im[j]);
|
||||
re[i] = (m00.re * ar - m00.im * ai) + (m01.re * br - m01.im * bi);
|
||||
im[i] = m00.re * ai + m00.im * ar + m01.re * bi + m01.im * br;
|
||||
re[j] = m10.re * ar - m10.im * ai + m11.re * br - m11.im * bi;
|
||||
im[j] = m10.re * ai + m10.im * ar + m11.re * bi + m11.im * br;
|
||||
}
|
||||
} else {
|
||||
replay(&mut re, &mut im, std::slice::from_ref(l));
|
||||
}
|
||||
}
|
||||
bits(&state_output(&re, &im)) != bits(&case.reference().unwrap())
|
||||
});
|
||||
assert!(differs, "the corpus cannot tell a reassociated formula from the reference");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plans_refuse_what_the_state_vector_refuses() {
|
||||
let bad = Gate { qubits: vec![0, 0], matrix: vec![C { re: 1.0, im: 0.0 }; 16] };
|
||||
assert_eq!(plan(2, &[bad], 0), Err(SvError::BadGate(0)));
|
||||
assert_eq!(plan(1, &[Gate::h(1)], 0), Err(SvError::BadGate(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_implementation_id_follows_the_kernel_and_every_mutant_edits_once() {
|
||||
let id = implementation_id();
|
||||
assert!(id.starts_with("cuda-sv64/") && id.len() == "cuda-sv64/".len() + 16);
|
||||
for m in &MUTANTS {
|
||||
assert_ne!(implementation_id_of(&mutate(m)), id, "{}", m.name);
|
||||
}
|
||||
assert!(SOURCE.contains("#define SV_MAX_D 64") && MAX_D == 64);
|
||||
let cl = cl_implementation_id();
|
||||
assert!(cl.starts_with("opencl-sv64/") && cl != id);
|
||||
for m in &MUTANTS {
|
||||
assert_ne!(cl_implementation_id_of(&mutate_cl(m)), cl, "{}", m.name);
|
||||
}
|
||||
assert!(CL_SOURCE.contains("#pragma OPENCL FP_CONTRACT OFF") && CL_SOURCE.contains("#define SV_MAX_D 64"));
|
||||
}
|
||||
}
|
||||
587
wai/wai-quantum/src/quantum_fabric/vqpu.rs
Normal file
587
wai/wai-quantum/src/quantum_fabric/vqpu.rs
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
//! A virtual quantum processor: a [`Sampler`] that returns the counts a noisy
|
||||
//! device would, from a stated noise model, so that every piece of a quantum
|
||||
//! processor's path through the fabric (submission, counts, statistical
|
||||
//! certification, placement, receipts) runs and is tested before any real
|
||||
//! device is reachable. Its certificates say they were earned in a simulator
|
||||
//! ([`Provenance::Simulator`]), and placement ranks them after silicon.
|
||||
//!
|
||||
//! **The model** ([`NoiseProfile`], version [`MODEL_VERSION`]), applied to the
|
||||
//! exact distribution `p` of the CPU's integer simulation:
|
||||
//!
|
||||
//! 1. **Gate errors, as global depolarizing.** Each gate with no control fails
|
||||
//! with probability `gate_1q`, each gate with controls with `gate_2q`; the
|
||||
//! run is error-free with probability `f = (1 − gate_1q)^{n₁}·(1 − gate_2q)^{n₂}`,
|
||||
//! and an error leaves the uniform distribution:
|
||||
//! `p' = f·p + (1 − f)/2ⁿ`. This is the digital error model the noisy
|
||||
//! simulator's tests measure against: it captures the fidelity's decay with
|
||||
//! depth, not the shape of real errors.
|
||||
//! 2. **Readout errors**: each qubit's outcome flips with probability
|
||||
//! `readout`, independently.
|
||||
//! 3. **Sampling**: `shots` draws by inverse transform from a splitmix64 stream
|
||||
//! seeded by the processor's seed, the circuit's hash and the shots, so the
|
||||
//! same request gives the same counts on every machine with IEEE binary64.
|
||||
//!
|
||||
//! It is a stand-in that is labelled as one. It claims nothing about any real
|
||||
//! device's errors.
|
||||
//!
|
||||
//! [`VirtualDevice`] adds a device's shape: a coupling map and a native basis.
|
||||
//! It compiles each circuit with [`crate::quantum_toolchain::native`], verifies
|
||||
//! the program, and charges the gate errors for the program it would run, so a
|
||||
//! circuit that needs routing on a sparse map samples worse than on a dense one,
|
||||
//! as on a device.
|
||||
|
||||
use super::sampling::{Counts, SampleFuture, Sampler, check_counts, reference_distribution, sample_kernel, certify_sampler, QpuCorpus};
|
||||
use super::{FabricClass, KernelEntry, Member, MemberError, Presence, Provenance, RunMeter, Window};
|
||||
use crate::quantum::{Circuit, Gateset, QuantumError, circuit_hash};
|
||||
use crate::quantum_toolchain::native::{compile, Basis, Coupling, NativeGate, NativeProgram};
|
||||
|
||||
/// The model's version: any change to how a distribution or a sample is
|
||||
/// computed bumps it, and it is in the implementation id and the member id.
|
||||
pub const MODEL_VERSION: u32 = 1;
|
||||
|
||||
/// The implementation name the id starts with.
|
||||
pub const IMPLEMENTATION: &str = "vqpu-depolarizing-readout";
|
||||
|
||||
/// The simulator a certificate names.
|
||||
pub const SIMULATOR: &str = "wai-quantum vqpu";
|
||||
|
||||
const DOMAIN_MEMBER: &[u8] = b"wai:quantum-fabric-vqpu-member\x01";
|
||||
const DOMAIN_STREAM: &[u8] = b"wai:quantum-fabric-vqpu-stream\x01";
|
||||
|
||||
/// A virtual processor's error rates, each a probability in `[0, 1)`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct NoiseProfile {
|
||||
/// The chance a gate with no control fails.
|
||||
pub gate_1q: f64,
|
||||
/// The chance a gate with controls fails.
|
||||
pub gate_2q: f64,
|
||||
/// The chance a qubit's measured bit is flipped.
|
||||
pub readout: f64,
|
||||
}
|
||||
|
||||
impl NoiseProfile {
|
||||
/// No errors: the processor samples the exact distribution.
|
||||
pub const IDEAL: NoiseProfile = NoiseProfile { gate_1q: 0.0, gate_2q: 0.0, readout: 0.0 };
|
||||
|
||||
/// Rates of the order superconducting processors publish in 2026:
|
||||
/// `1e-3` per one-qubit gate, `5e-3` per two-qubit gate, `1e-2` readout.
|
||||
pub const TYPICAL: NoiseProfile = NoiseProfile { gate_1q: 1e-3, gate_2q: 5e-3, readout: 1e-2 };
|
||||
|
||||
/// Whether every rate is a probability in `[0, 1)`.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
[self.gate_1q, self.gate_2q, self.readout].iter().all(|r| r.is_finite() && (0.0..1.0).contains(r))
|
||||
}
|
||||
}
|
||||
|
||||
/// A virtual quantum processor: see the module docs.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VirtualQpu {
|
||||
noise: NoiseProfile,
|
||||
seed: u64,
|
||||
max_qubits: u8,
|
||||
id: String,
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
fn splitmix(s: &mut u64) -> u64 {
|
||||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = *s;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
/// `p` after gate errors as global depolarizing (`n1` uncontrolled gates, `n2`
|
||||
/// controlled) and independent readout flips on `n` qubits.
|
||||
fn noisy(mut p: Vec<f64>, noise: NoiseProfile, n1: i32, n2: i32, n: u8) -> Vec<f64> {
|
||||
let f = (1.0 - noise.gate_1q).powi(n1) * (1.0 - noise.gate_2q).powi(n2);
|
||||
let uniform = (1.0 - f) / p.len() as f64;
|
||||
p.iter_mut().for_each(|x| *x = f * *x + uniform);
|
||||
let r = noise.readout;
|
||||
for q in 0..n {
|
||||
let bit = 1usize << q;
|
||||
for x in 0..p.len() {
|
||||
if x & bit == 0 {
|
||||
let (a, b) = (p[x], p[x | bit]);
|
||||
p[x] = (1.0 - r) * a + r * b;
|
||||
p[x | bit] = r * a + (1.0 - r) * b;
|
||||
}
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// `shots` inverse-transform draws from `p`, from a splitmix64 stream seeded by
|
||||
/// the domain, the seed, the circuit's hash and the shots.
|
||||
fn draw(p: &[f64], domain: &[u8], seed: u64, c: &Circuit, shots: u64) -> Result<Counts, MemberError> {
|
||||
let hash = circuit_hash(c).map_err(MemberError::Circuit)?;
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(domain);
|
||||
h.update(&seed.to_le_bytes());
|
||||
h.update(&hash);
|
||||
h.update(&shots.to_le_bytes());
|
||||
let d = h.finalize();
|
||||
let mut s = u64::from_le_bytes(d.as_bytes()[..8].try_into().expect("8 bytes"));
|
||||
let mut cum = Vec::with_capacity(p.len());
|
||||
let mut total = 0.0;
|
||||
for x in p {
|
||||
total += x;
|
||||
cum.push(total);
|
||||
}
|
||||
let mut counts = vec![0u64; p.len()];
|
||||
for _ in 0..shots {
|
||||
let u = (splitmix(&mut s) >> 11) as f64 * (1.0 / (1u64 << 53) as f64) * total;
|
||||
let i = cum.partition_point(|&c| c <= u).min(counts.len() - 1);
|
||||
counts[i] += 1;
|
||||
}
|
||||
Ok(counts)
|
||||
}
|
||||
|
||||
impl VirtualQpu {
|
||||
/// A processor with `noise`, drawing from `seed`, holding at most
|
||||
/// `max_qubits` qubits (a real device's width); `None` unless every rate
|
||||
/// is a probability in `[0, 1)` and the width is 1 to 20.
|
||||
pub fn new(noise: NoiseProfile, seed: u64, max_qubits: u8) -> Option<VirtualQpu> {
|
||||
if !noise.is_valid() || !(1..=20).contains(&max_qubits) {
|
||||
return None;
|
||||
}
|
||||
let mut o = DOMAIN_MEMBER.to_vec();
|
||||
o.extend_from_slice(&MODEL_VERSION.to_be_bytes());
|
||||
for r in [noise.gate_1q, noise.gate_2q, noise.readout] {
|
||||
o.extend_from_slice(&r.to_bits().to_be_bytes());
|
||||
}
|
||||
o.extend_from_slice(&seed.to_be_bytes());
|
||||
o.push(max_qubits);
|
||||
let id = format!("local/vqpu-{}", hex(&blake3::hash(&o).as_bytes()[..6]));
|
||||
Some(VirtualQpu { noise, seed, max_qubits, id })
|
||||
}
|
||||
|
||||
/// `"local/vqpu-<12 hex>"`, over the model version, the rates, the seed
|
||||
/// and the width.
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn noise(&self) -> NoiseProfile {
|
||||
self.noise
|
||||
}
|
||||
|
||||
pub fn max_qubits(&self) -> u8 {
|
||||
self.max_qubits
|
||||
}
|
||||
|
||||
/// `"vqpu-depolarizing-readout/<MODEL_VERSION>"`.
|
||||
pub fn implementation() -> String {
|
||||
format!("{IMPLEMENTATION}/{MODEL_VERSION}")
|
||||
}
|
||||
|
||||
/// `Simulator("wai-quantum vqpu <MODEL_VERSION>")`.
|
||||
pub fn provenance() -> Provenance {
|
||||
Provenance::Simulator(format!("{SIMULATOR} {MODEL_VERSION}"))
|
||||
}
|
||||
|
||||
/// The distribution this processor samples for `c`: the exact one, then
|
||||
/// gate errors as global depolarizing, then readout flips. Refuses what
|
||||
/// the simulator refuses.
|
||||
pub fn distribution(&self, c: &Circuit) -> Result<Vec<f64>, QuantumError> {
|
||||
let p = reference_distribution(c)?;
|
||||
let n1 = c.ops.iter().filter(|g| g.controls.is_empty()).count() as i32;
|
||||
let n2 = c.ops.len() as i32 - n1;
|
||||
Ok(noisy(p, self.noise, n1, n2, c.n_qubits))
|
||||
}
|
||||
|
||||
/// `shots` draws for `c` (see the module docs), or why there are none: what
|
||||
/// the simulator refuses, or more qubits than this processor holds.
|
||||
pub fn sample_now(&self, c: &Circuit, shots: u64) -> Result<Counts, MemberError> {
|
||||
if c.n_qubits > self.max_qubits {
|
||||
return Err(MemberError::TooWide { n_qubits: c.n_qubits, max: self.max_qubits });
|
||||
}
|
||||
let p = self.distribution(c).map_err(MemberError::Circuit)?;
|
||||
let counts = draw(&p, DOMAIN_STREAM, self.seed, c, shots)?;
|
||||
check_counts(c, shots, &counts)?;
|
||||
Ok(counts)
|
||||
}
|
||||
|
||||
/// A fabric [`Member`] of class [`FabricClass::Qpu`]: one sampling entry per
|
||||
/// gateset, each certified in this call on `corpus` with `shots` per
|
||||
/// circuit, earned in a simulator.
|
||||
pub fn member(&self, corpus: QpuCorpus, shots: u64) -> Result<Member, MemberError> {
|
||||
let mut kernels = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
let certificate = super::ready(certify_sampler(self, &Self::implementation(), gs, corpus, shots, Self::provenance()))
|
||||
.ok_or_else(|| MemberError::Other("the certification did not resolve".into()))??;
|
||||
kernels.push(KernelEntry { kernel: sample_kernel(gs).into(), implementation: Self::implementation(), certificate, energy: None });
|
||||
}
|
||||
let n = self.noise;
|
||||
Ok(Member {
|
||||
id: self.id.clone(),
|
||||
class: FabricClass::Qpu,
|
||||
presence: Presence::Detected,
|
||||
kernels,
|
||||
detail: format!(
|
||||
"virtual quantum processor, model {MODEL_VERSION}: gate errors {} (one-qubit) and {} (controlled), readout {}; up to {} qubits; a simulator, not a device",
|
||||
n.gate_1q, n.gate_2q, n.readout, self.max_qubits
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Sampler for VirtualQpu {
|
||||
fn member_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn sample<'r>(&'r self, implementation: &'r str, c: &'r Circuit, shots: u64, meter: &'r mut dyn RunMeter) -> SampleFuture<'r> {
|
||||
Box::pin(async move {
|
||||
if implementation != Self::implementation() {
|
||||
return Err(MemberError::Other(format!("{} runs implementation {}, not {implementation:?}", self.id, Self::implementation())));
|
||||
}
|
||||
let mut window = Window::new(meter);
|
||||
window.open();
|
||||
let counts = self.sample_now(c, shots)?;
|
||||
let (joules_micro, class) = window.close();
|
||||
Ok((counts, joules_micro, class))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The virtual device's model version: in its implementation id and member id.
|
||||
pub const DEVICE_MODEL_VERSION: u32 = 1;
|
||||
|
||||
/// The implementation name a virtual device's id starts with.
|
||||
pub const DEVICE_IMPLEMENTATION: &str = "vdevice-native-depolarizing-readout";
|
||||
|
||||
/// The simulator a virtual device's certificate names.
|
||||
pub const DEVICE_SIMULATOR: &str = "wai-quantum vdevice";
|
||||
|
||||
const DOMAIN_DEVICE: &[u8] = b"wai:quantum-fabric-vdevice-member\x01";
|
||||
const DOMAIN_DEVICE_STREAM: &[u8] = b"wai:quantum-fabric-vdevice-stream\x01";
|
||||
|
||||
/// A virtual quantum processor with a device's shape: a coupling map and a
|
||||
/// native gate basis. Each circuit is compiled to a [`NativeProgram`] for that
|
||||
/// map and basis, the program is verified against the circuit, and the gate
|
||||
/// errors are charged for the program the device would run, routing `SWAP`s
|
||||
/// included: each `sx`, `x`, `prx`, `gpi` or `gpi2` at `gate_1q`, each `cz` or
|
||||
/// `zz` at `gate_2q`, and `rz` free, as a virtual rotation is. Readout and
|
||||
/// sampling are as for [`VirtualQpu`]. Logical qubit `i` is read from the physical qubit it ends on,
|
||||
/// into bit `i` of the counts' index.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VirtualDevice {
|
||||
coupling: Coupling,
|
||||
basis: Basis,
|
||||
noise: NoiseProfile,
|
||||
seed: u64,
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl VirtualDevice {
|
||||
/// A device with `coupling` and `basis`, `noise`, drawing from `seed`;
|
||||
/// `None` unless every rate is a probability in `[0, 1)` and the map has a
|
||||
/// qubit.
|
||||
pub fn new(coupling: Coupling, basis: Basis, noise: NoiseProfile, seed: u64) -> Option<VirtualDevice> {
|
||||
if !noise.is_valid() || coupling.qubits() == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut o = DOMAIN_DEVICE.to_vec();
|
||||
o.extend_from_slice(&DEVICE_MODEL_VERSION.to_be_bytes());
|
||||
o.push(match basis {
|
||||
Basis::RzSxXCz => 1,
|
||||
Basis::PrxCz => 2,
|
||||
Basis::GpiZz => 3,
|
||||
});
|
||||
o.extend_from_slice(&coupling.qubits().to_be_bytes());
|
||||
for a in 0..coupling.qubits() {
|
||||
for b in a + 1..coupling.qubits() {
|
||||
if coupling.connected(a, b) {
|
||||
o.extend_from_slice(&a.to_be_bytes());
|
||||
o.extend_from_slice(&b.to_be_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
for r in [noise.gate_1q, noise.gate_2q, noise.readout] {
|
||||
o.extend_from_slice(&r.to_bits().to_be_bytes());
|
||||
}
|
||||
o.extend_from_slice(&seed.to_be_bytes());
|
||||
let id = format!("local/vdevice-{}", hex(&blake3::hash(&o).as_bytes()[..6]));
|
||||
Some(VirtualDevice { coupling, basis, noise, seed, id })
|
||||
}
|
||||
|
||||
/// `"local/vdevice-<12 hex>"`, over the model version, the basis, the map,
|
||||
/// the rates and the seed.
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn coupling(&self) -> &Coupling {
|
||||
&self.coupling
|
||||
}
|
||||
|
||||
pub fn basis(&self) -> Basis {
|
||||
self.basis
|
||||
}
|
||||
|
||||
/// `"vdevice-native-depolarizing-readout/<DEVICE_MODEL_VERSION>"`.
|
||||
pub fn implementation() -> String {
|
||||
format!("{DEVICE_IMPLEMENTATION}/{DEVICE_MODEL_VERSION}")
|
||||
}
|
||||
|
||||
/// `Simulator("wai-quantum vdevice <DEVICE_MODEL_VERSION>")`.
|
||||
pub fn provenance() -> Provenance {
|
||||
Provenance::Simulator(format!("{DEVICE_SIMULATOR} {DEVICE_MODEL_VERSION}"))
|
||||
}
|
||||
|
||||
/// The program this device runs for `c`, verified against `c`.
|
||||
pub fn compile(&self, c: &Circuit) -> Result<NativeProgram, MemberError> {
|
||||
if c.n_qubits > MAX_DEVICE_SAMPLE_QUBITS {
|
||||
return Err(MemberError::TooWide { n_qubits: c.n_qubits, max: MAX_DEVICE_SAMPLE_QUBITS });
|
||||
}
|
||||
let program = compile(c, self.basis, &self.coupling).map_err(|e| MemberError::Other(format!("{}: {e}", self.id)))?;
|
||||
program.verify(c, &self.coupling).map_err(|e| MemberError::Other(format!("{}: the compiled program failed its check: {e}", self.id)))?;
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
/// The distribution this device samples for `c`, and the program it runs.
|
||||
pub fn distribution(&self, c: &Circuit) -> Result<(Vec<f64>, NativeProgram), MemberError> {
|
||||
let program = self.compile(c)?;
|
||||
let p = reference_distribution(c).map_err(MemberError::Circuit)?;
|
||||
let n1 = program.gates.iter().filter(|g| matches!(g, NativeGate::Sx(_) | NativeGate::X(_) | NativeGate::Prx(..) | NativeGate::Gpi(..) | NativeGate::Gpi2(..))).count() as i32;
|
||||
let n2 = program.gates.iter().filter(|g| matches!(g, NativeGate::Cz(..) | NativeGate::Zz(..))).count() as i32;
|
||||
Ok((noisy(p, self.noise, n1, n2, c.n_qubits), program))
|
||||
}
|
||||
|
||||
/// `shots` draws for `c`, or why there are none.
|
||||
pub fn sample_now(&self, c: &Circuit, shots: u64) -> Result<Counts, MemberError> {
|
||||
let (p, _) = self.distribution(c)?;
|
||||
let counts = draw(&p, DOMAIN_DEVICE_STREAM, self.seed, c, shots)?;
|
||||
check_counts(c, shots, &counts)?;
|
||||
Ok(counts)
|
||||
}
|
||||
|
||||
/// A fabric [`Member`] of class [`FabricClass::Qpu`], as for
|
||||
/// [`VirtualQpu::member`].
|
||||
pub fn member(&self, corpus: QpuCorpus, shots: u64) -> Result<Member, MemberError> {
|
||||
let mut kernels = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
let certificate = super::ready(certify_sampler(self, &Self::implementation(), gs, corpus, shots, Self::provenance()))
|
||||
.ok_or_else(|| MemberError::Other("the certification did not resolve".into()))??;
|
||||
kernels.push(KernelEntry { kernel: sample_kernel(gs).into(), implementation: Self::implementation(), certificate, energy: None });
|
||||
}
|
||||
let n = self.noise;
|
||||
let basis = match self.basis {
|
||||
Basis::RzSxXCz => "rz, sx, x, cz",
|
||||
Basis::PrxCz => "prx, cz",
|
||||
Basis::GpiZz => "gpi, gpi2, zz",
|
||||
};
|
||||
Ok(Member {
|
||||
id: self.id.clone(),
|
||||
class: FabricClass::Qpu,
|
||||
presence: Presence::Detected,
|
||||
kernels,
|
||||
detail: format!(
|
||||
"virtual device, model {DEVICE_MODEL_VERSION}: {} qubits, basis {basis}, programs routed on its coupling map and verified before they run; gate errors {} (one-qubit rotations) and {} (two-qubit gates), readout {}; a simulator, not a device",
|
||||
self.coupling.qubits(),
|
||||
n.gate_1q,
|
||||
n.gate_2q,
|
||||
n.readout
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The widest circuit a virtual device samples: its distribution is a dense
|
||||
/// table of `2^n` probabilities.
|
||||
pub const MAX_DEVICE_SAMPLE_QUBITS: u8 = 20;
|
||||
|
||||
impl Sampler for VirtualDevice {
|
||||
fn member_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn sample<'r>(&'r self, implementation: &'r str, c: &'r Circuit, shots: u64, meter: &'r mut dyn RunMeter) -> SampleFuture<'r> {
|
||||
Box::pin(async move {
|
||||
if implementation != Self::implementation() {
|
||||
return Err(MemberError::Other(format!("{} runs implementation {}, not {implementation:?}", self.id, Self::implementation())));
|
||||
}
|
||||
let mut window = Window::new(meter);
|
||||
window.open();
|
||||
let counts = self.sample_now(c, shots)?;
|
||||
let (joules_micro, class) = window.close();
|
||||
Ok((counts, joules_micro, class))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::quantum_fabric::sampling::{QPU_DEFAULT, local_sampling_member};
|
||||
use crate::quantum_fabric::{Parity, Unmetered, place, ready, statistical};
|
||||
|
||||
fn bell() -> Circuit {
|
||||
let mut c = Circuit::new(2);
|
||||
c.h(0).cx(0, 1);
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rates_must_be_probabilities_and_the_id_names_the_model() {
|
||||
assert!(VirtualQpu::new(NoiseProfile { gate_1q: -0.1, ..NoiseProfile::IDEAL }, 1, 5).is_none());
|
||||
assert!(VirtualQpu::new(NoiseProfile { readout: 1.0, ..NoiseProfile::IDEAL }, 1, 5).is_none());
|
||||
assert!(VirtualQpu::new(NoiseProfile { gate_2q: f64::NAN, ..NoiseProfile::IDEAL }, 1, 5).is_none());
|
||||
assert!(VirtualQpu::new(NoiseProfile::IDEAL, 1, 0).is_none());
|
||||
let a = VirtualQpu::new(NoiseProfile::TYPICAL, 1, 5).unwrap();
|
||||
assert!(a.id().starts_with("local/vqpu-"));
|
||||
assert_ne!(VirtualQpu::new(NoiseProfile::TYPICAL, 2, 5).unwrap().id(), a.id());
|
||||
assert_ne!(VirtualQpu::new(NoiseProfile::IDEAL, 1, 5).unwrap().id(), a.id());
|
||||
assert_ne!(VirtualQpu::new(NoiseProfile::TYPICAL, 1, 6).unwrap().id(), a.id());
|
||||
assert_eq!(VirtualQpu::implementation(), "vqpu-depolarizing-readout/1");
|
||||
assert_eq!(VirtualQpu::provenance().label(), "simulator:wai-quantum vqpu 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ideal_processor_samples_the_exact_distribution() {
|
||||
let v = VirtualQpu::new(NoiseProfile::IDEAL, 9, 8).unwrap();
|
||||
for c in [bell(), Circuit::qft(4)] {
|
||||
assert_eq!(v.distribution(&c).unwrap(), reference_distribution(&c).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_mixes_toward_uniform_and_flips_readout() {
|
||||
let c = bell();
|
||||
let gates = VirtualQpu::new(NoiseProfile { gate_2q: 0.1, ..NoiseProfile::IDEAL }, 1, 5).unwrap().distribution(&c).unwrap();
|
||||
// One controlled gate at 0.1: 0.9·0.5 + 0.1/4 on the two peaks.
|
||||
assert!((gates[0] - 0.475).abs() < 1e-12 && (gates[1] - 0.025).abs() < 1e-12, "{gates:?}");
|
||||
let readout = VirtualQpu::new(NoiseProfile { readout: 0.1, ..NoiseProfile::IDEAL }, 1, 5).unwrap().distribution(&c).unwrap();
|
||||
// |00⟩ stays with (0.9)² or comes from |11⟩ with (0.1)².
|
||||
assert!((readout[0] - 0.5 * (0.81 + 0.01)).abs() < 1e-12, "{readout:?}");
|
||||
for d in [&gates, &readout] {
|
||||
assert!((d.iter().sum::<f64>() - 1.0).abs() < 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_request_gives_the_same_counts() {
|
||||
let v = VirtualQpu::new(NoiseProfile::TYPICAL, 3, 5).unwrap();
|
||||
let a = v.sample_now(&Circuit::qft(3), 2000).unwrap();
|
||||
assert_eq!(v.sample_now(&Circuit::qft(3), 2000).unwrap(), a);
|
||||
assert_eq!(a.iter().sum::<u64>(), 2000);
|
||||
assert_eq!(v.sample_now(&Circuit::qft(3), 2001).unwrap().iter().sum::<u64>(), 2001);
|
||||
assert_eq!(v.sample_now(&Circuit::new(6), 10), Err(MemberError::TooWide { n_qubits: 6, max: 5 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fidelity_falls_as_the_noise_rises() {
|
||||
let c = QPU_DEFAULT.circuits(Gateset::V2).pop().unwrap();
|
||||
let reference = reference_distribution(&c).unwrap();
|
||||
let mut last = 1.0;
|
||||
for scale in [0.0, 1.0, 10.0, 50.0] {
|
||||
let noise = NoiseProfile { gate_1q: 1e-3 * scale, gate_2q: 5e-3 * scale, readout: (1e-2 * scale).min(0.4) };
|
||||
let v = VirtualQpu::new(noise, 5, 5).unwrap();
|
||||
let Parity::Statistical { fidelity, .. } = statistical(&reference, &v.sample_now(&c, 20_000).unwrap()) else { panic!() };
|
||||
assert!(fidelity <= last + 1e-3, "scale {scale}: {fidelity} after {last}");
|
||||
last = fidelity;
|
||||
}
|
||||
assert!(last < 0.9, "heavy noise is visible: {last}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_virtual_member_is_a_simulated_qpu_placed_after_silicon() {
|
||||
let v = VirtualQpu::new(NoiseProfile::TYPICAL, 11, 5).unwrap();
|
||||
let member = v.member(QPU_DEFAULT, 2000).unwrap();
|
||||
assert_eq!((member.class, member.presence, member.kernels.len()), (FabricClass::Qpu, Presence::Detected, 2));
|
||||
for (entry, gs) in member.kernels.iter().zip(Gateset::ALL) {
|
||||
assert_eq!(entry.kernel, sample_kernel(gs));
|
||||
assert_eq!(entry.certificate.provenance, VirtualQpu::provenance());
|
||||
let Parity::Statistical { fidelity, shots } = entry.certificate.parity else { panic!("{:?}", entry.certificate.parity) };
|
||||
assert_eq!(shots, 2000);
|
||||
assert!(fidelity > 0.8 && fidelity < 1.0, "{gs:?}: {fidelity}");
|
||||
}
|
||||
let need = Parity::Statistical { fidelity: 0.5, shots: 1000 };
|
||||
let local = local_sampling_member();
|
||||
for gs in Gateset::ALL {
|
||||
let pair = [member.clone(), local.clone()];
|
||||
let (placed, _) = place(&pair, sample_kernel(gs), &need).unwrap();
|
||||
assert_eq!(placed.id, local.id);
|
||||
let (placed, k) = place(std::slice::from_ref(&member), sample_kernel(gs), &need).unwrap();
|
||||
assert_eq!((placed.id.as_str(), k.implementation.as_str()), (v.id(), VirtualQpu::implementation().as_str()));
|
||||
}
|
||||
// On the Sampler surface, with the wrong implementation, nothing runs.
|
||||
assert!(ready(v.sample("other", &bell(), 10, &mut Unmetered)).unwrap().is_err());
|
||||
}
|
||||
|
||||
fn complete(n: u16) -> Coupling {
|
||||
let mut pairs = Vec::new();
|
||||
for a in 0..n {
|
||||
for b in a + 1..n {
|
||||
pairs.push((a, b));
|
||||
}
|
||||
}
|
||||
Coupling::new(n, &pairs).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ideal_device_samples_the_exact_distribution_in_either_basis() {
|
||||
for basis in [Basis::RzSxXCz, Basis::PrxCz, Basis::GpiZz] {
|
||||
let d = VirtualDevice::new(Coupling::grid(2, 3), basis, NoiseProfile::IDEAL, 4).unwrap();
|
||||
for c in [bell(), Circuit::qft(4)] {
|
||||
let (p, program) = d.distribution(&c).unwrap();
|
||||
assert_eq!(p, reference_distribution(&c).unwrap());
|
||||
assert_eq!(program.basis, basis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routing_on_a_sparse_map_costs_fidelity() {
|
||||
// Every pair of five qubits interacts: a line needs SWAPs, a complete map
|
||||
// none, so the line's program carries more cz and samples worse.
|
||||
let mut c = Circuit::new(5);
|
||||
for a in 0..5u8 {
|
||||
c.h(a);
|
||||
}
|
||||
for a in 0..5u8 {
|
||||
for b in a + 1..5 {
|
||||
c.cz(a, b);
|
||||
}
|
||||
}
|
||||
// Read in the X basis: depolarizing leaves a uniform distribution as it
|
||||
// was, so the distribution must not be one.
|
||||
for a in 0..5u8 {
|
||||
c.h(a);
|
||||
}
|
||||
let noise = NoiseProfile { gate_1q: 1e-3, gate_2q: 2e-2, readout: 0.0 };
|
||||
let line = VirtualDevice::new(Coupling::line(5), Basis::RzSxXCz, noise, 1).unwrap();
|
||||
let full = VirtualDevice::new(complete(5), Basis::RzSxXCz, noise, 1).unwrap();
|
||||
let (pl, prog_line) = line.distribution(&c).unwrap();
|
||||
let (pf, prog_full) = full.distribution(&c).unwrap();
|
||||
assert!(prog_line.swaps > 0 && prog_full.swaps == 0);
|
||||
let reference = reference_distribution(&c).unwrap();
|
||||
assert!(reference.iter().cloned().fold(0.0, f64::max) > 1.5 / 32.0);
|
||||
let overlap = |p: &[f64]| -> f64 { p.iter().zip(&reference).map(|(a, b)| (a * b).sqrt()).sum::<f64>().powi(2) };
|
||||
assert!(overlap(&pl) < overlap(&pf), "line {} vs complete {}", overlap(&pl), overlap(&pf));
|
||||
assert_ne!(line.id(), full.id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_device_member_is_certified_through_compiled_programs() {
|
||||
let d = VirtualDevice::new(Coupling::grid(3, 3), Basis::PrxCz, NoiseProfile::TYPICAL, 21).unwrap();
|
||||
let member = d.member(QPU_DEFAULT, 2000).unwrap();
|
||||
assert_eq!((member.class, member.kernels.len()), (FabricClass::Qpu, 2));
|
||||
for entry in &member.kernels {
|
||||
assert_eq!(entry.certificate.provenance, VirtualDevice::provenance());
|
||||
let Parity::Statistical { fidelity, shots } = entry.certificate.parity else { panic!("{:?}", entry.certificate.parity) };
|
||||
assert_eq!(shots, 2000);
|
||||
assert!(fidelity > 0.7 && fidelity < 1.0, "{fidelity}");
|
||||
}
|
||||
assert_eq!(d.sample_now(&Circuit::qft(3), 500).unwrap(), d.sample_now(&Circuit::qft(3), 500).unwrap());
|
||||
// Wider than the map: no layout exists.
|
||||
assert!(d.sample_now(&Circuit::new(10), 10).is_err());
|
||||
assert!(ready(d.sample("other", &bell(), 10, &mut Unmetered)).unwrap().is_err());
|
||||
assert!(VirtualDevice::new(Coupling::line(0), Basis::PrxCz, NoiseProfile::IDEAL, 1).is_none());
|
||||
assert!(VirtualDevice::new(Coupling::line(3), Basis::PrxCz, NoiseProfile { readout: 2.0, ..NoiseProfile::IDEAL }, 1).is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -711,6 +711,7 @@ mod tests {
|
|||
parity: Parity::BitExact,
|
||||
cases: 224,
|
||||
corpus: [7; 32],
|
||||
provenance: Default::default(),
|
||||
};
|
||||
let entry = KernelEntry { kernel: kernel.into(), implementation: implementation_id(), certificate, energy: None };
|
||||
Member { id, class: a.class(), presence: Presence::Detected, kernels: vec![entry], detail: String::new() }
|
||||
|
|
@ -739,7 +740,7 @@ mod tests {
|
|||
for gs in Gateset::ALL {
|
||||
let kernel = apply_kernel(gs);
|
||||
let entry = |implementation: String| {
|
||||
let certificate = Certificate { kernel: kernel.into(), implementation: implementation.clone(), member: id.clone(), generator: FABRIC_DEFAULT.generator(gs), parity: Parity::BitExact, cases: 224, corpus: [7; 32] };
|
||||
let certificate = Certificate { kernel: kernel.into(), implementation: implementation.clone(), member: id.clone(), generator: FABRIC_DEFAULT.generator(gs), parity: Parity::BitExact, cases: 224, corpus: [7; 32], provenance: Default::default() };
|
||||
KernelEntry { kernel: kernel.into(), implementation, certificate, energy: None }
|
||||
};
|
||||
for kernels in [vec![entry(implementation_id()), entry(tile_implementation_id())], vec![entry(tile_implementation_id()), entry(implementation_id())]] {
|
||||
|
|
|
|||
|
|
@ -581,6 +581,139 @@ impl CudaMember {
|
|||
}
|
||||
}
|
||||
|
||||
/// The dense f64 state vector on this device
|
||||
/// ([`quantum_fabric::sv64`](crate::quantum_fabric::sv64)): the CPU's
|
||||
/// launches, each formula one IEEE binary64 operation at a time with no fused
|
||||
/// multiply-add, so the state comes back with the CPU's bits, or the
|
||||
/// certificate says it did not.
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
#[derive(Clone)]
|
||||
pub struct CudaSv {
|
||||
member: CudaMember,
|
||||
diag: CudaFunction,
|
||||
one: CudaFunction,
|
||||
many: CudaFunction,
|
||||
implementation: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
impl std::fmt::Debug for CudaSv {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CudaSv").field("member", &self.member.id).field("implementation", &self.implementation).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
impl CudaMember {
|
||||
/// The state-vector kernels compiled for this device.
|
||||
pub fn sv(&self) -> Result<CudaSv, CudaError> {
|
||||
self.sv_with_source(crate::quantum_fabric::sv64::SOURCE)
|
||||
}
|
||||
|
||||
/// The same, from another kernel text: for the mutation tests.
|
||||
#[doc(hidden)]
|
||||
pub fn sv_with_source(&self, source: &str) -> Result<CudaSv, CudaError> {
|
||||
use crate::quantum_fabric::sv64;
|
||||
let opts = CompileOptions {
|
||||
arch: Some(arch_option(self.identity.arch())),
|
||||
options: sv64::COMPILE_OPTIONS.iter().map(|s| s.to_string()).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let ptx = compile_ptx_with_opts(source, opts).map_err(|e| CudaError::Compile(e.to_string()))?;
|
||||
let module = self.ctx.load_module(ptx).map_err(driver)?;
|
||||
Ok(CudaSv {
|
||||
member: self.clone(),
|
||||
diag: module.load_function("sv_diag").map_err(driver)?,
|
||||
one: module.load_function("sv_one").map_err(driver)?,
|
||||
many: module.load_function("sv_many").map_err(driver)?,
|
||||
implementation: sv64::implementation_id_of(source),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
impl CudaSv {
|
||||
/// The implementation id ([`sv64::implementation_id`](crate::quantum_fabric::sv64::implementation_id), or a mutant's).
|
||||
pub fn implementation(&self) -> &str {
|
||||
&self.implementation
|
||||
}
|
||||
|
||||
/// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses
|
||||
/// them: the real and imaginary parts.
|
||||
pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec<f64>, Vec<f64>), CudaError> {
|
||||
use crate::quantum_fabric::sv64::{Launch, plan};
|
||||
let launches = plan(n, gates, width).map_err(|_| CudaError::Internal("a gate the state vector refuses"))?;
|
||||
// Two f64 arrays of 2^n.
|
||||
if n == 0 || n > u32::from(self.member.max_qubits.saturating_sub(1)) {
|
||||
return Err(CudaError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) });
|
||||
}
|
||||
let len = 1usize << n;
|
||||
let stream = &self.member.stream;
|
||||
let mut init = vec![0.0f64; len];
|
||||
init[0] = 1.0;
|
||||
let mut re = stream.clone_htod(&init).map_err(driver)?;
|
||||
init[0] = 0.0;
|
||||
let mut im = stream.clone_htod(&init).map_err(driver)?;
|
||||
drop(init);
|
||||
let flat = |m: &[crate::linalg::C]| -> Vec<f64> { m.iter().flat_map(|c| [c.re, c.im]).collect() };
|
||||
for l in &launches {
|
||||
match l {
|
||||
Launch::Diag { bitpos, diag } => {
|
||||
let bp = stream.clone_htod(bitpos).map_err(driver)?;
|
||||
let dg = stream.clone_htod(&flat(diag)).map_err(driver)?;
|
||||
let (count, k) = (len as u64, bitpos.len() as u32);
|
||||
let mut a = stream.launch_builder(&self.diag);
|
||||
a.arg(&mut re).arg(&mut im).arg(&count).arg(&bp).arg(&k).arg(&dg);
|
||||
// SAFETY: `re` and `im` hold `len` doubles, `bp` k positions
|
||||
// below n and `dg` 2·2^k doubles: the kernel's arguments.
|
||||
unsafe { a.launch(launch(count)) }.map_err(driver)?;
|
||||
}
|
||||
Launch::One { s, m } => {
|
||||
let pairs = (len / 2) as u64;
|
||||
let v: Vec<f64> = flat(m);
|
||||
let mut a = stream.launch_builder(&self.one);
|
||||
a.arg(&mut re).arg(&mut im).arg(&pairs).arg(s);
|
||||
for x in &v {
|
||||
a.arg(x);
|
||||
}
|
||||
// SAFETY: as above; `s` is a power of two below 2^n.
|
||||
unsafe { a.launch(launch(pairs)) }.map_err(driver)?;
|
||||
}
|
||||
Launch::Many { sorted, offsets, m } => {
|
||||
let so = stream.clone_htod(sorted).map_err(driver)?;
|
||||
let of = stream.clone_htod(offsets).map_err(driver)?;
|
||||
let mm = stream.clone_htod(&flat(m)).map_err(driver)?;
|
||||
let (groups, k) = ((len >> sorted.len()) as u64, sorted.len() as u32);
|
||||
let mut a = stream.launch_builder(&self.many);
|
||||
a.arg(&mut re).arg(&mut im).arg(&groups).arg(&so).arg(&k).arg(&of).arg(&mm);
|
||||
// SAFETY: as above; 2^k is at most the kernel's SV_MAX_D,
|
||||
// which `plan` holds.
|
||||
unsafe { a.launch(launch(groups)) }.map_err(driver)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((stream.clone_dtoh(&re).map_err(driver)?, stream.clone_dtoh(&im).map_err(driver)?))
|
||||
}
|
||||
|
||||
/// The certificate of [`KERNEL_SV_RUN`](crate::quantum_fabric::sv64::KERNEL_SV_RUN)
|
||||
/// on this device over `corpus`, against the CPU's state vector.
|
||||
pub fn certify(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result<Certificate, CudaError> {
|
||||
use crate::quantum_fabric::sv64::{KERNEL_SV_RUN, state_output};
|
||||
let mut cert = Certification::new();
|
||||
for case in corpus.cases() {
|
||||
let reference = case.reference().map_err(|_| CudaError::Internal("the corpus holds a gate the state vector refuses"))?;
|
||||
let (re, im) = self.run(case.n, &case.gates, case.width)?;
|
||||
cert.add(&case.bytes(), &reference, &state_output(&re, &im));
|
||||
}
|
||||
Ok(cert.finish(KERNEL_SV_RUN, &self.implementation, &self.member.id, &corpus.generator()))
|
||||
}
|
||||
|
||||
/// The member's entry for the state vector, certified in this call.
|
||||
pub fn entry(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result<KernelEntry, CudaError> {
|
||||
Ok(KernelEntry { kernel: crate::quantum_fabric::sv64::KERNEL_SV_RUN.into(), implementation: self.implementation.clone(), certificate: self.certify(corpus)?, energy: None })
|
||||
}
|
||||
}
|
||||
|
||||
/// The member on the fabric ([`Fabric`](crate::quantum_fabric::Fabric)): its
|
||||
/// own implementation, through [`CudaMember::simulate_metered`]. Pair it with
|
||||
/// a [`Member`] whose entries name that implementation, such as
|
||||
|
|
|
|||
612
wai/wai-quantum/src/quantum_fabric_fpga.rs
Normal file
612
wai/wai-quantum/src/quantum_fabric_fpga.rs
Normal file
|
|
@ -0,0 +1,612 @@
|
|||
//! The fabric's simulated FPGA member: the gate datapath of
|
||||
//! [`quantum_fabric::fpga`](crate::quantum_fabric::fpga), compiled at run time
|
||||
//! by Verilator into a cycle-accurate simulation and driven over a pipe. See
|
||||
//! that module for the datapath and the argument that it is bit-exact.
|
||||
//!
|
||||
//! - **Discovery** ([`discover`]): Verilator is looked for when the process
|
||||
//! first asks (`WAI_QUANTUM_VERILATOR`, else `verilator` on the path), so a
|
||||
//! build with this feature runs on a host without it and finds no member
|
||||
//! there ([`Discovery::unavailable`] says why). The datapath and harness are
|
||||
//! compiled into a cache directory keyed by the implementation id and the
|
||||
//! simulator (`WAI_QUANTUM_FPGA_CACHE`, else the system's temporary
|
||||
//! directory), and the simulation is probed against [`lanes`] in this process
|
||||
//! before it is a member.
|
||||
//! - **Running** ([`FpgaSimMember::run_program`]): the state is loaded into the
|
||||
//! datapath's RAM through its host port, each gate runs to completion, and
|
||||
//! the report word and the state come back. The datapath reports exactly when
|
||||
//! the reference's result leaves 32 bits; the member then returns
|
||||
//! [`FpgaError::OutOfRange`], never a state.
|
||||
//! - **Certification, placement, receipts and metering** are as for the CUDA
|
||||
//! member, except that every certificate says it was earned in a simulator
|
||||
//! ([`SimIdentity::provenance`]), so placement ranks it after silicon.
|
||||
//!
|
||||
//! Every call is synchronous. The simulation is one child process per member,
|
||||
//! behind a lock, ended when the last clone of the member is dropped.
|
||||
//!
|
||||
//! # Honest boundaries
|
||||
//!
|
||||
//! - **A simulation, labelled as one.** It proves what the datapath computes
|
||||
//! cycle by cycle, not what any board does; no board is claimed.
|
||||
//! - **Two cycles a pair, in order.** The datapath is written to be read and
|
||||
//! checked, not to be fast; [`FpgaSimMember::cycles`] reports what a run took.
|
||||
//! - **Unmetered by default**, as for every member: a meter reads the host
|
||||
//! running the simulation.
|
||||
|
||||
use crate::quantum::{Amp, Circuit, Gateset, MAX_QUBITS, ONE, QuantumError, StateVector};
|
||||
use crate::quantum_energy::EnergyClass;
|
||||
use crate::quantum_fabric::fpga::{self, SimIdentity};
|
||||
use crate::quantum_fabric::lanes::{self, GateWords, Lane};
|
||||
use crate::quantum_fabric::plan::{self, Config, LaneRun, Program, Step};
|
||||
use crate::quantum_fabric::{
|
||||
Certificate, Certification, Corpus, EnergyCalibration, FabricClass, KernelEntry, Member, MemberError, Presence, RunFuture, RunMeter, Runner, Unmetered, Window,
|
||||
apply_kernel, pair_updates,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
/// One probe case's results: [`lanes::mul_wide`] and [`lanes::fxmul_wide`],
|
||||
/// each as `(high, low)` words.
|
||||
pub type ProbeOutput = ((u32, u32), (u32, u32));
|
||||
|
||||
/// A run's lanes and report, with the meter's accepted reading (zero and
|
||||
/// `None` when unmetered).
|
||||
type MeteredRun = (LaneRun, (u64, Option<EnergyClass>));
|
||||
|
||||
/// Why the simulation is not a member, or a run has no state.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum FpgaError {
|
||||
/// Verilator or a C++ compiler could not be used, and why.
|
||||
Unavailable(String),
|
||||
/// Verilator refused the datapath or the harness; its output.
|
||||
Build(String),
|
||||
/// The simulation's process failed, or broke the protocol.
|
||||
Simulator(String),
|
||||
/// Exactly what the CPU reference refuses.
|
||||
Circuit(QuantumError),
|
||||
/// More qubits than the datapath's RAM holds.
|
||||
TooWide { n_qubits: u8, max: u8 },
|
||||
/// The reference's result leaves 32 bits at this op; the member returns
|
||||
/// no state.
|
||||
OutOfRange { gate: u32 },
|
||||
/// A program the host cannot lay out: a defect of the host, never a
|
||||
/// result.
|
||||
Internal(&'static str),
|
||||
/// The datapath's arithmetic disagreed with the CPU's on this pair.
|
||||
Probe { a: i32, b: i32 },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FpgaError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FpgaError::Unavailable(why) => write!(f, "the FPGA simulation is unavailable: {why}"),
|
||||
FpgaError::Build(log) => write!(f, "the datapath did not compile: {log}"),
|
||||
FpgaError::Simulator(e) => write!(f, "simulation: {e}"),
|
||||
FpgaError::Circuit(e) => write!(f, "{e}"),
|
||||
FpgaError::TooWide { n_qubits, max } => write!(f, "{n_qubits} qubits; this member holds at most {max}"),
|
||||
FpgaError::OutOfRange { gate } => write!(f, "the result of op {gate} leaves 32 bits"),
|
||||
FpgaError::Internal(why) => write!(f, "host defect: {why}"),
|
||||
FpgaError::Probe { a, b } => write!(f, "the datapath's arithmetic differs from the CPU's at ({a}, {b})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FpgaError {}
|
||||
|
||||
impl From<FpgaError> for MemberError {
|
||||
fn from(e: FpgaError) -> MemberError {
|
||||
match e {
|
||||
FpgaError::TooWide { n_qubits, max } => MemberError::TooWide { n_qubits, max },
|
||||
FpgaError::OutOfRange { gate } => MemberError::OutOfRange { gate },
|
||||
FpgaError::Simulator(why) => MemberError::Device(why),
|
||||
FpgaError::Circuit(e) => MemberError::Circuit(e),
|
||||
other => MemberError::Other(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn io(e: std::io::Error) -> FpgaError {
|
||||
FpgaError::Simulator(e.to_string())
|
||||
}
|
||||
|
||||
/// A simulation that is not a member, and why.
|
||||
#[derive(Debug)]
|
||||
pub struct Refusal {
|
||||
pub key: String,
|
||||
pub error: FpgaError,
|
||||
}
|
||||
|
||||
/// What [`discover`] found: at most one member, the simulation.
|
||||
#[derive(Debug)]
|
||||
pub struct Discovery {
|
||||
pub found: Vec<FpgaSimMember>,
|
||||
pub refused: Vec<Refusal>,
|
||||
/// Why nothing was built: no Verilator, or no C++ compiler. `None` when
|
||||
/// the simulator was found.
|
||||
pub unavailable: Option<String>,
|
||||
}
|
||||
|
||||
/// The simulator's path and version line, or why there is none.
|
||||
fn verilator() -> Result<(PathBuf, String), String> {
|
||||
let path = std::env::var_os("WAI_QUANTUM_VERILATOR").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("verilator"));
|
||||
let out = Command::new(&path).arg("--version").output().map_err(|e| format!("no Verilator at {}: {e}", path.display()))?;
|
||||
let line = String::from_utf8_lossy(&out.stdout).lines().next().unwrap_or("").trim().to_string();
|
||||
if !out.status.success() || !line.starts_with("Verilator ") {
|
||||
return Err(format!("{} --version did not name Verilator", path.display()));
|
||||
}
|
||||
Ok((path, line))
|
||||
}
|
||||
|
||||
/// The C++ compiler's version line: `$CXX`, else `c++`.
|
||||
fn compiler() -> Result<String, String> {
|
||||
let cxx = std::env::var("CXX").unwrap_or_else(|_| "c++".into());
|
||||
let out = Command::new(&cxx).arg("--version").output().map_err(|e| format!("no C++ compiler {cxx}: {e}"))?;
|
||||
let line = String::from_utf8_lossy(&out.stdout).lines().next().unwrap_or("").trim().to_string();
|
||||
if !out.status.success() || line.is_empty() {
|
||||
return Err(format!("{cxx} --version printed nothing"));
|
||||
}
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
/// Whether the simulator and a C++ compiler can be used, or why not.
|
||||
pub fn available() -> Result<(), String> {
|
||||
verilator()?;
|
||||
compiler()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The simulation, built and probed in this process, or why there is none.
|
||||
pub fn discover() -> Discovery {
|
||||
let mut d = Discovery { found: Vec::new(), refused: Vec::new(), unavailable: None };
|
||||
let (path, simulator) = match verilator() {
|
||||
Ok(v) => v,
|
||||
Err(why) => {
|
||||
d.unavailable = Some(why);
|
||||
return d;
|
||||
}
|
||||
};
|
||||
let compiler = match compiler() {
|
||||
Ok(c) => c,
|
||||
Err(why) => {
|
||||
d.unavailable = Some(why);
|
||||
return d;
|
||||
}
|
||||
};
|
||||
let identity = SimIdentity { simulator, compiler, host: format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS) };
|
||||
let key = identity.key();
|
||||
match open(&path, identity) {
|
||||
Ok(m) => d.found.push(m),
|
||||
Err(error) => d.refused.push(Refusal { key, error }),
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// The cache directory for a datapath text under a simulator.
|
||||
fn cache_dir(identity: &SimIdentity, rtl: &str) -> PathBuf {
|
||||
let base = std::env::var_os("WAI_QUANTUM_FPGA_CACHE").map(PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("wai-quantum-fpga"));
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(fpga::implementation_id_of(rtl).as_bytes());
|
||||
h.update(identity.member_id().as_bytes());
|
||||
let d = h.finalize();
|
||||
let tag: String = d.as_bytes()[..8].iter().map(|b| format!("{b:02x}")).collect();
|
||||
base.join(tag)
|
||||
}
|
||||
|
||||
/// The simulation's executable for `rtl`: from the cache, or compiled into it.
|
||||
fn build(verilator: &Path, identity: &SimIdentity, rtl: &str) -> Result<PathBuf, FpgaError> {
|
||||
let dir = cache_dir(identity, rtl);
|
||||
let exe = dir.join("obj").join("wq_sim");
|
||||
if exe.is_file() {
|
||||
return Ok(exe);
|
||||
}
|
||||
// Compile beside the cache entry, then move it into place, so that a
|
||||
// concurrent build never sees half a directory.
|
||||
let staging = dir.with_extension(format!("building-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&staging);
|
||||
std::fs::create_dir_all(&staging).map_err(|e| FpgaError::Build(format!("{}: {e}", staging.display())))?;
|
||||
std::fs::write(staging.join("lanes64.v"), rtl).map_err(|e| FpgaError::Build(e.to_string()))?;
|
||||
std::fs::write(staging.join("lanes64_harness.cpp"), fpga::HARNESS).map_err(|e| FpgaError::Build(e.to_string()))?;
|
||||
let out = Command::new(verilator)
|
||||
.current_dir(&staging)
|
||||
.args(fpga::VERILATOR_OPTIONS)
|
||||
.args(["--top-module", "wq_lanes64", "--Mdir", "obj", "-o", "wq_sim", "lanes64.v", "lanes64_harness.cpp"])
|
||||
.output()
|
||||
.map_err(|e| FpgaError::Unavailable(format!("{}: {e}", verilator.display())))?;
|
||||
if !out.status.success() {
|
||||
let log = String::from_utf8_lossy(&out.stderr);
|
||||
let _ = std::fs::remove_dir_all(&staging);
|
||||
return Err(FpgaError::Build(log.lines().filter(|l| l.contains("%Error")).take(8).collect::<Vec<_>>().join("\n")));
|
||||
}
|
||||
match std::fs::rename(&staging, &dir) {
|
||||
Ok(()) => {}
|
||||
// Another process finished first: keep its build.
|
||||
Err(_) if exe.is_file() => {
|
||||
let _ = std::fs::remove_dir_all(&staging);
|
||||
}
|
||||
Err(e) => return Err(FpgaError::Build(format!("{} into {}: {e}", staging.display(), dir.display()))),
|
||||
}
|
||||
Ok(exe)
|
||||
}
|
||||
|
||||
/// The running simulation: its process and pipes.
|
||||
struct Sim {
|
||||
child: Child,
|
||||
stdin: ChildStdin,
|
||||
stdout: ChildStdout,
|
||||
/// Cycles the last run took.
|
||||
cycles: u64,
|
||||
}
|
||||
|
||||
impl Sim {
|
||||
fn start(exe: &Path) -> Result<Sim, FpgaError> {
|
||||
let mut child = Command::new(exe).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().map_err(io)?;
|
||||
let stdin = child.stdin.take().ok_or(FpgaError::Internal("the simulation has no stdin"))?;
|
||||
let stdout = child.stdout.take().ok_or(FpgaError::Internal("the simulation has no stdout"))?;
|
||||
Ok(Sim { child, stdin, stdout, cycles: 0 })
|
||||
}
|
||||
|
||||
fn read_words(&mut self, n: usize) -> Result<Vec<u32>, FpgaError> {
|
||||
let mut bytes = vec![0u8; n * 4];
|
||||
self.stdout.read_exact(&mut bytes).map_err(|e| self.died(e))?;
|
||||
Ok(bytes.as_chunks::<4>().0.iter().map(|w| u32::from_le_bytes(*w)).collect())
|
||||
}
|
||||
|
||||
/// The process's exit status, when a pipe broke because it ended.
|
||||
fn died(&mut self, e: std::io::Error) -> FpgaError {
|
||||
match self.child.try_wait() {
|
||||
Ok(Some(status)) => FpgaError::Simulator(format!("the simulation ended ({status})")),
|
||||
_ => io(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Sim {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stdin.write_all(&[0]);
|
||||
let _ = self.stdin.flush();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
fn open(verilator: &Path, identity: SimIdentity) -> Result<FpgaSimMember, FpgaError> {
|
||||
let exe = build(verilator, &identity, fpga::RTL)?;
|
||||
let sim = Sim::start(&exe)?;
|
||||
let max_qubits = fpga::ADDRESS_BITS.min(MAX_QUBITS);
|
||||
let detail = format!(
|
||||
"fpga datapath, simulated; {}; compiled by {}; on {}; RAM of 2^{} lanes, up to {max_qubits} qubits; two cycles a pair; a metered run's window covers the simulation on this host",
|
||||
identity.simulator,
|
||||
identity.compiler,
|
||||
identity.host,
|
||||
fpga::ADDRESS_BITS,
|
||||
);
|
||||
let member = FpgaSimMember {
|
||||
sim: Arc::new(Mutex::new(sim)),
|
||||
verilator: verilator.to_path_buf(),
|
||||
id: identity.member_id(),
|
||||
key: identity.key(),
|
||||
implementation: fpga::implementation_id(),
|
||||
detail,
|
||||
max_qubits,
|
||||
identity,
|
||||
};
|
||||
let pairs = lanes::probe_pairs(4096, 0x5eed);
|
||||
let out = member.probe(&pairs)?;
|
||||
match lanes::first_mismatch(&pairs, &out) {
|
||||
None => Ok(member),
|
||||
Some((a, b)) => Err(FpgaError::Probe { a, b }),
|
||||
}
|
||||
}
|
||||
|
||||
/// The simulated datapath as a fabric member.
|
||||
#[derive(Clone)]
|
||||
pub struct FpgaSimMember {
|
||||
sim: Arc<Mutex<Sim>>,
|
||||
verilator: PathBuf,
|
||||
identity: SimIdentity,
|
||||
id: String,
|
||||
key: String,
|
||||
implementation: String,
|
||||
detail: String,
|
||||
max_qubits: u8,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FpgaSimMember {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FpgaSimMember").field("id", &self.id).field("implementation", &self.implementation).field("max_qubits", &self.max_qubits).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FpgaSimMember {
|
||||
/// The member id ([`SimIdentity::member_id`]).
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// The key a runner declares for it ([`SimIdentity::key`]).
|
||||
pub fn key(&self) -> &str {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// Always [`FabricClass::Fpga`].
|
||||
pub fn class(&self) -> FabricClass {
|
||||
FabricClass::Fpga
|
||||
}
|
||||
|
||||
/// What discovery found: the simulator, its compiler, the host and the
|
||||
/// datapath's shape. Not signed.
|
||||
pub fn detail(&self) -> &str {
|
||||
&self.detail
|
||||
}
|
||||
|
||||
/// The most qubits a run holds: the RAM's address width.
|
||||
pub fn max_qubits(&self) -> u8 {
|
||||
self.max_qubits
|
||||
}
|
||||
|
||||
/// The implementation id ([`fpga::implementation_id`], or a mutant's).
|
||||
pub fn implementation(&self) -> &str {
|
||||
&self.implementation
|
||||
}
|
||||
|
||||
/// What the member id hashes.
|
||||
pub fn identity(&self) -> &SimIdentity {
|
||||
&self.identity
|
||||
}
|
||||
|
||||
/// The cycles the last run took on the datapath's clock.
|
||||
pub fn cycles(&self) -> u64 {
|
||||
self.lock().cycles
|
||||
}
|
||||
|
||||
fn lock(&self) -> MutexGuard<'_, Sim> {
|
||||
self.sim.lock().unwrap_or_else(|p| p.into_inner())
|
||||
}
|
||||
|
||||
/// The datapath's [`lanes::mul_wide`] and [`lanes::fxmul_wide`] of each
|
||||
/// pair, in order.
|
||||
pub fn probe(&self, pairs: &[(i32, i32)]) -> Result<Vec<ProbeOutput>, FpgaError> {
|
||||
if pairs.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut req = Vec::with_capacity(9 + pairs.len() * 8);
|
||||
req.push(1u8);
|
||||
req.extend_from_slice(&(pairs.len() as u64).to_le_bytes());
|
||||
for &(a, b) in pairs {
|
||||
req.extend_from_slice(&a.to_le_bytes());
|
||||
req.extend_from_slice(&b.to_le_bytes());
|
||||
}
|
||||
let mut sim = self.lock();
|
||||
sim.stdin.write_all(&req).and_then(|()| sim.stdin.flush()).map_err(io)?;
|
||||
let words = sim.read_words(pairs.len() * 4)?;
|
||||
Ok(words.as_chunks::<4>().0.iter().map(|w| ((w[0], w[1]), (w[2], w[3]))).collect())
|
||||
}
|
||||
|
||||
/// The planning this member runs: [`Config::PER_GATE`].
|
||||
pub fn config(&self) -> Config {
|
||||
Config::PER_GATE
|
||||
}
|
||||
|
||||
/// Runs `p` from its basis state: the lanes, and the first op whose exact
|
||||
/// result left 32 bits. A program with tile steps is
|
||||
/// [`FpgaError::Internal`]: this member runs gate steps only.
|
||||
pub fn run_program(&self, p: &Program) -> Result<LaneRun, FpgaError> {
|
||||
Ok(self.run_program_metered(p, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
fn run_program_metered<M: RunMeter + ?Sized>(&self, p: &Program, meter: &mut M) -> Result<MeteredRun, FpgaError> {
|
||||
let n = p.n_qubits;
|
||||
if n == 0 {
|
||||
return Ok((LaneRun { amps: vec![[ONE as i32, 0]], out_of_range: None }, (0, None)));
|
||||
}
|
||||
if n > self.max_qubits {
|
||||
return Err(FpgaError::TooWide { n_qubits: n, max: self.max_qubits });
|
||||
}
|
||||
if n > MAX_QUBITS || p.start >= 1usize << n {
|
||||
return Err(FpgaError::Internal("the start is outside the state"));
|
||||
}
|
||||
let len = 1usize << n;
|
||||
let mut gates = Vec::with_capacity(p.steps.len());
|
||||
for step in &p.steps {
|
||||
let Step::Gate { gate } = *step else { return Err(FpgaError::Internal("a tile step; this member runs gate steps only")) };
|
||||
let g = p.gates.get(gate as usize).ok_or(FpgaError::Internal("a step names a gate the program does not hold"))?;
|
||||
if !g.is_well_formed() {
|
||||
return Err(FpgaError::Internal("malformed gate words for this state"));
|
||||
}
|
||||
gates.push((&g.words, g.index));
|
||||
}
|
||||
let mut init: Vec<Lane> = Vec::new();
|
||||
init.try_reserve_exact(len).map_err(|e| FpgaError::Simulator(e.to_string()))?;
|
||||
init.resize(len, [0, 0]);
|
||||
init[p.start] = [ONE as i32, 0];
|
||||
self.execute(&init, &gates, meter)
|
||||
}
|
||||
|
||||
/// Gates in order from `state`, as [`lanes::run_words`] runs them. A state
|
||||
/// that is not `2^n` lanes (`n ≥ 1`) or words [`lanes::apply_words`]
|
||||
/// refuses for it are [`FpgaError::Internal`].
|
||||
pub fn run_lanes(&self, state: &[Lane], gates: &[GateWords]) -> Result<LaneRun, FpgaError> {
|
||||
let len = state.len();
|
||||
if len < 2 || !len.is_power_of_two() {
|
||||
return Err(FpgaError::Internal("a state that is not 2^n lanes, n at least 1"));
|
||||
}
|
||||
let n = len.trailing_zeros() as u8;
|
||||
if n > self.max_qubits {
|
||||
return Err(FpgaError::TooWide { n_qubits: n, max: self.max_qubits });
|
||||
}
|
||||
let planned: Vec<(&GateWords, u32)> = gates.iter().zip(0u32..).collect();
|
||||
Ok(self.execute(state, &planned, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
/// Loads `state`, runs `gates` in order inside a window of `meter`, and
|
||||
/// reads the report and the state back. Every gate's words are checked
|
||||
/// against the state first.
|
||||
fn execute<M: RunMeter + ?Sized>(&self, state: &[Lane], gates: &[(&GateWords, u32)], meter: &mut M) -> Result<MeteredRun, FpgaError> {
|
||||
let len = state.len();
|
||||
for (w, _) in gates {
|
||||
let (tbit, ctrl) = (w.tbit as usize, w.ctrl as usize);
|
||||
if tbit == 0 || !tbit.is_power_of_two() || tbit > len / 2 || ctrl & tbit != 0 || ctrl & !(len - 1) != 0 {
|
||||
return Err(FpgaError::Internal("malformed gate words for this state"));
|
||||
}
|
||||
}
|
||||
let n = len.trailing_zeros();
|
||||
let mut req: Vec<u8> = Vec::new();
|
||||
req.try_reserve_exact(9 + len * 8 + gates.len() * 44).map_err(|e| FpgaError::Simulator(e.to_string()))?;
|
||||
req.push(2u8);
|
||||
req.extend_from_slice(&n.to_le_bytes());
|
||||
req.extend_from_slice(&(gates.len() as u32).to_le_bytes());
|
||||
for l in state {
|
||||
req.extend_from_slice(&l[0].to_le_bytes());
|
||||
req.extend_from_slice(&l[1].to_le_bytes());
|
||||
}
|
||||
for &(w, index) in gates {
|
||||
for m in w.m {
|
||||
req.extend_from_slice(&m.to_le_bytes());
|
||||
}
|
||||
req.extend_from_slice(&w.tbit.to_le_bytes());
|
||||
req.extend_from_slice(&w.ctrl.to_le_bytes());
|
||||
req.extend_from_slice(&index.to_le_bytes());
|
||||
}
|
||||
let mut window = Window::new(meter);
|
||||
let mut sim = self.lock();
|
||||
window.open();
|
||||
sim.stdin.write_all(&req).and_then(|()| sim.stdin.flush()).map_err(io)?;
|
||||
drop(req);
|
||||
let report = sim.read_words(1)?[0];
|
||||
let words = sim.read_words(2 * len)?;
|
||||
let cycles = sim.read_words(2)?;
|
||||
let reading = window.close();
|
||||
sim.cycles = u64::from(cycles[0]) | (u64::from(cycles[1]) << 32);
|
||||
let out_of_range = Some(report).filter(|&k| k != u32::MAX);
|
||||
let amps: Vec<Lane> = words.as_chunks::<2>().0.iter().map(|w| [w[0] as i32, w[1] as i32]).collect();
|
||||
Ok((LaneRun { amps, out_of_range }, reading))
|
||||
}
|
||||
|
||||
/// Equal to `c.simulate_from(start)`, or a typed refusal: what the CPU
|
||||
/// reference refuses ([`FpgaError::Circuit`]), more qubits than the member
|
||||
/// holds, or a result that leaves 32 bits.
|
||||
pub fn simulate_from(&self, c: &Circuit, start: usize) -> Result<StateVector, FpgaError> {
|
||||
Ok(self.simulate_with(c, start, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
/// [`Self::simulate_from`] inside a window of `meter`.
|
||||
pub fn simulate_metered(&self, c: &Circuit, start: usize, meter: &mut dyn RunMeter) -> Result<(StateVector, u64, Option<EnergyClass>), FpgaError> {
|
||||
self.simulate_with(c, start, meter)
|
||||
}
|
||||
|
||||
fn simulate_with<M: RunMeter + ?Sized>(&self, c: &Circuit, start: usize, meter: &mut M) -> Result<(StateVector, u64, Option<EnergyClass>), FpgaError> {
|
||||
let p = plan::compile_with(c, start, &self.config()).map_err(FpgaError::Circuit)?;
|
||||
if p.n_qubits > self.max_qubits {
|
||||
return Err(FpgaError::TooWide { n_qubits: p.n_qubits, max: self.max_qubits });
|
||||
}
|
||||
let (run, (joules_micro, class)) = self.run_program_metered(&p, meter)?;
|
||||
if let Some(gate) = run.out_of_range {
|
||||
return Err(FpgaError::OutOfRange { gate });
|
||||
}
|
||||
let amps = run.amps.iter().map(|l| Amp { re: i64::from(l[0]), im: i64::from(l[1]) }).collect();
|
||||
Ok((StateVector { n_qubits: p.n_qubits, amps }, joules_micro, class))
|
||||
}
|
||||
|
||||
/// [`Self::simulate_from`] the all-zeros state.
|
||||
pub fn simulate(&self, c: &Circuit) -> Result<StateVector, FpgaError> {
|
||||
self.simulate_from(c, 0)
|
||||
}
|
||||
|
||||
/// The certificate of [`apply_kernel`]`(gs)` as [`Self::implementation`]
|
||||
/// on this member, over `corpus`, earned in a simulator.
|
||||
pub fn certify(&self, gs: Gateset, corpus: Corpus) -> Result<Certificate, FpgaError> {
|
||||
Ok(self.certify_metered(gs, corpus, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
fn certify_metered<M: RunMeter + ?Sized>(&self, gs: Gateset, corpus: Corpus, meter: &mut M) -> Result<(Certificate, Option<(u64, EnergyClass)>), FpgaError> {
|
||||
let mut cert = Certification::new();
|
||||
let mut calibration = EnergyCalibration::new();
|
||||
for c in corpus.circuits(gs) {
|
||||
let reference = lanes::reference_output(&c).map_err(FpgaError::Circuit)?;
|
||||
let (run, (joules_micro, class)) = self.run_program_metered(&plan::compile_with(&c, 0, &self.config()).map_err(FpgaError::Circuit)?, &mut *meter)?;
|
||||
if let Some(gate) = run.out_of_range {
|
||||
return Err(FpgaError::OutOfRange { gate });
|
||||
}
|
||||
cert.add(&lanes::circuit_bytes(&c), &reference, &lanes::output(&run.amps));
|
||||
calibration.add(pair_updates(&c), joules_micro, class.as_ref());
|
||||
}
|
||||
let certificate = cert.finish(apply_kernel(gs), &self.implementation, &self.id, &corpus.generator(gs)).with_provenance(self.identity.provenance());
|
||||
Ok((certificate, calibration.finish()))
|
||||
}
|
||||
|
||||
/// A fabric [`Member`] of class [`FabricClass::Fpga`]: one entry per
|
||||
/// gateset, each certified in this call on `corpus` and earned in a
|
||||
/// simulator, and no energy figure.
|
||||
pub fn member(&self, corpus: Corpus) -> Result<Member, FpgaError> {
|
||||
self.member_with(corpus, &mut Unmetered)
|
||||
}
|
||||
|
||||
/// [`Self::member`], each corpus run inside its own window of `meter`.
|
||||
pub fn member_metered(&self, corpus: Corpus, meter: &mut dyn RunMeter) -> Result<Member, FpgaError> {
|
||||
self.member_with(corpus, meter)
|
||||
}
|
||||
|
||||
fn member_with<M: RunMeter + ?Sized>(&self, corpus: Corpus, meter: &mut M) -> Result<Member, FpgaError> {
|
||||
let mut kernels = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
let (certificate, energy) = self.certify_metered(gs, corpus, &mut *meter)?;
|
||||
kernels.push(KernelEntry { kernel: apply_kernel(gs).into(), implementation: self.implementation.clone(), certificate, energy });
|
||||
}
|
||||
Ok(Member { id: self.id.clone(), class: FabricClass::Fpga, presence: Presence::Detected, kernels, detail: self.detail.clone() })
|
||||
}
|
||||
|
||||
/// The same member holding at most `max_qubits` qubits.
|
||||
#[doc(hidden)]
|
||||
pub fn with_max_qubits(&self, max_qubits: u8) -> FpgaSimMember {
|
||||
FpgaSimMember { max_qubits: max_qubits.min(self.max_qubits), ..self.clone() }
|
||||
}
|
||||
|
||||
/// The same simulator running `rtl` in place of the datapath, under the
|
||||
/// implementation id that text earns: for the mutation tests.
|
||||
#[doc(hidden)]
|
||||
pub fn with_rtl(&self, rtl: &str) -> Result<FpgaSimMember, FpgaError> {
|
||||
let exe = build(&self.verilator, &self.identity, rtl)?;
|
||||
let sim = Sim::start(&exe)?;
|
||||
Ok(FpgaSimMember { sim: Arc::new(Mutex::new(sim)), implementation: fpga::implementation_id_of(rtl), ..self.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
/// The member on the fabric ([`Fabric`](crate::quantum_fabric::Fabric)).
|
||||
impl Runner for FpgaSimMember {
|
||||
fn member_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn run<'r>(&'r self, implementation: &'r str, c: &'r Circuit, start: usize, meter: &'r mut dyn RunMeter) -> RunFuture<'r> {
|
||||
Box::pin(async move {
|
||||
if implementation != self.implementation {
|
||||
return Err(MemberError::Other(format!("member {} runs implementation {}, not {implementation:?}", self.id, self.implementation)));
|
||||
}
|
||||
Ok(self.simulate_with(c, start, meter)?)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn errors_map_onto_the_fabric() {
|
||||
assert!(MemberError::from(FpgaError::OutOfRange { gate: 3 }).falls_back());
|
||||
assert!(!MemberError::from(FpgaError::Probe { a: 1, b: 2 }).falls_back());
|
||||
assert!(!MemberError::from(FpgaError::Internal("x")).falls_back());
|
||||
assert!(matches!(MemberError::from(FpgaError::Simulator("x".into())), MemberError::Device(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cache_keys_on_the_datapath_and_the_simulator() {
|
||||
let id = SimIdentity { simulator: "Verilator 5.052".into(), compiler: "c".into(), host: "h".into() };
|
||||
let other = SimIdentity { simulator: "Verilator 5.054".into(), ..id.clone() };
|
||||
let a = cache_dir(&id, fpga::RTL);
|
||||
assert_ne!(a, cache_dir(&other, fpga::RTL));
|
||||
assert_ne!(a, cache_dir(&id, &fpga::mutate(&fpga::MUTANTS[0])));
|
||||
assert_eq!(a, cache_dir(&id, fpga::RTL));
|
||||
}
|
||||
}
|
||||
853
wai/wai-quantum/src/quantum_fabric_opencl.rs
Normal file
853
wai/wai-quantum/src/quantum_fabric_opencl.rs
Normal file
|
|
@ -0,0 +1,853 @@
|
|||
//! The fabric's OpenCL member: the gate kernel of
|
||||
//! [`quantum_fabric::opencl`](crate::quantum_fabric::opencl) on any OpenCL
|
||||
//! device, GPU or CPU, built at run time by the device's own compiler. See that
|
||||
//! module for the kernel and the argument that it is bit-exact.
|
||||
//!
|
||||
//! - **Discovery** ([`discover`]): the ICD loader is opened when the process
|
||||
//! first asks for it, so a build with this feature runs on a host with no
|
||||
//! OpenCL at all and finds no member there ([`Discovery::unavailable`] says
|
||||
//! why). Every device of every platform is opened, the kernel is built for
|
||||
//! it, and its arithmetic is probed against [`lanes`] in this process. A
|
||||
//! device that cannot host the kernel, whose arithmetic disagrees, or which
|
||||
//! no fabric class names (an OpenCL accelerator), is listed with the reason
|
||||
//! ([`Refusal`]); one that passes is an [`OpenClMember`], present as
|
||||
//! [`Presence::Detected`].
|
||||
//! - **Running** ([`OpenClMember::run_program`]): the state stays on the
|
||||
//! device; one launch per gate, in op-log order, on one in-order queue; then
|
||||
//! the report word and the state come back. The kernel reports exactly when
|
||||
//! the reference's result leaves 32 bits; the member then returns
|
||||
//! [`OpenClError::OutOfRange`], never a state.
|
||||
//! - **Certification, placement, receipts and metering** are as for the CUDA
|
||||
//! member ([`crate::quantum_fabric_cuda`]).
|
||||
//!
|
||||
//! Every call is synchronous; the device is waited on before a call returns.
|
||||
//! No type of the OpenCL bindings appears in this module's public API.
|
||||
//!
|
||||
//! # Honest boundaries
|
||||
//!
|
||||
//! - **A member is what this process measured.** The probe and the
|
||||
//! certificates are earned per process; the member id binds the platform,
|
||||
//! the device, its OpenCL and driver versions, its UUID where the platform
|
||||
//! reports one, its compute units and its memory.
|
||||
//! - **A CPU device is a CPU member.** Its class is [`FabricClass::Cpu`], and
|
||||
//! its id sorts after the CPU reference's, so with no energy figures
|
||||
//! placement keeps the reference.
|
||||
//! - **Plans are per gate** ([`Config::PER_GATE`]), as for the CUDA member.
|
||||
//! - **Unmetered by default**, as for every member.
|
||||
|
||||
use crate::quantum::{Amp, Circuit, Gateset, MAX_QUBITS, ONE, QuantumError, StateVector};
|
||||
use crate::quantum_energy::EnergyClass;
|
||||
use crate::quantum_fabric::lanes::{self, GateWords, Lane};
|
||||
use crate::quantum_fabric::opencl::{self, DeviceIdentity, DeviceKind};
|
||||
use crate::quantum_fabric::plan::{self, Config, LaneRun, Program, Step};
|
||||
use crate::quantum_fabric::{
|
||||
Certificate, Certification, Corpus, EnergyCalibration, FabricClass, KernelEntry, Member, MemberError, Presence, RunFuture, RunMeter, Runner, Unmetered, Window,
|
||||
apply_kernel, pair_updates,
|
||||
};
|
||||
use opencl3::command_queue::CommandQueue;
|
||||
use opencl3::context::Context;
|
||||
use opencl3::device::{CL_DEVICE_TYPE_ALL, CL_DEVICE_TYPE_CPU, CL_DEVICE_TYPE_GPU, Device};
|
||||
use opencl3::error_codes::ClError;
|
||||
use opencl3::kernel::{ExecuteKernel, Kernel as ClKernel};
|
||||
use opencl3::memory::{Buffer, CL_MEM_READ_WRITE};
|
||||
use opencl3::platform::{Platform, get_platforms};
|
||||
use opencl3::program::Program as ClProgram;
|
||||
use opencl3::types::{CL_BLOCKING, cl_device_id, cl_int, cl_uint, cl_ulong};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
/// One probe case's device results: [`lanes::mul_wide`] and
|
||||
/// [`lanes::fxmul_wide`], each as `(high, low)` words.
|
||||
pub type ProbeOutput = ((u32, u32), (u32, u32));
|
||||
|
||||
/// A run's lanes and report, with the meter's accepted reading (zero and
|
||||
/// `None` when unmetered).
|
||||
type MeteredRun = (LaneRun, (u64, Option<EnergyClass>));
|
||||
|
||||
/// The OpenCL status codes that mean memory or resources ran out:
|
||||
/// `CL_MEM_OBJECT_ALLOCATION_FAILURE`, `CL_OUT_OF_RESOURCES` and
|
||||
/// `CL_OUT_OF_HOST_MEMORY`.
|
||||
const OUT_OF_MEMORY: [cl_int; 3] = [-4, -5, -6];
|
||||
|
||||
/// The status the bindings return when the ICD loader cannot be opened.
|
||||
const LOADER_MISSING: cl_int = -2000;
|
||||
|
||||
/// Why a device is not a member, or a run has no state.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum OpenClError {
|
||||
/// The ICD loader could not be used, or it lists no platform, and why.
|
||||
Unavailable(String),
|
||||
/// The device's compiler refused the kernel; its build log.
|
||||
Build(String),
|
||||
/// The runtime reported an error, or the device was lost.
|
||||
Device(String),
|
||||
/// Out of memory or resources on the device or the host.
|
||||
Memory(String),
|
||||
/// Exactly what the CPU reference refuses.
|
||||
Circuit(QuantumError),
|
||||
/// More qubits than this member's memory holds.
|
||||
TooWide { n_qubits: u8, max: u8 },
|
||||
/// The reference's result leaves 32 bits at this op; the member returns
|
||||
/// no state.
|
||||
OutOfRange { gate: u32 },
|
||||
/// A program the host cannot lay out (malformed words, a start outside the
|
||||
/// state, a step this member does not run): a defect of the host, never a
|
||||
/// result.
|
||||
Internal(&'static str),
|
||||
/// The device's arithmetic disagreed with the CPU's on this pair.
|
||||
Probe { a: i32, b: i32 },
|
||||
/// A device the fabric cannot use as a member, and why.
|
||||
Unsupported(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for OpenClError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
OpenClError::Unavailable(why) => write!(f, "OpenCL is unavailable: {why}"),
|
||||
OpenClError::Build(log) => write!(f, "the kernel did not build: {log}"),
|
||||
OpenClError::Device(e) => write!(f, "device: {e}"),
|
||||
OpenClError::Memory(e) => write!(f, "out of memory: {e}"),
|
||||
OpenClError::Circuit(e) => write!(f, "{e}"),
|
||||
OpenClError::TooWide { n_qubits, max } => write!(f, "{n_qubits} qubits; this member holds at most {max}"),
|
||||
OpenClError::OutOfRange { gate } => write!(f, "the result of op {gate} leaves 32 bits"),
|
||||
OpenClError::Internal(why) => write!(f, "host defect: {why}"),
|
||||
OpenClError::Probe { a, b } => write!(f, "the device's arithmetic differs from the CPU's at ({a}, {b})"),
|
||||
OpenClError::Unsupported(why) => write!(f, "not a member: {why}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OpenClError {}
|
||||
|
||||
impl From<OpenClError> for MemberError {
|
||||
fn from(e: OpenClError) -> MemberError {
|
||||
match e {
|
||||
OpenClError::TooWide { n_qubits, max } => MemberError::TooWide { n_qubits, max },
|
||||
OpenClError::OutOfRange { gate } => MemberError::OutOfRange { gate },
|
||||
OpenClError::Memory(why) => MemberError::Memory(why),
|
||||
OpenClError::Device(why) => MemberError::Device(why),
|
||||
OpenClError::Circuit(e) => MemberError::Circuit(e),
|
||||
other => MemberError::Other(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A runtime error as this module reports it: out of memory or resources as
|
||||
/// [`OpenClError::Memory`], anything else as [`OpenClError::Device`].
|
||||
fn cl(e: ClError) -> OpenClError {
|
||||
if OUT_OF_MEMORY.contains(&e.0) { OpenClError::Memory(e.to_string()) } else { OpenClError::Device(e.to_string()) }
|
||||
}
|
||||
|
||||
/// A device that is not a member, and why.
|
||||
#[derive(Debug)]
|
||||
pub struct Refusal {
|
||||
/// The device's name, or its platform and position when it gave none.
|
||||
pub device: String,
|
||||
/// Its CI key ([`DeviceIdentity::key`]), when the device could be read.
|
||||
pub key: String,
|
||||
pub error: OpenClError,
|
||||
}
|
||||
|
||||
/// What [`discover`] found.
|
||||
#[derive(Debug)]
|
||||
pub struct Discovery {
|
||||
/// Devices that built the kernel and passed the probe, in platform order.
|
||||
pub found: Vec<OpenClMember>,
|
||||
/// Devices that did not, with why.
|
||||
pub refused: Vec<Refusal>,
|
||||
/// Why no device was even listed: no ICD loader, or no platform. `None`
|
||||
/// when a platform was listed.
|
||||
pub unavailable: Option<String>,
|
||||
}
|
||||
|
||||
/// The platforms the ICD loader lists, or why there are none.
|
||||
fn platforms() -> Result<Vec<Platform>, String> {
|
||||
match get_platforms() {
|
||||
Err(e) if e.0 == LOADER_MISSING => Err("no OpenCL ICD loader on this host".into()),
|
||||
Err(e) => Err(format!("the ICD loader lists no platform: {e}")),
|
||||
Ok(p) if p.is_empty() => Err("the ICD loader lists no platform".into()),
|
||||
Ok(p) => Ok(p),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the ICD loader opens and lists a platform, or why not.
|
||||
pub fn available() -> Result<(), String> {
|
||||
platforms().map(|_| ())
|
||||
}
|
||||
|
||||
/// Every device of every platform the ICD loader lists, each opened, built for
|
||||
/// and probed in this process before it is listed. On a host without OpenCL,
|
||||
/// nothing is found and [`Discovery::unavailable`] says why.
|
||||
pub fn discover() -> Discovery {
|
||||
let mut d = Discovery { found: Vec::new(), refused: Vec::new(), unavailable: None };
|
||||
let platforms = match platforms() {
|
||||
Ok(p) => p,
|
||||
Err(why) => {
|
||||
d.unavailable = Some(why);
|
||||
return d;
|
||||
}
|
||||
};
|
||||
for platform in platforms {
|
||||
let info = PlatformInfo {
|
||||
name: platform.name().unwrap_or_default(),
|
||||
version: platform.version().unwrap_or_default(),
|
||||
vendor: platform.vendor().unwrap_or_default(),
|
||||
};
|
||||
let ids = match platform.get_devices(CL_DEVICE_TYPE_ALL) {
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
d.refused.push(Refusal { device: format!("platform {:?}", info.name), key: String::new(), error: cl(e) });
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for (k, id) in ids.into_iter().enumerate() {
|
||||
match open(&info, k, id) {
|
||||
Ok(m) => d.found.push(m),
|
||||
Err(r) => d.refused.push(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
struct PlatformInfo {
|
||||
name: String,
|
||||
version: String,
|
||||
vendor: String,
|
||||
}
|
||||
|
||||
/// The widest state of `2^n` interleaved 8-byte lanes that `bytes` holds, at
|
||||
/// most [`MAX_QUBITS`].
|
||||
fn qubits_for(bytes: u64) -> u8 {
|
||||
let lanes = bytes / 8;
|
||||
if lanes < 2 { 0 } else { (lanes.ilog2() as u8).min(MAX_QUBITS) }
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
/// Reads the device, builds the kernel for it and probes it.
|
||||
fn open(platform: &PlatformInfo, position: usize, id: cl_device_id) -> Result<OpenClMember, Refusal> {
|
||||
let unread = |error: OpenClError| Refusal { device: format!("{:?} device {position}", platform.name), key: String::new(), error };
|
||||
let device = Device::new(id);
|
||||
let name = device.name().map_err(|e| unread(cl(e)))?;
|
||||
let kind = match device.dev_type().map_err(|e| unread(cl(e)))? {
|
||||
t if t & CL_DEVICE_TYPE_GPU != 0 => DeviceKind::Gpu,
|
||||
t if t & CL_DEVICE_TYPE_CPU != 0 => DeviceKind::Cpu,
|
||||
_ => DeviceKind::Accelerator,
|
||||
};
|
||||
let extensions = device.extensions().unwrap_or_default();
|
||||
let uuid = if extensions.split_whitespace().any(|x| x == "cl_khr_device_uuid") { device.uuid_khr().map(|u| hex(&u)).unwrap_or_default() } else { String::new() };
|
||||
let memory = device.global_mem_size().map_err(|e| unread(cl(e)))?;
|
||||
let identity = DeviceIdentity {
|
||||
platform: platform.name.clone(),
|
||||
platform_version: platform.version.clone(),
|
||||
vendor: platform.vendor.clone(),
|
||||
name: name.clone(),
|
||||
version: device.version().unwrap_or_default(),
|
||||
driver: device.driver_version().unwrap_or_default(),
|
||||
kind,
|
||||
uuid,
|
||||
compute_units: device.max_compute_units().unwrap_or(0),
|
||||
memory,
|
||||
};
|
||||
let key = identity.key();
|
||||
let refuse = |error: OpenClError| Refusal { device: name.clone(), key: key.clone(), error };
|
||||
let Some(class) = identity.class() else {
|
||||
return Err(refuse(OpenClError::Unsupported("an OpenCL accelerator, which no fabric class names".into())));
|
||||
};
|
||||
if !device.available().unwrap_or(false) {
|
||||
return Err(refuse(OpenClError::Unsupported("the device reports itself unavailable".into())));
|
||||
}
|
||||
let context = Context::from_device(&device).map_err(|e| refuse(cl(e)))?;
|
||||
#[allow(deprecated)] // clCreateCommandQueue: OpenCL 1.2 platforms have no other.
|
||||
let queue = CommandQueue::create_default(&context, 0).map_err(|e| refuse(cl(e)))?;
|
||||
let kernel = Kernel::build(&context, opencl::SOURCE).map_err(refuse)?;
|
||||
// The state is one buffer: no larger than one allocation, or half the
|
||||
// device's memory.
|
||||
let max_alloc = device.max_mem_alloc_size().unwrap_or(0);
|
||||
let max_qubits = qubits_for(max_alloc.min(memory / 2));
|
||||
let detail = format!(
|
||||
"opencl; platform {:?} ({}); device {name:?}, {}; {} compute units; {}; driver {}; uuid {}; memory {memory} bytes, {max_alloc} per allocation; up to {max_qubits} qubits; a metered run's window runs from the first upload to the last readback",
|
||||
identity.platform,
|
||||
identity.platform_version,
|
||||
kind_label(kind),
|
||||
identity.compute_units,
|
||||
identity.version,
|
||||
identity.driver,
|
||||
if identity.uuid.is_empty() { "not reported" } else { &identity.uuid },
|
||||
);
|
||||
let member = OpenClMember {
|
||||
context: Arc::new(context),
|
||||
queue: Arc::new(queue),
|
||||
kernel: Arc::new(Mutex::new(kernel)),
|
||||
id: identity.member_id(),
|
||||
key: key.clone(),
|
||||
class,
|
||||
implementation: opencl::implementation_id(),
|
||||
detail,
|
||||
max_qubits,
|
||||
binary64: extensions.split_whitespace().any(|x| x == "cl_khr_fp64"),
|
||||
identity,
|
||||
};
|
||||
let pairs = lanes::probe_pairs(4096, 0x5eed);
|
||||
match member.probe(&pairs) {
|
||||
Ok(out) => match lanes::first_mismatch(&pairs, &out) {
|
||||
None => Ok(member),
|
||||
Some((a, b)) => Err(refuse(OpenClError::Probe { a, b })),
|
||||
},
|
||||
Err(e) => Err(refuse(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn kind_label(kind: DeviceKind) -> &'static str {
|
||||
match kind {
|
||||
DeviceKind::Gpu => "a GPU",
|
||||
DeviceKind::Cpu => "a CPU",
|
||||
DeviceKind::Accelerator => "an accelerator",
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel built for one device: its two entry points.
|
||||
struct Kernel {
|
||||
apply: ClKernel,
|
||||
probe: ClKernel,
|
||||
}
|
||||
|
||||
impl Kernel {
|
||||
fn build(context: &Context, source: &str) -> Result<Kernel, OpenClError> {
|
||||
let program = ClProgram::create_and_build_from_source(context, source, opencl::BUILD_OPTIONS).map_err(OpenClError::Build)?;
|
||||
Ok(Kernel { apply: ClKernel::create(&program, "apply_gate").map_err(cl)?, probe: ClKernel::create(&program, "probe").map_err(cl)? })
|
||||
}
|
||||
}
|
||||
|
||||
/// The global work size for `work` items: at most [`opencl::GLOBAL_MAX`],
|
||||
/// the kernel striding over the rest; the work-group size is the runtime's.
|
||||
fn global(work: u64) -> usize {
|
||||
work.clamp(1, opencl::GLOBAL_MAX) as usize
|
||||
}
|
||||
|
||||
/// One OpenCL device as a fabric member: a context and an in-order queue on
|
||||
/// it, the kernel built for it, and the arithmetic probe passed in this
|
||||
/// process.
|
||||
#[derive(Clone)]
|
||||
pub struct OpenClMember {
|
||||
context: Arc<Context>,
|
||||
queue: Arc<CommandQueue>,
|
||||
/// Behind a lock: setting a kernel's arguments is the one OpenCL call that
|
||||
/// is not thread-safe, so each launch holds it from the first argument to
|
||||
/// the enqueue.
|
||||
kernel: Arc<Mutex<Kernel>>,
|
||||
identity: DeviceIdentity,
|
||||
id: String,
|
||||
key: String,
|
||||
class: FabricClass,
|
||||
implementation: String,
|
||||
detail: String,
|
||||
max_qubits: u8,
|
||||
/// Whether the device has binary64 (`cl_khr_fp64`).
|
||||
binary64: bool,
|
||||
}
|
||||
|
||||
// SAFETY: every OpenCL API call is thread-safe except those that change a
|
||||
// kernel object's state (OpenCL 3.0 API, "Multiple Host Threads"):
|
||||
// `clSetKernelArg` and the enqueue that reads the arguments run only while
|
||||
// `kernel`'s lock is held. The context and the queue are only passed to calls
|
||||
// that are thread-safe.
|
||||
unsafe impl Send for OpenClMember {}
|
||||
// SAFETY: as above.
|
||||
unsafe impl Sync for OpenClMember {}
|
||||
|
||||
impl std::fmt::Debug for OpenClMember {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OpenClMember").field("id", &self.id).field("key", &self.key).field("implementation", &self.implementation).field("max_qubits", &self.max_qubits).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenClMember {
|
||||
/// The member id ([`DeviceIdentity::member_id`]).
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// The key a runner declares for it ([`DeviceIdentity::key`]).
|
||||
pub fn key(&self) -> &str {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// [`FabricClass::Gpu`] or [`FabricClass::Cpu`].
|
||||
pub fn class(&self) -> FabricClass {
|
||||
self.class
|
||||
}
|
||||
|
||||
/// What discovery found: platform, device, versions, memory, and what a
|
||||
/// metered run's window covers. Not signed.
|
||||
pub fn detail(&self) -> &str {
|
||||
&self.detail
|
||||
}
|
||||
|
||||
/// The most qubits a run holds on this member.
|
||||
pub fn max_qubits(&self) -> u8 {
|
||||
self.max_qubits
|
||||
}
|
||||
|
||||
/// The implementation id ([`opencl::implementation_id`], or a mutant's).
|
||||
pub fn implementation(&self) -> &str {
|
||||
&self.implementation
|
||||
}
|
||||
|
||||
/// What the member id hashes.
|
||||
pub fn identity(&self) -> &DeviceIdentity {
|
||||
&self.identity
|
||||
}
|
||||
|
||||
/// Whether the device has binary64 arithmetic (`cl_khr_fp64`), which the
|
||||
/// state vector needs.
|
||||
pub fn has_binary64(&self) -> bool {
|
||||
self.binary64
|
||||
}
|
||||
|
||||
fn kernel(&self) -> MutexGuard<'_, Kernel> {
|
||||
self.kernel.lock().unwrap_or_else(|p| p.into_inner())
|
||||
}
|
||||
|
||||
/// A device buffer of `data`.
|
||||
fn upload<T: Copy>(&self, data: &[T]) -> Result<Buffer<T>, OpenClError> {
|
||||
// SAFETY: no host pointer is given; the buffer is `data.len()` items.
|
||||
let mut b = unsafe { Buffer::<T>::create(&self.context, CL_MEM_READ_WRITE, data.len(), std::ptr::null_mut()) }.map_err(cl)?;
|
||||
// SAFETY: a blocking write of exactly the buffer's size.
|
||||
unsafe { self.queue.enqueue_write_buffer(&mut b, CL_BLOCKING, 0, data, &[]) }.map_err(cl)?;
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
/// The contents of `b`, `len` items.
|
||||
fn download<T: Copy + Default>(&self, b: &Buffer<T>, len: usize) -> Result<Vec<T>, OpenClError> {
|
||||
let mut out: Vec<T> = Vec::new();
|
||||
out.try_reserve_exact(len).map_err(|e| OpenClError::Memory(e.to_string()))?;
|
||||
out.resize(len, T::default());
|
||||
// SAFETY: a blocking read of exactly the buffer's size into `out`.
|
||||
unsafe { self.queue.enqueue_read_buffer(b, CL_BLOCKING, 0, &mut out, &[]) }.map_err(cl)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The device's [`lanes::mul_wide`] and [`lanes::fxmul_wide`] of each
|
||||
/// pair, in order.
|
||||
pub fn probe(&self, pairs: &[(i32, i32)]) -> Result<Vec<ProbeOutput>, OpenClError> {
|
||||
if pairs.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let flat: Vec<cl_int> = pairs.iter().flat_map(|&(a, b)| [a, b]).collect();
|
||||
let cases = self.upload(&flat)?;
|
||||
let out = self.upload(&vec![0 as cl_uint; pairs.len() * 4])?;
|
||||
let count = pairs.len() as cl_ulong;
|
||||
{
|
||||
let k = self.kernel();
|
||||
// SAFETY: `cases` holds `count` int2 cases and `out` `count` uint4
|
||||
// outputs, the kernel's arguments in order.
|
||||
unsafe { ExecuteKernel::new(&k.probe).set_arg(&cases).set_arg(&out).set_arg(&count).set_global_work_size(global(count)).enqueue_nd_range(&self.queue) }
|
||||
.map_err(cl)?;
|
||||
}
|
||||
let words = self.download(&out, pairs.len() * 4)?;
|
||||
Ok(words.as_chunks::<4>().0.iter().map(|w| ((w[0], w[1]), (w[2], w[3]))).collect())
|
||||
}
|
||||
|
||||
/// The planning this member runs: [`Config::PER_GATE`].
|
||||
pub fn config(&self) -> Config {
|
||||
Config::PER_GATE
|
||||
}
|
||||
|
||||
/// Runs `p` from its basis state: the lanes, and the first op whose exact
|
||||
/// result left 32 bits. A program another config planned (one with tile
|
||||
/// steps) is [`OpenClError::Internal`]: this member runs gate steps only.
|
||||
pub fn run_program(&self, p: &Program) -> Result<LaneRun, OpenClError> {
|
||||
Ok(self.run_program_metered(p, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
fn run_program_metered<M: RunMeter + ?Sized>(&self, p: &Program, meter: &mut M) -> Result<MeteredRun, OpenClError> {
|
||||
let n = p.n_qubits;
|
||||
if n == 0 {
|
||||
return Ok((LaneRun { amps: vec![[ONE as i32, 0]], out_of_range: None }, (0, None)));
|
||||
}
|
||||
if n > self.max_qubits {
|
||||
return Err(OpenClError::TooWide { n_qubits: n, max: self.max_qubits });
|
||||
}
|
||||
if n > MAX_QUBITS || p.start >= 1usize << n {
|
||||
return Err(OpenClError::Internal("the start is outside the state"));
|
||||
}
|
||||
let len = 1usize << n;
|
||||
let mut gates = Vec::with_capacity(p.steps.len());
|
||||
for step in &p.steps {
|
||||
let Step::Gate { gate } = *step else { return Err(OpenClError::Internal("a tile step; this member runs gate steps only")) };
|
||||
let g = p.gates.get(gate as usize).ok_or(OpenClError::Internal("a step names a gate the program does not hold"))?;
|
||||
if !g.is_well_formed() {
|
||||
return Err(OpenClError::Internal("malformed gate words for this state"));
|
||||
}
|
||||
gates.push((&g.words, g.index));
|
||||
}
|
||||
let mut init: Vec<i32> = Vec::new();
|
||||
init.try_reserve_exact(2 * len).map_err(|e| OpenClError::Memory(e.to_string()))?;
|
||||
init.resize(2 * len, 0);
|
||||
init[2 * p.start] = ONE as i32;
|
||||
self.execute(init, &gates, meter)
|
||||
}
|
||||
|
||||
/// Gates in order from `state`, as [`lanes::run_words`] runs them: the
|
||||
/// lanes, and the lowest position in `gates` of a gate whose exact result
|
||||
/// left 32 bits. A state that is not `2^n` lanes (`n ≥ 1`) or words
|
||||
/// [`lanes::apply_words`] refuses for it are [`OpenClError::Internal`].
|
||||
pub fn run_lanes(&self, state: &[Lane], gates: &[GateWords]) -> Result<LaneRun, OpenClError> {
|
||||
let len = state.len();
|
||||
if len < 2 || !len.is_power_of_two() {
|
||||
return Err(OpenClError::Internal("a state that is not 2^n lanes, n at least 1"));
|
||||
}
|
||||
let n = len.trailing_zeros() as u8;
|
||||
if n > self.max_qubits {
|
||||
return Err(OpenClError::TooWide { n_qubits: n, max: self.max_qubits });
|
||||
}
|
||||
let planned: Vec<(&GateWords, u32)> = gates.iter().zip(0u32..).collect();
|
||||
let mut init: Vec<i32> = Vec::new();
|
||||
init.try_reserve_exact(2 * len).map_err(|e| OpenClError::Memory(e.to_string()))?;
|
||||
init.extend(state.iter().flat_map(|l| [l[0], l[1]]));
|
||||
Ok(self.execute(init, &planned, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
/// Uploads `init` (interleaved lanes, `2·2^n` words), runs `gates` in order
|
||||
/// inside a window of `meter`, and reads the report and the state back.
|
||||
/// Every gate's words are checked against the state first: the words
|
||||
/// [`lanes::apply_words`] refuses, the host refuses.
|
||||
fn execute<M: RunMeter + ?Sized>(&self, init: Vec<i32>, gates: &[(&GateWords, u32)], meter: &mut M) -> Result<MeteredRun, OpenClError> {
|
||||
let len = init.len() / 2;
|
||||
for (w, _) in gates {
|
||||
let (tbit, ctrl) = (w.tbit as usize, w.ctrl as usize);
|
||||
if tbit == 0 || !tbit.is_power_of_two() || tbit > len / 2 || ctrl & tbit != 0 || ctrl & !(len - 1) != 0 {
|
||||
return Err(OpenClError::Internal("malformed gate words for this state"));
|
||||
}
|
||||
}
|
||||
let pairs = (len / 2) as cl_ulong;
|
||||
let mut window = Window::new(meter);
|
||||
window.open();
|
||||
let amps = self.upload(&init)?;
|
||||
drop(init);
|
||||
let report = self.upload(&[u32::MAX as cl_uint])?;
|
||||
{
|
||||
let k = self.kernel();
|
||||
for &(w, index) in gates {
|
||||
let m: [cl_int; 8] = w.m.map(|x| x as i32);
|
||||
let mut launch = ExecuteKernel::new(&k.apply);
|
||||
// SAFETY: `amps` holds `2·pairs` int2 lanes and `report` one word,
|
||||
// the words were checked against the state above, and the
|
||||
// arguments are the kernel's, in order.
|
||||
unsafe {
|
||||
launch.set_arg(&s).set_arg(&pairs).set_arg(&w.tbit).set_arg(&w.ctrl);
|
||||
for v in &m {
|
||||
launch.set_arg(v);
|
||||
}
|
||||
launch.set_arg(&index).set_arg(&report).set_global_work_size(global(pairs)).enqueue_nd_range(&self.queue)
|
||||
}
|
||||
.map_err(cl)?;
|
||||
}
|
||||
}
|
||||
let bad = self.download(&report, 1)?;
|
||||
let words = self.download(&s, 2 * len)?;
|
||||
let reading = window.close();
|
||||
let out_of_range = bad.first().copied().filter(|&k| k != u32::MAX);
|
||||
let mut lanes_out: Vec<Lane> = Vec::new();
|
||||
lanes_out.try_reserve_exact(len).map_err(|e| OpenClError::Memory(e.to_string()))?;
|
||||
lanes_out.extend(words.as_chunks::<2>().0.iter().copied());
|
||||
Ok((LaneRun { amps: lanes_out, out_of_range }, reading))
|
||||
}
|
||||
|
||||
/// Equal to `c.simulate_from(start)`, or a typed refusal: what the CPU
|
||||
/// reference refuses ([`OpenClError::Circuit`]), more qubits than the
|
||||
/// member holds, or a result that leaves 32 bits.
|
||||
pub fn simulate_from(&self, c: &Circuit, start: usize) -> Result<StateVector, OpenClError> {
|
||||
Ok(self.simulate_with(c, start, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
/// [`Self::simulate_from`] inside a window of `meter`: the state, and the
|
||||
/// microjoules and class the meter read, which are zero and `None` unless
|
||||
/// the crate's label check accepts them. The window opens just before the
|
||||
/// first upload and closes after the last readback.
|
||||
pub fn simulate_metered(&self, c: &Circuit, start: usize, meter: &mut dyn RunMeter) -> Result<(StateVector, u64, Option<EnergyClass>), OpenClError> {
|
||||
self.simulate_with(c, start, meter)
|
||||
}
|
||||
|
||||
fn simulate_with<M: RunMeter + ?Sized>(&self, c: &Circuit, start: usize, meter: &mut M) -> Result<(StateVector, u64, Option<EnergyClass>), OpenClError> {
|
||||
let p = plan::compile_with(c, start, &self.config()).map_err(OpenClError::Circuit)?;
|
||||
if p.n_qubits > self.max_qubits {
|
||||
return Err(OpenClError::TooWide { n_qubits: p.n_qubits, max: self.max_qubits });
|
||||
}
|
||||
let (run, (joules_micro, class)) = self.run_program_metered(&p, meter)?;
|
||||
if let Some(gate) = run.out_of_range {
|
||||
return Err(OpenClError::OutOfRange { gate });
|
||||
}
|
||||
let mut amps = Vec::new();
|
||||
amps.try_reserve_exact(run.amps.len()).map_err(|e| OpenClError::Memory(e.to_string()))?;
|
||||
amps.extend(run.amps.iter().map(|l| Amp { re: i64::from(l[0]), im: i64::from(l[1]) }));
|
||||
Ok((StateVector { n_qubits: p.n_qubits, amps }, joules_micro, class))
|
||||
}
|
||||
|
||||
/// [`Self::simulate_from`] the all-zeros state.
|
||||
pub fn simulate(&self, c: &Circuit) -> Result<StateVector, OpenClError> {
|
||||
self.simulate_from(c, 0)
|
||||
}
|
||||
|
||||
/// The certificate of [`apply_kernel`]`(gs)` as [`Self::implementation`]
|
||||
/// on this member, over `corpus`: each circuit run from `|0…0⟩`, against
|
||||
/// the native simulator. A range report inside a corpus is an error: its
|
||||
/// circuits are valid and within the horizon.
|
||||
pub fn certify(&self, gs: Gateset, corpus: Corpus) -> Result<Certificate, OpenClError> {
|
||||
Ok(self.certify_metered(gs, corpus, &mut Unmetered)?.0)
|
||||
}
|
||||
|
||||
fn certify_metered<M: RunMeter + ?Sized>(&self, gs: Gateset, corpus: Corpus, meter: &mut M) -> Result<(Certificate, Option<(u64, EnergyClass)>), OpenClError> {
|
||||
let mut cert = Certification::new();
|
||||
let mut calibration = EnergyCalibration::new();
|
||||
for c in corpus.circuits(gs) {
|
||||
let reference = lanes::reference_output(&c).map_err(OpenClError::Circuit)?;
|
||||
let (run, (joules_micro, class)) = self.run_program_metered(&plan::compile_with(&c, 0, &self.config()).map_err(OpenClError::Circuit)?, &mut *meter)?;
|
||||
if let Some(gate) = run.out_of_range {
|
||||
return Err(OpenClError::OutOfRange { gate });
|
||||
}
|
||||
cert.add(&lanes::circuit_bytes(&c), &reference, &lanes::output(&run.amps));
|
||||
calibration.add(pair_updates(&c), joules_micro, class.as_ref());
|
||||
}
|
||||
Ok((cert.finish(apply_kernel(gs), &self.implementation, &self.id, &corpus.generator(gs)), calibration.finish()))
|
||||
}
|
||||
|
||||
/// A fabric [`Member`]: present as [`Presence::Detected`], with one entry
|
||||
/// per gateset, each certified in this call on `corpus`, and no energy
|
||||
/// figure.
|
||||
pub fn member(&self, corpus: Corpus) -> Result<Member, OpenClError> {
|
||||
self.member_with(corpus, &mut Unmetered)
|
||||
}
|
||||
|
||||
/// [`Self::member`], each corpus run inside its own window of `meter`:
|
||||
/// every entry also carries the energy figure its runs earn
|
||||
/// ([`EnergyCalibration`]), or none if any run went unmetered.
|
||||
pub fn member_metered(&self, corpus: Corpus, meter: &mut dyn RunMeter) -> Result<Member, OpenClError> {
|
||||
self.member_with(corpus, meter)
|
||||
}
|
||||
|
||||
fn member_with<M: RunMeter + ?Sized>(&self, corpus: Corpus, meter: &mut M) -> Result<Member, OpenClError> {
|
||||
let mut kernels = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
let (certificate, energy) = self.certify_metered(gs, corpus, &mut *meter)?;
|
||||
kernels.push(KernelEntry { kernel: apply_kernel(gs).into(), implementation: self.implementation.clone(), certificate, energy });
|
||||
}
|
||||
Ok(Member { id: self.id.clone(), class: self.class, presence: Presence::Detected, kernels, detail: self.detail.clone() })
|
||||
}
|
||||
|
||||
/// The same member holding at most `max_qubits` qubits (no more than it
|
||||
/// holds), so that tests can make it refuse a circuit as too wide.
|
||||
#[doc(hidden)]
|
||||
pub fn with_max_qubits(&self, max_qubits: u8) -> OpenClMember {
|
||||
OpenClMember { max_qubits: max_qubits.min(self.max_qubits), ..self.clone() }
|
||||
}
|
||||
|
||||
/// The same device running `source` in place of the kernel, under the
|
||||
/// implementation id that text earns: for the mutation tests, which
|
||||
/// require every mutant to be caught.
|
||||
#[doc(hidden)]
|
||||
pub fn with_source(&self, source: &str) -> Result<OpenClMember, OpenClError> {
|
||||
let kernel = Kernel::build(&self.context, source)?;
|
||||
Ok(OpenClMember { kernel: Arc::new(Mutex::new(kernel)), implementation: opencl::implementation_id_of(source), ..self.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
/// The dense f64 state vector on this device
|
||||
/// ([`quantum_fabric::sv64`](crate::quantum_fabric::sv64)), as for the CUDA
|
||||
/// member: the CPU's launches, each formula one IEEE binary64 operation at a
|
||||
/// time, contraction off. A device without binary64 refuses it.
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
#[derive(Clone)]
|
||||
pub struct OpenClSv {
|
||||
member: OpenClMember,
|
||||
kernels: Arc<Mutex<SvKernels>>,
|
||||
implementation: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
struct SvKernels {
|
||||
diag: ClKernel,
|
||||
one: ClKernel,
|
||||
many: ClKernel,
|
||||
}
|
||||
|
||||
// SAFETY: as for [`OpenClMember`]: the kernels' arguments are set and enqueued
|
||||
// only under their lock.
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
unsafe impl Send for OpenClSv {}
|
||||
// SAFETY: as above.
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
unsafe impl Sync for OpenClSv {}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
impl std::fmt::Debug for OpenClSv {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OpenClSv").field("member", &self.member.id).field("implementation", &self.implementation).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
impl OpenClMember {
|
||||
/// The state-vector kernels built for this device, or
|
||||
/// [`OpenClError::Unsupported`] on a device without binary64.
|
||||
pub fn sv(&self) -> Result<OpenClSv, OpenClError> {
|
||||
self.sv_with_source(crate::quantum_fabric::sv64::CL_SOURCE)
|
||||
}
|
||||
|
||||
/// The same, from another kernel text: for the mutation tests.
|
||||
#[doc(hidden)]
|
||||
pub fn sv_with_source(&self, source: &str) -> Result<OpenClSv, OpenClError> {
|
||||
use crate::quantum_fabric::sv64;
|
||||
if !self.binary64 {
|
||||
return Err(OpenClError::Unsupported("the device has no binary64 (cl_khr_fp64)".into()));
|
||||
}
|
||||
let program = ClProgram::create_and_build_from_source(&self.context, source, sv64::CL_BUILD_OPTIONS).map_err(OpenClError::Build)?;
|
||||
let kernels = SvKernels {
|
||||
diag: ClKernel::create(&program, "sv_diag").map_err(cl)?,
|
||||
one: ClKernel::create(&program, "sv_one").map_err(cl)?,
|
||||
many: ClKernel::create(&program, "sv_many").map_err(cl)?,
|
||||
};
|
||||
Ok(OpenClSv { member: self.clone(), kernels: Arc::new(Mutex::new(kernels)), implementation: sv64::cl_implementation_id_of(source) })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
impl OpenClSv {
|
||||
pub fn implementation(&self) -> &str {
|
||||
&self.implementation
|
||||
}
|
||||
|
||||
/// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses
|
||||
/// them: the real and imaginary parts.
|
||||
pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec<f64>, Vec<f64>), OpenClError> {
|
||||
use crate::quantum_fabric::sv64::{Launch, plan};
|
||||
let launches = plan(n, gates, width).map_err(|_| OpenClError::Internal("a gate the state vector refuses"))?;
|
||||
let max = self.member.max_qubits.saturating_sub(1);
|
||||
if n == 0 || n > u32::from(max) {
|
||||
return Err(OpenClError::TooWide { n_qubits: n.min(255) as u8, max });
|
||||
}
|
||||
let len = 1usize << n;
|
||||
let mut init = vec![0.0f64; len];
|
||||
init[0] = 1.0;
|
||||
let re = self.member.upload(&init)?;
|
||||
init[0] = 0.0;
|
||||
let im = self.member.upload(&init)?;
|
||||
drop(init);
|
||||
let flat = |m: &[crate::linalg::C]| -> Vec<f64> { m.iter().flat_map(|c| [c.re, c.im]).collect() };
|
||||
{
|
||||
let k = self.kernels.lock().unwrap_or_else(|p| p.into_inner());
|
||||
for l in &launches {
|
||||
match l {
|
||||
Launch::Diag { bitpos, diag } => {
|
||||
let bp = self.member.upload(bitpos)?;
|
||||
let dg = self.member.upload(&flat(diag))?;
|
||||
let (count, nk): (cl_ulong, cl_uint) = (len as u64, bitpos.len() as u32);
|
||||
// SAFETY: `re`, `im` hold `len` doubles, `bp` k bit
|
||||
// positions below n, `dg` 2·2^k doubles: the kernel's
|
||||
// arguments, in order.
|
||||
unsafe { ExecuteKernel::new(&k.diag).set_arg(&re).set_arg(&im).set_arg(&count).set_arg(&bp).set_arg(&nk).set_arg(&dg).set_global_work_size(global(count)).enqueue_nd_range(&self.member.queue) }.map_err(cl)?;
|
||||
}
|
||||
Launch::One { s, m } => {
|
||||
let pairs = (len / 2) as cl_ulong;
|
||||
let v = flat(m);
|
||||
let mut launch = ExecuteKernel::new(&k.one);
|
||||
// SAFETY: as above; `s` is a power of two below 2^n.
|
||||
unsafe {
|
||||
launch.set_arg(&re).set_arg(&im).set_arg(&pairs).set_arg(s);
|
||||
for x in &v {
|
||||
launch.set_arg(x);
|
||||
}
|
||||
launch.set_global_work_size(global(pairs)).enqueue_nd_range(&self.member.queue)
|
||||
}
|
||||
.map_err(cl)?;
|
||||
}
|
||||
Launch::Many { sorted, offsets, m } => {
|
||||
let so = self.member.upload(sorted)?;
|
||||
let of = self.member.upload(offsets)?;
|
||||
let mm = self.member.upload(&flat(m))?;
|
||||
let (groups, nk): (cl_ulong, cl_uint) = ((len >> sorted.len()) as u64, sorted.len() as u32);
|
||||
// SAFETY: as above; 2^k is at most the kernel's SV_MAX_D,
|
||||
// which `plan` holds.
|
||||
unsafe { ExecuteKernel::new(&k.many).set_arg(&re).set_arg(&im).set_arg(&groups).set_arg(&so).set_arg(&nk).set_arg(&of).set_arg(&mm).set_global_work_size(global(groups)).enqueue_nd_range(&self.member.queue) }.map_err(cl)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((self.member.download(&re, len)?, self.member.download(&im, len)?))
|
||||
}
|
||||
|
||||
/// The certificate of [`KERNEL_SV_RUN`](crate::quantum_fabric::sv64::KERNEL_SV_RUN)
|
||||
/// on this device over `corpus`, against the CPU's state vector.
|
||||
pub fn certify(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result<Certificate, OpenClError> {
|
||||
use crate::quantum_fabric::sv64::{KERNEL_SV_RUN, state_output};
|
||||
let mut cert = Certification::new();
|
||||
for case in corpus.cases() {
|
||||
let reference = case.reference().map_err(|_| OpenClError::Internal("the corpus holds a gate the state vector refuses"))?;
|
||||
let (re, im) = self.run(case.n, &case.gates, case.width)?;
|
||||
cert.add(&case.bytes(), &reference, &state_output(&re, &im));
|
||||
}
|
||||
Ok(cert.finish(KERNEL_SV_RUN, &self.implementation, &self.member.id, &corpus.generator()))
|
||||
}
|
||||
|
||||
/// The member's entry for the state vector, certified in this call.
|
||||
pub fn entry(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result<KernelEntry, OpenClError> {
|
||||
Ok(KernelEntry { kernel: crate::quantum_fabric::sv64::KERNEL_SV_RUN.into(), implementation: self.implementation.clone(), certificate: self.certify(corpus)?, energy: None })
|
||||
}
|
||||
}
|
||||
|
||||
/// The member on the fabric ([`Fabric`](crate::quantum_fabric::Fabric)): its
|
||||
/// own implementation, through [`OpenClMember::simulate_metered`]. Pair it
|
||||
/// with a [`Member`] whose entries name that implementation, such as
|
||||
/// [`OpenClMember::member`]'s; another implementation is
|
||||
/// [`MemberError::Other`].
|
||||
impl Runner for OpenClMember {
|
||||
fn member_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn run<'r>(&'r self, implementation: &'r str, c: &'r Circuit, start: usize, meter: &'r mut dyn RunMeter) -> RunFuture<'r> {
|
||||
Box::pin(async move {
|
||||
if implementation != self.implementation {
|
||||
return Err(MemberError::Other(format!("member {} runs implementation {}, not {implementation:?}", self.id, self.implementation)));
|
||||
}
|
||||
Ok(self.simulate_with(c, start, meter)?)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn running_out_of_memory_or_resources_is_a_memory_error_and_every_other_a_device_error() {
|
||||
for code in OUT_OF_MEMORY {
|
||||
assert!(matches!(cl(ClError(code)), OpenClError::Memory(_)), "{code}");
|
||||
}
|
||||
assert!(matches!(cl(ClError(-36)), OpenClError::Device(_)));
|
||||
assert!(MemberError::from(OpenClError::Memory("x".into())).falls_back());
|
||||
assert!(MemberError::from(OpenClError::OutOfRange { gate: 3 }).falls_back());
|
||||
assert!(!MemberError::from(OpenClError::Probe { a: 1, b: 2 }).falls_back());
|
||||
assert!(!MemberError::from(OpenClError::Internal("x")).falls_back());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_state_a_member_holds_is_what_one_allocation_holds() {
|
||||
assert_eq!(qubits_for(0), 0);
|
||||
assert_eq!(qubits_for(15), 0);
|
||||
assert_eq!(qubits_for(16), 1);
|
||||
assert_eq!(qubits_for(1536 << 20), MAX_QUBITS.min(27));
|
||||
assert_eq!(qubits_for(u64::MAX), MAX_QUBITS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_global_size_covers_the_work_and_is_capped() {
|
||||
assert_eq!(global(0), 1);
|
||||
assert_eq!(global(7), 7);
|
||||
assert_eq!(global(1 << 40), opencl::GLOBAL_MAX as usize);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,9 @@
|
|||
|
||||
use crate::quantum::{circuit_hash, Amp, BaseGate, Circuit, Gate, QuantumError, StateVector, FRAC};
|
||||
|
||||
pub mod device;
|
||||
pub mod native;
|
||||
|
||||
// ===========================================================================
|
||||
// Algorithm library
|
||||
// ===========================================================================
|
||||
|
|
@ -633,7 +636,7 @@ pub fn to_qasm(c: &Circuit) -> String {
|
|||
/// of the grammar: both register forms (`qubit[n] q;` and `qreg q[n];`), both
|
||||
/// spellings of measurement (`measure q[0] -> c[0];` and the OpenQASM 3 assignment
|
||||
/// `c[0] = measure q[0];`), the Clifford+T names, the rotations `rx`/`ry`/`rz`,
|
||||
/// `sx`/`sxdg`, the universal `u`/`u2`/`u3`, and `p`/`cp`/`crz`.
|
||||
/// `sx`/`sxdg`, the universal `u`/`u2`/`u3`, `ch`, and `p`/`cp`/`crz`.
|
||||
/// `barrier`/`reset`/includes are ignored.
|
||||
///
|
||||
/// # Angles
|
||||
|
|
@ -712,6 +715,7 @@ pub fn from_qasm(src: &str) -> Result<Circuit, String> {
|
|||
"cx" | "cnot" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::X, vec![a], t, 0))?,
|
||||
"cy" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::Y, vec![a], t, 0))?,
|
||||
"cz" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::Z, vec![a], t, 0))?,
|
||||
"ch" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::H, vec![a], t, 0))?,
|
||||
"swap" => require2(&qs).and_then(|(a, b)| {
|
||||
distinct_qubits(&[a], b)?;
|
||||
c.swap(a, b);
|
||||
|
|
@ -792,7 +796,7 @@ pub fn from_qasm(src: &str) -> Result<Circuit, String> {
|
|||
other => {
|
||||
return Err(format!(
|
||||
"unsupported gate: '{other}' (supported: id x y z h s sdg t tdg sx sxdg \
|
||||
rx ry rz p/u1/phase u/u2/u3 cx cy cz cp/cu1 crz swap ccx ccz)"
|
||||
rx ry rz p/u1/phase u/u2/u3 cx cy cz ch cp/cu1 crz swap ccx ccz)"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
539
wai/wai-quantum/src/quantum_toolchain/device.rs
Normal file
539
wai/wai-quantum/src/quantum_toolchain/device.rs
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
//! Device-ready OpenQASM: a circuit in the form a quantum processor's service
|
||||
//! accepts, and the service's counts read back onto this crate's basis.
|
||||
//!
|
||||
//! [`super::to_qasm`] writes the crate's own gate set, which is wider than any
|
||||
//! device's: a gate may carry any number of controls. A service reads only its
|
||||
//! dialect's standard library (OpenQASM 2's `qelib1.inc`, OpenQASM 3's
|
||||
//! `stdgates.inc`), and needs every qubit measured into a classical register.
|
||||
//! [`device_qasm`] lowers the circuit to those standard gates and measures each
|
||||
//! qubit `i` into bit `i` of a register `c`.
|
||||
//!
|
||||
//! # Exact or refused
|
||||
//!
|
||||
//! Every lowering is an identity, not an approximation, and every angle it
|
||||
//! writes is a signed dyadic turn `±2π/2^k`, so the output reads back through
|
||||
//! [`super::from_qasm`] without rounding:
|
||||
//!
|
||||
//! - a controlled `Z`, `S`, `T` or `P` is a controlled phase; with two or more
|
||||
//! controls it splits, by the identity `θ·ab = ½θ·(a + b − (a⊕b))`, into
|
||||
//! controlled phases of half the angle around a multi-controlled `X` on the last
|
||||
//! control;
|
||||
//! - a multi-controlled `X` is a multi-controlled `Z` between two `H`s (two
|
||||
//! controls stay `ccx`);
|
||||
//! - a controlled `Y` is a controlled `X` between `S†` and `S`;
|
||||
//! - a multi-controlled `H` is a multi-controlled `Z` between `ry(−π/4)` and
|
||||
//! `ry(π/4)`, since `Ry(π/4)·Z·Ry(−π/4) = H`.
|
||||
//!
|
||||
//! Each halving costs one bit of angle, so a gate whose lowering would need a turn
|
||||
//! finer than `2π/2^DYADIC_MAX` is refused, as `from_qasm` refuses such an angle.
|
||||
//! The lowered length grows about threefold per control, so a gate with more than
|
||||
//! [`MAX_LOWERED_CONTROLS`] controls is refused too.
|
||||
//!
|
||||
//! The angles written are the ideal ones whatever the circuit's gateset, as in
|
||||
//! [`super::to_qasm`].
|
||||
//!
|
||||
//! # Bit order
|
||||
//!
|
||||
//! Qubit `i` is bit `i` of this crate's basis index, and is measured into
|
||||
//! classical bit `c[i]`. A service that reports a sample as the integer whose bit
|
||||
//! `i` is `c[i]` therefore reports this crate's index unchanged, and one that
|
||||
//! prints the register with `c[0]` last prints it in binary.
|
||||
//! [`counts_from_bitstrings`] and [`counts_from_integers`] read either back.
|
||||
|
||||
use crate::quantum::{BaseGate, Circuit, QuantumError, DYADIC_MAX};
|
||||
|
||||
/// The most controls a gate may carry and still be lowered: each control
|
||||
/// roughly triples the lowered length (a gate with 8 controls lowers to some
|
||||
/// thousands of standard gates).
|
||||
pub const MAX_LOWERED_CONTROLS: usize = 8;
|
||||
|
||||
/// An OpenQASM dialect.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Dialect {
|
||||
/// OpenQASM 2.0 with `qelib1.inc`: phases are `u1` and `cu1`.
|
||||
Qasm2,
|
||||
/// OpenQASM 3.0 with `stdgates.inc`: phases are `p` and `cp`.
|
||||
Qasm3,
|
||||
}
|
||||
|
||||
/// A signed dyadic turn `±2π/2^k`, `1 ≤ k ≤ DYADIC_MAX`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Turn {
|
||||
pub k: u8,
|
||||
pub negative: bool,
|
||||
}
|
||||
|
||||
impl Turn {
|
||||
/// `+2π/2^k`.
|
||||
pub const fn of(k: u8) -> Self {
|
||||
Turn { k, negative: false }
|
||||
}
|
||||
|
||||
/// The turn negated: `∓2π/2^k`.
|
||||
pub const fn neg(self) -> Self {
|
||||
Turn { k: self.k, negative: !self.negative }
|
||||
}
|
||||
|
||||
/// Half the turn, or `None` past `2π/2^DYADIC_MAX`.
|
||||
pub fn half(self) -> Option<Self> {
|
||||
(usize::from(self.k) < DYADIC_MAX).then_some(Turn { k: self.k + 1, negative: self.negative })
|
||||
}
|
||||
|
||||
/// The angle as OpenQASM writes it: `2π/2^k = π/2^(k−1)`, so `pi`, `pi/2`,
|
||||
/// `-pi/4`.
|
||||
pub fn qasm(self) -> String {
|
||||
let sign = if self.negative { "-" } else { "" };
|
||||
if self.k == 1 {
|
||||
format!("{sign}pi")
|
||||
} else {
|
||||
format!("{sign}pi/{}", 1u64 << (self.k - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A gate both standard libraries define, on qubit indices.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum StdGate {
|
||||
X(u8),
|
||||
Y(u8),
|
||||
Z(u8),
|
||||
H(u8),
|
||||
S(u8),
|
||||
Sdg(u8),
|
||||
T(u8),
|
||||
Tdg(u8),
|
||||
/// `p(θ)` (OpenQASM 2: `u1`).
|
||||
P(u8, Turn),
|
||||
/// `ry(θ)`.
|
||||
Ry(u8, Turn),
|
||||
/// Control, target.
|
||||
Cx(u8, u8),
|
||||
Cy(u8, u8),
|
||||
Cz(u8, u8),
|
||||
Ch(u8, u8),
|
||||
/// `cp(θ)` (OpenQASM 2: `cu1`): control, target.
|
||||
Cp(u8, u8, Turn),
|
||||
/// Two controls, target.
|
||||
Ccx(u8, u8, u8),
|
||||
}
|
||||
|
||||
impl StdGate {
|
||||
/// The gate as one OpenQASM statement in `d`.
|
||||
pub fn qasm(&self, d: Dialect) -> String {
|
||||
let (p, cp) = match d {
|
||||
Dialect::Qasm2 => ("u1", "cu1"),
|
||||
Dialect::Qasm3 => ("p", "cp"),
|
||||
};
|
||||
match *self {
|
||||
StdGate::X(q) => format!("x q[{q}];"),
|
||||
StdGate::Y(q) => format!("y q[{q}];"),
|
||||
StdGate::Z(q) => format!("z q[{q}];"),
|
||||
StdGate::H(q) => format!("h q[{q}];"),
|
||||
StdGate::S(q) => format!("s q[{q}];"),
|
||||
StdGate::Sdg(q) => format!("sdg q[{q}];"),
|
||||
StdGate::T(q) => format!("t q[{q}];"),
|
||||
StdGate::Tdg(q) => format!("tdg q[{q}];"),
|
||||
StdGate::P(q, t) => format!("{p}({}) q[{q}];", t.qasm()),
|
||||
StdGate::Ry(q, t) => format!("ry({}) q[{q}];", t.qasm()),
|
||||
StdGate::Cx(c, t) => format!("cx q[{c}],q[{t}];"),
|
||||
StdGate::Cy(c, t) => format!("cy q[{c}],q[{t}];"),
|
||||
StdGate::Cz(c, t) => format!("cz q[{c}],q[{t}];"),
|
||||
StdGate::Ch(c, t) => format!("ch q[{c}],q[{t}];"),
|
||||
StdGate::Cp(c, t, a) => format!("{cp}({}) q[{c}],q[{t}];", a.qasm()),
|
||||
StdGate::Ccx(a, b, t) => format!("ccx q[{a}],q[{b}],q[{t}];"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a circuit cannot be exported, or counts cannot be read back.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum DeviceError {
|
||||
/// The circuit fails [`Circuit::validate`].
|
||||
Invalid(QuantumError),
|
||||
/// Op `op` has more than [`MAX_LOWERED_CONTROLS`] controls.
|
||||
TooManyControls { op: usize, controls: usize },
|
||||
/// Lowering op `op` would need a turn finer than `2π/2^DYADIC_MAX`.
|
||||
TooFine { op: usize },
|
||||
/// A reported outcome is not a basis state of the register.
|
||||
Outcome(String),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for DeviceError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
DeviceError::Invalid(e) => write!(f, "invalid circuit: {e:?}"),
|
||||
DeviceError::TooManyControls { op, controls } => {
|
||||
write!(f, "op {op} has {controls} controls; at most {MAX_LOWERED_CONTROLS} are lowered")
|
||||
}
|
||||
DeviceError::TooFine { op } => {
|
||||
write!(f, "op {op} would need a turn finer than 2π/2^{DYADIC_MAX} to lower exactly")
|
||||
}
|
||||
DeviceError::Outcome(s) => write!(f, "outcome {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DeviceError {}
|
||||
|
||||
/// Lower a circuit to gates both standard libraries define. Exact: see the
|
||||
/// module documentation.
|
||||
pub fn lower(c: &Circuit) -> Result<Vec<StdGate>, DeviceError> {
|
||||
c.validate().map_err(DeviceError::Invalid)?;
|
||||
let mut out = Vec::new();
|
||||
for (op, g) in c.ops.iter().enumerate() {
|
||||
if g.controls.len() > MAX_LOWERED_CONTROLS {
|
||||
return Err(DeviceError::TooManyControls { op, controls: g.controls.len() });
|
||||
}
|
||||
lower_gate(&mut out, g.base, g.param, &g.controls, g.target).ok_or(DeviceError::TooFine { op })?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The phase a diagonal base gate applies to `|1⟩`, as a turn.
|
||||
fn phase_of(base: BaseGate, param: u16) -> Option<Turn> {
|
||||
match base {
|
||||
BaseGate::Z => Some(Turn::of(1)),
|
||||
BaseGate::S => Some(Turn::of(2)),
|
||||
BaseGate::Sdg => Some(Turn::of(2).neg()),
|
||||
BaseGate::T => Some(Turn::of(3)),
|
||||
BaseGate::Tdg => Some(Turn::of(3).neg()),
|
||||
// `validate` holds `param` to `1..=DYADIC_MAX`, which fits a `u8`.
|
||||
BaseGate::P => Some(Turn::of(param as u8)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_gate(out: &mut Vec<StdGate>, base: BaseGate, param: u16, cs: &[u8], t: u8) -> Option<()> {
|
||||
match (base, cs) {
|
||||
(BaseGate::I, _) => {}
|
||||
(BaseGate::X, []) => out.push(StdGate::X(t)),
|
||||
(BaseGate::Y, []) => out.push(StdGate::Y(t)),
|
||||
(BaseGate::Z, []) => out.push(StdGate::Z(t)),
|
||||
(BaseGate::H, []) => out.push(StdGate::H(t)),
|
||||
(BaseGate::S, []) => out.push(StdGate::S(t)),
|
||||
(BaseGate::Sdg, []) => out.push(StdGate::Sdg(t)),
|
||||
(BaseGate::T, []) => out.push(StdGate::T(t)),
|
||||
(BaseGate::Tdg, []) => out.push(StdGate::Tdg(t)),
|
||||
(BaseGate::P, []) => out.push(StdGate::P(t, Turn::of(param as u8))),
|
||||
(BaseGate::X, _) => mcx(out, cs, t)?,
|
||||
(BaseGate::Y, _) => {
|
||||
out.push(StdGate::Sdg(t));
|
||||
mcx(out, cs, t)?;
|
||||
out.push(StdGate::S(t));
|
||||
}
|
||||
(BaseGate::Z, [c]) => out.push(StdGate::Cz(*c, t)),
|
||||
(BaseGate::Z, [_, _]) => {
|
||||
out.push(StdGate::H(t));
|
||||
mcx(out, cs, t)?;
|
||||
out.push(StdGate::H(t));
|
||||
}
|
||||
(BaseGate::H, [c]) => out.push(StdGate::Ch(*c, t)),
|
||||
(BaseGate::H, _) => {
|
||||
out.push(StdGate::Ry(t, Turn::of(3).neg()));
|
||||
mcphase(out, cs, t, Turn::of(1))?;
|
||||
out.push(StdGate::Ry(t, Turn::of(3)));
|
||||
}
|
||||
(base, _) => mcphase(out, cs, t, phase_of(base, param)?)?,
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// `X` on `t` when every qubit in `cs` (at least one) is set.
|
||||
fn mcx(out: &mut Vec<StdGate>, cs: &[u8], t: u8) -> Option<()> {
|
||||
match cs {
|
||||
[] => out.push(StdGate::X(t)),
|
||||
[c] => out.push(StdGate::Cx(*c, t)),
|
||||
[a, b] => out.push(StdGate::Ccx(*a, *b, t)),
|
||||
_ => {
|
||||
out.push(StdGate::H(t));
|
||||
mcphase(out, cs, t, Turn::of(1))?;
|
||||
out.push(StdGate::H(t));
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The phase `turn` on `|1⟩` of `t` when every qubit in `cs` is set: for
|
||||
/// `cs = rest ++ [last]`, `θ·r·l = ½θ·(l + r − (r⊕l))` with `r` the conjunction
|
||||
/// of `rest`, the `r⊕l` written by a multi-controlled `X` onto `last`.
|
||||
fn mcphase(out: &mut Vec<StdGate>, cs: &[u8], t: u8, turn: Turn) -> Option<()> {
|
||||
match cs {
|
||||
[] => out.push(StdGate::P(t, turn)),
|
||||
[c] => out.push(StdGate::Cp(*c, t, turn)),
|
||||
[rest @ .., last] => {
|
||||
let half = turn.half()?;
|
||||
out.push(StdGate::Cp(*last, t, half));
|
||||
mcx(out, rest, *last)?;
|
||||
out.push(StdGate::Cp(*last, t, half.neg()));
|
||||
mcx(out, rest, *last)?;
|
||||
mcphase(out, rest, t, half)?;
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The circuit as OpenQASM a device service accepts: `d`'s header and standard
|
||||
/// library, a quantum register `q` and a classical register `c` of the circuit's
|
||||
/// width, the lowered gates, and qubit `i` measured into `c[i]`.
|
||||
pub fn device_qasm(c: &Circuit, d: Dialect) -> Result<String, DeviceError> {
|
||||
let gates = lower(c)?;
|
||||
let n = c.n_qubits;
|
||||
let mut s = match d {
|
||||
Dialect::Qasm2 => format!("OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{n}];\ncreg c[{n}];\n"),
|
||||
Dialect::Qasm3 => format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{n}] c;\n"),
|
||||
};
|
||||
for g in &gates {
|
||||
s.push_str(&g.qasm(d));
|
||||
s.push('\n');
|
||||
}
|
||||
for i in 0..n {
|
||||
s.push_str(&match d {
|
||||
Dialect::Qasm2 => format!("measure q[{i}] -> c[{i}];\n"),
|
||||
Dialect::Qasm3 => format!("c[{i}] = measure q[{i}];\n"),
|
||||
});
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// How a service prints a measured register.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum BitOrder {
|
||||
/// `c[n−1] … c[0]`: the register in binary, `c[0]` last.
|
||||
Bit0Last,
|
||||
/// `c[0] … c[n−1]`: `c[0]` first.
|
||||
Bit0First,
|
||||
}
|
||||
|
||||
/// Counts over this crate's basis index (`2^n` entries) from a service's counts
|
||||
/// keyed by printed register, each exactly `n` characters of `0` and `1`.
|
||||
/// A repeated outcome adds.
|
||||
pub fn counts_from_bitstrings<'a>(
|
||||
n: u8,
|
||||
counts: impl IntoIterator<Item = (&'a str, u64)>,
|
||||
order: BitOrder,
|
||||
) -> Result<Vec<u64>, DeviceError> {
|
||||
let width = register_width(n)?;
|
||||
let mut out = vec![0u64; 1 << width];
|
||||
for (s, k) in counts {
|
||||
if s.len() != width || !s.bytes().all(|b| b == b'0' || b == b'1') {
|
||||
return Err(DeviceError::Outcome(format!("{s:?} is not {width} binary digits")));
|
||||
}
|
||||
let mut x = 0usize;
|
||||
for (pos, b) in s.bytes().enumerate() {
|
||||
let bit = match order {
|
||||
BitOrder::Bit0Last => width - 1 - pos,
|
||||
BitOrder::Bit0First => pos,
|
||||
};
|
||||
x |= usize::from(b - b'0') << bit;
|
||||
}
|
||||
add(&mut out, x, k)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Counts over this crate's basis index from a service's counts keyed by the
|
||||
/// integer whose bit `i` is `c[i]`, which is this crate's index itself.
|
||||
pub fn counts_from_integers(n: u8, counts: impl IntoIterator<Item = (u64, u64)>) -> Result<Vec<u64>, DeviceError> {
|
||||
let width = register_width(n)?;
|
||||
let mut out = vec![0u64; 1 << width];
|
||||
for (v, k) in counts {
|
||||
let x = usize::try_from(v)
|
||||
.ok()
|
||||
.filter(|&x| x < out.len())
|
||||
.ok_or_else(|| DeviceError::Outcome(format!("{v} is outside a {width}-bit register")))?;
|
||||
add(&mut out, x, k)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn register_width(n: u8) -> Result<usize, DeviceError> {
|
||||
let max = crate::quantum::MAX_QUBITS;
|
||||
if n == 0 || n > max {
|
||||
return Err(DeviceError::Outcome(format!("a {n}-qubit register; counts are read for 1..={max}")));
|
||||
}
|
||||
Ok(usize::from(n))
|
||||
}
|
||||
|
||||
fn add(out: &mut [u64], x: usize, k: u64) -> Result<(), DeviceError> {
|
||||
out[x] = out[x].checked_add(k).ok_or_else(|| DeviceError::Outcome(format!("the count of {x} overflows")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::quantum::{Gate, ONE};
|
||||
use crate::quantum_toolchain::from_qasm;
|
||||
|
||||
/// A circuit whose state, read back from `text`, matches `c`'s up to global
|
||||
/// phase, at the fixed-point simulator's resolution.
|
||||
fn same_state(c: &Circuit, text: &str) {
|
||||
let back = from_qasm(text).unwrap_or_else(|e| panic!("reparse: {e}\n{text}"));
|
||||
let f = back.simulate().unwrap().fidelity_fx(&c.simulate().unwrap());
|
||||
assert!((f - ONE).abs() < 1 << 12, "fidelity {f}, want {ONE}\n{text}");
|
||||
}
|
||||
|
||||
fn one(base: BaseGate, controls: &[u8], target: u8, param: u16) -> Gate {
|
||||
Gate { base, controls: controls.to_vec(), target, param }
|
||||
}
|
||||
|
||||
/// Superpose every qubit first, with distinct phases, so a wrong phase or a
|
||||
/// wrong control condition anywhere shows in the state.
|
||||
fn prepared(n: u8) -> Circuit {
|
||||
let mut c = Circuit::new(n);
|
||||
for q in 0..n {
|
||||
c.ops.push(one(BaseGate::H, &[], q, 0));
|
||||
c.ops.push(one(BaseGate::P, &[], q, u16::from(q) + 2));
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_base_gate_under_zero_to_four_controls_lowers_exactly() {
|
||||
let bases = [
|
||||
(BaseGate::X, 0),
|
||||
(BaseGate::Y, 0),
|
||||
(BaseGate::Z, 0),
|
||||
(BaseGate::H, 0),
|
||||
(BaseGate::S, 0),
|
||||
(BaseGate::Sdg, 0),
|
||||
(BaseGate::T, 0),
|
||||
(BaseGate::Tdg, 0),
|
||||
(BaseGate::P, 1),
|
||||
(BaseGate::P, 4),
|
||||
(BaseGate::P, 9),
|
||||
];
|
||||
for (base, param) in bases {
|
||||
for k in 0..=4u8 {
|
||||
// Controls out of order and on both sides of the target.
|
||||
let controls: Vec<u8> = [4u8, 0, 3, 1].iter().copied().take(usize::from(k)).collect();
|
||||
let mut c = prepared(5);
|
||||
c.ops.push(one(base, &controls, 2, param));
|
||||
for d in [Dialect::Qasm2, Dialect::Qasm3] {
|
||||
let text = device_qasm(&c, d).unwrap();
|
||||
assert!(!text.contains("ccz") && !text.contains("unmapped"), "{text}");
|
||||
same_state(&c, &text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_circuits_round_trip_in_both_dialects() {
|
||||
let mut s = 0x5eed_u64;
|
||||
let mut next = || {
|
||||
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
(s >> 33) as u32
|
||||
};
|
||||
let bases = [BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::T, BaseGate::Tdg, BaseGate::P];
|
||||
for _ in 0..24 {
|
||||
let n = 6u8;
|
||||
let mut c = prepared(n);
|
||||
for _ in 0..14 {
|
||||
let target = (next() % u32::from(n)) as u8;
|
||||
let mut controls = Vec::new();
|
||||
for _ in 0..next() % 4 {
|
||||
let q = (next() % u32::from(n)) as u8;
|
||||
if q != target && !controls.contains(&q) {
|
||||
controls.push(q);
|
||||
}
|
||||
}
|
||||
let base = bases[(next() as usize) % bases.len()];
|
||||
let param = if base == BaseGate::P { 1 + (next() % 12) as u16 } else { 0 };
|
||||
c.ops.push(one(base, &controls, target, param));
|
||||
}
|
||||
for d in [Dialect::Qasm2, Dialect::Qasm3] {
|
||||
same_state(&c, &device_qasm(&c, d).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_output_names_only_its_dialects_standard_gates_and_measures_every_qubit() {
|
||||
let mut c = prepared(4);
|
||||
c.ops.push(one(BaseGate::Z, &[0, 1, 3], 2, 0));
|
||||
c.ops.push(one(BaseGate::H, &[0, 1], 3, 0));
|
||||
c.ops.push(one(BaseGate::P, &[2], 0, 5));
|
||||
let v2: &[&str] = &["x", "y", "z", "h", "s", "sdg", "t", "tdg", "u1", "ry", "cx", "cy", "cz", "ch", "cu1", "ccx"];
|
||||
let v3: &[&str] = &["x", "y", "z", "h", "s", "sdg", "t", "tdg", "p", "ry", "cx", "cy", "cz", "ch", "cp", "ccx"];
|
||||
for (d, names, header) in [(Dialect::Qasm2, v2, "OPENQASM 2.0;"), (Dialect::Qasm3, v3, "OPENQASM 3.0;")] {
|
||||
let text = device_qasm(&c, d).unwrap();
|
||||
let mut lines = text.lines();
|
||||
assert_eq!(lines.next(), Some(header));
|
||||
let mut measured = Vec::new();
|
||||
for line in lines.skip(3) {
|
||||
if line.contains("measure") {
|
||||
measured.push(line.to_string());
|
||||
continue;
|
||||
}
|
||||
let name: String = line.chars().take_while(|c| c.is_ascii_alphanumeric()).collect();
|
||||
assert!(names.contains(&name.as_str()), "{d:?}: {line}");
|
||||
}
|
||||
let want: Vec<String> = (0..4)
|
||||
.map(|i| match d {
|
||||
Dialect::Qasm2 => format!("measure q[{i}] -> c[{i}];"),
|
||||
Dialect::Qasm3 => format!("c[{i}] = measure q[{i}];"),
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(measured, want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turns_print_as_multiples_of_pi() {
|
||||
assert_eq!(Turn::of(1).qasm(), "pi");
|
||||
assert_eq!(Turn::of(2).qasm(), "pi/2");
|
||||
assert_eq!(Turn::of(3).neg().qasm(), "-pi/4");
|
||||
assert_eq!(Turn::of(32).qasm(), "pi/2147483648");
|
||||
assert_eq!(Turn::of(32).half(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_cannot_be_lowered_exactly_is_refused() {
|
||||
// Two controls halve the turn once: P(32) has no half in the table.
|
||||
let mut c = Circuit::new(3);
|
||||
c.ops.push(one(BaseGate::P, &[0, 1], 2, 32));
|
||||
assert_eq!(lower(&c), Err(DeviceError::TooFine { op: 0 }));
|
||||
// One control needs no halving.
|
||||
let mut c = Circuit::new(3);
|
||||
c.ops.push(one(BaseGate::P, &[0], 2, 32));
|
||||
assert!(lower(&c).is_ok());
|
||||
let mut c = Circuit::new(10);
|
||||
c.ops.push(one(BaseGate::X, &[0, 1, 2, 3, 4, 5, 6, 7, 8], 9, 0));
|
||||
assert_eq!(lower(&c), Err(DeviceError::TooManyControls { op: 0, controls: 9 }));
|
||||
let mut c = Circuit::new(2);
|
||||
c.ops.push(one(BaseGate::P, &[], 0, 0));
|
||||
assert!(matches!(lower(&c), Err(DeviceError::Invalid(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bell_pair_lowers_to_the_textbook_program() {
|
||||
let text = device_qasm(&crate::quantum_toolchain::bell(), Dialect::Qasm2).unwrap();
|
||||
assert_eq!(
|
||||
text,
|
||||
"OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q[0] -> c[0];\nmeasure q[1] -> c[1];\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_read_back_onto_the_crates_index() {
|
||||
// X on qubit 0 only: index 1 in this crate; a service prints `c[0]` last as
|
||||
// "001" and reports the integer 1.
|
||||
let mut c = Circuit::new(3);
|
||||
c.ops.push(one(BaseGate::X, &[], 0, 0));
|
||||
let shots = c.simulate().unwrap().sample_shots(1, 13);
|
||||
assert_eq!(shots[1], 13);
|
||||
let a = counts_from_bitstrings(3, [("001", 10), ("110", 2), ("001", 1)], BitOrder::Bit0Last).unwrap();
|
||||
let b = counts_from_bitstrings(3, [("100", 10), ("011", 2), ("100", 1)], BitOrder::Bit0First).unwrap();
|
||||
let i = counts_from_integers(3, [(1, 11), (6, 2)]).unwrap();
|
||||
assert_eq!(a, vec![0, 11, 0, 0, 0, 0, 2, 0]);
|
||||
assert_eq!(a, b);
|
||||
assert_eq!(a, i);
|
||||
assert!(counts_from_bitstrings(3, [("01", 1)], BitOrder::Bit0Last).is_err());
|
||||
assert!(counts_from_bitstrings(3, [("0 01", 1)], BitOrder::Bit0Last).is_err());
|
||||
assert!(counts_from_bitstrings(2, [("12", 1)], BitOrder::Bit0Last).is_err());
|
||||
assert!(counts_from_integers(3, [(8, 1)]).is_err());
|
||||
assert!(counts_from_integers(3, [(1, u64::MAX), (1, 1)]).is_err());
|
||||
assert!(counts_from_integers(0, []).is_err());
|
||||
}
|
||||
}
|
||||
890
wai/wai-quantum/src/quantum_toolchain/native.rs
Normal file
890
wai/wai-quantum/src/quantum_toolchain/native.rs
Normal file
|
|
@ -0,0 +1,890 @@
|
|||
//! Native programs: a circuit in a processor's own gates, on its own qubits.
|
||||
//!
|
||||
//! A cloud processor runs only its basis gates, and a two-qubit gate only on a
|
||||
//! pair its coupling map connects. [`compile`] takes a circuit there in four
|
||||
//! exact steps:
|
||||
//!
|
||||
//! 1. [`super::device::lower`] to the standard gates;
|
||||
//! 2. each standard gate to `rz`, `sx`, `x` and `cz` by textbook identities
|
||||
//! (`H = Rz(π/2)·SX·Rz(π/2)`, `CX = H·CZ·H`, the controlled phase through two
|
||||
//! `CX` and half-angle phases, the Toffoli through seven `T`s), every angle a
|
||||
//! dyadic multiple of `2π`;
|
||||
//! 3. onto physical qubits: an initial layout, then for each `cz` on a pair the
|
||||
//! map does not connect, `SWAP`s along a shortest path;
|
||||
//! 4. into the [`Basis`]: consecutive `rz` merged, or, for phased-X processors,
|
||||
//! every `rz` carried forward into the phase of the next rotation.
|
||||
//!
|
||||
//! Single-qubit gates are replaced up to a global phase, which no measurement
|
||||
//! sees. A phased-X program leaves each qubit's carried `Z` rotation unapplied at
|
||||
//! the end ([`NativeProgram::residual`]): a `Z` rotation just before a
|
||||
//! computational-basis measurement changes no outcome.
|
||||
//!
|
||||
//! # Layout
|
||||
//!
|
||||
//! Each physical qubit in turn seeds a breadth-first layout of the circuit's
|
||||
//! qubits over the coupling map; the layout whose routing needs the fewest `SWAP`s
|
||||
//! wins, the lowest seed breaking ties. Deterministic: the same circuit and map
|
||||
//! give the same program on every machine.
|
||||
//!
|
||||
//! # Verification
|
||||
//!
|
||||
//! [`NativeProgram::verify`] checks the program against its circuit before it is
|
||||
//! sent anywhere: every `cz` on a connected pair, and the program's final state
|
||||
//! (with the residual rotations applied) equal to the circuit's, with each logical
|
||||
//! qubit on the physical qubit it ends on, by this crate's own simulator.
|
||||
|
||||
use super::device::{lower, DeviceError, StdGate, Turn};
|
||||
use crate::quantum::{BaseGate, Circuit, Gate, DYADIC_MAX, MAX_QUBITS, ONE};
|
||||
|
||||
/// The angle `2π·m/2^k`, reduced: `m < 2^k`, and `m` odd unless the angle is
|
||||
/// zero (`m = k = 0`). `k ≤ DYADIC_MAX`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Dyadic {
|
||||
m: u64,
|
||||
k: u8,
|
||||
}
|
||||
|
||||
impl Dyadic {
|
||||
pub const ZERO: Dyadic = Dyadic { m: 0, k: 0 };
|
||||
|
||||
/// `2π·m/2^k` reduced modulo `2π`, or `None` past `k = DYADIC_MAX`.
|
||||
pub fn new(m: u64, k: u8) -> Option<Self> {
|
||||
(usize::from(k) <= DYADIC_MAX).then(|| Dyadic::reduce(m, k))
|
||||
}
|
||||
|
||||
fn reduce(m: u64, k: u8) -> Self {
|
||||
let (mut m, mut k) = (m & ((1u64 << k) - 1), k);
|
||||
if m == 0 {
|
||||
return Dyadic::ZERO;
|
||||
}
|
||||
while m & 1 == 0 {
|
||||
m >>= 1;
|
||||
k -= 1;
|
||||
}
|
||||
Dyadic { m, k }
|
||||
}
|
||||
|
||||
/// The angle as `(m, k)`: `m/2^k` of a full turn, which a service taking
|
||||
/// turns reads exactly.
|
||||
pub fn turns(self) -> (u64, u8) {
|
||||
(self.m, self.k)
|
||||
}
|
||||
|
||||
pub fn is_zero(self) -> bool {
|
||||
self.m == 0
|
||||
}
|
||||
|
||||
/// Half this representative in `[0, 2π)`, or `None` past `DYADIC_MAX`.
|
||||
pub fn half(self) -> Option<Dyadic> {
|
||||
if self.m == 0 {
|
||||
return Some(self);
|
||||
}
|
||||
(usize::from(self.k) < DYADIC_MAX).then_some(Dyadic { m: self.m, k: self.k + 1 })
|
||||
}
|
||||
|
||||
/// The angle in `(−π, π]` as OpenQASM writes it: `0`, `pi`, `pi/4`,
|
||||
/// `-3*pi/8`.
|
||||
pub fn qasm(self) -> String {
|
||||
match self.k {
|
||||
0 => "0".into(),
|
||||
1 => "pi".into(),
|
||||
k => {
|
||||
let full = 1u64 << k;
|
||||
let (sign, m) = if self.m > full / 2 { ("-", full - self.m) } else { ("", self.m) };
|
||||
let den = 1u64 << (k - 1);
|
||||
if m == 1 {
|
||||
format!("{sign}pi/{den}")
|
||||
} else {
|
||||
format!("{sign}{m}*pi/{den}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add for Dyadic {
|
||||
type Output = Dyadic;
|
||||
fn add(self, o: Dyadic) -> Dyadic {
|
||||
let k = self.k.max(o.k);
|
||||
Dyadic::reduce((self.m << (k - self.k)) + (o.m << (k - o.k)), k)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Neg for Dyadic {
|
||||
type Output = Dyadic;
|
||||
fn neg(self) -> Dyadic {
|
||||
if self.m == 0 {
|
||||
self
|
||||
} else {
|
||||
Dyadic { m: (1u64 << self.k) - self.m, k: self.k }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Turn> for Dyadic {
|
||||
fn from(t: Turn) -> Dyadic {
|
||||
let d = Dyadic::reduce(1, t.k);
|
||||
if t.negative {
|
||||
-d
|
||||
} else {
|
||||
d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A processor's gate basis.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Basis {
|
||||
/// `rz(θ)`, `sx`, `x` and `cz`.
|
||||
RzSxXCz,
|
||||
/// `prx(θ, φ) = Rz(φ)·Rx(θ)·Rz(−φ)` and `cz`, the `Z` rotations carried in
|
||||
/// the phases.
|
||||
PrxCz,
|
||||
/// `gpi(φ)`, `gpi2(φ)` and `zz(θ)`, with angles in turns: `gpi2(φ)` is
|
||||
/// `prx(π/2, φ)`, `gpi(φ)` is `prx(π, φ)` up to a global phase, and
|
||||
/// `zz(θ) = exp(−iπθ·Z⊗Z)`. A `cz` is `zz(¼)` with a `Z` rotation of `−π/2`
|
||||
/// on each qubit, carried in the phases like every other.
|
||||
GpiZz,
|
||||
}
|
||||
|
||||
/// A native gate on physical qubits.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum NativeGate {
|
||||
Rz(u16, Dyadic),
|
||||
Sx(u16),
|
||||
X(u16),
|
||||
/// `prx(θ, φ)`.
|
||||
Prx(u16, Dyadic, Dyadic),
|
||||
Cz(u16, u16),
|
||||
/// `gpi(φ)`, the phase a [`Dyadic`] read as turns.
|
||||
Gpi(u16, Dyadic),
|
||||
/// `gpi2(φ)`.
|
||||
Gpi2(u16, Dyadic),
|
||||
/// `zz(θ)` on a pair, the angle in turns.
|
||||
Zz(u16, u16, Dyadic),
|
||||
}
|
||||
|
||||
/// A processor's connectivity: which physical pairs a two-qubit gate may act
|
||||
/// on, in either order.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Coupling {
|
||||
adj: Vec<Vec<u16>>,
|
||||
}
|
||||
|
||||
impl Coupling {
|
||||
/// `n` physical qubits and the pairs connected. A pair may be listed in
|
||||
/// either order or both; a qubit paired with itself or outside `0..n` is
|
||||
/// refused.
|
||||
pub fn new(n: u16, pairs: &[(u16, u16)]) -> Result<Self, NativeError> {
|
||||
let mut adj = vec![Vec::new(); usize::from(n)];
|
||||
for &(a, b) in pairs {
|
||||
if a == b || a >= n || b >= n {
|
||||
return Err(NativeError::Coupling(format!("pair ({a}, {b}) on {n} qubits")));
|
||||
}
|
||||
adj[usize::from(a)].push(b);
|
||||
adj[usize::from(b)].push(a);
|
||||
}
|
||||
for list in &mut adj {
|
||||
list.sort_unstable();
|
||||
list.dedup();
|
||||
}
|
||||
Ok(Coupling { adj })
|
||||
}
|
||||
|
||||
/// `n` qubits in a line.
|
||||
pub fn line(n: u16) -> Self {
|
||||
let pairs: Vec<(u16, u16)> = (1..n).map(|i| (i - 1, i)).collect();
|
||||
Coupling::new(n, &pairs).expect("a line is a valid map")
|
||||
}
|
||||
|
||||
/// A `rows × cols` grid, row-major.
|
||||
pub fn grid(rows: u16, cols: u16) -> Self {
|
||||
let mut pairs = Vec::new();
|
||||
for r in 0..rows {
|
||||
for c in 0..cols {
|
||||
let q = r * cols + c;
|
||||
if c + 1 < cols {
|
||||
pairs.push((q, q + 1));
|
||||
}
|
||||
if r + 1 < rows {
|
||||
pairs.push((q, q + cols));
|
||||
}
|
||||
}
|
||||
}
|
||||
Coupling::new(rows * cols, &pairs).expect("a grid is a valid map")
|
||||
}
|
||||
|
||||
pub fn qubits(&self) -> u16 {
|
||||
self.adj.len() as u16
|
||||
}
|
||||
|
||||
pub fn connected(&self, a: u16, b: u16) -> bool {
|
||||
self.adj.get(usize::from(a)).is_some_and(|l| l.binary_search(&b).is_ok())
|
||||
}
|
||||
|
||||
/// Breadth-first order from `s`, neighbours in index order.
|
||||
fn bfs(&self, s: u16) -> Vec<u16> {
|
||||
let mut seen = vec![false; self.adj.len()];
|
||||
let mut order = vec![s];
|
||||
seen[usize::from(s)] = true;
|
||||
let mut i = 0;
|
||||
while i < order.len() {
|
||||
for &v in &self.adj[usize::from(order[i])] {
|
||||
if !seen[usize::from(v)] {
|
||||
seen[usize::from(v)] = true;
|
||||
order.push(v);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
order
|
||||
}
|
||||
|
||||
/// For each target `b`, each qubit's next step towards `b` on a shortest
|
||||
/// path, and its distance; `u16::MAX` where `b` is unreachable.
|
||||
fn paths(&self) -> (Vec<Vec<u16>>, Vec<Vec<u16>>) {
|
||||
let n = self.adj.len();
|
||||
let mut next = vec![vec![u16::MAX; n]; n];
|
||||
let mut dist = vec![vec![u16::MAX; n]; n];
|
||||
for b in 0..n {
|
||||
dist[b][b] = 0;
|
||||
next[b][b] = b as u16;
|
||||
let mut queue = vec![b as u16];
|
||||
let mut i = 0;
|
||||
while i < queue.len() {
|
||||
let u = queue[i];
|
||||
for &v in &self.adj[usize::from(u)] {
|
||||
if dist[b][usize::from(v)] == u16::MAX {
|
||||
dist[b][usize::from(v)] = dist[b][usize::from(u)] + 1;
|
||||
next[b][usize::from(v)] = u;
|
||||
queue.push(v);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
(next, dist)
|
||||
}
|
||||
}
|
||||
|
||||
/// A circuit compiled for a processor.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NativeProgram {
|
||||
pub basis: Basis,
|
||||
/// The circuit's width.
|
||||
pub n_qubits: u8,
|
||||
/// Logical qubit `i` starts on physical qubit `layout[i]`.
|
||||
pub layout: Vec<u16>,
|
||||
/// Logical qubit `i` ends, and is measured into `c[i]`, on
|
||||
/// `final_layout[i]`.
|
||||
pub final_layout: Vec<u16>,
|
||||
pub gates: Vec<NativeGate>,
|
||||
/// The `Z` rotation still owed on each physical qubit when the gates end,
|
||||
/// as `(qubit, angle)`: none for [`Basis::RzSxXCz`]. A `Z` rotation just
|
||||
/// before a computational-basis measurement changes no outcome, so it is
|
||||
/// never sent.
|
||||
pub residual: Vec<(u16, Dyadic)>,
|
||||
/// The `SWAP`s routing inserted.
|
||||
pub swaps: usize,
|
||||
}
|
||||
|
||||
/// Why a circuit cannot be compiled for a processor, or a program fails its
|
||||
/// verification.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum NativeError {
|
||||
Device(DeviceError),
|
||||
Coupling(String),
|
||||
/// No connected part of the map has as many qubits as the circuit.
|
||||
TooFewQubits { need: u8, largest: usize },
|
||||
/// A controlled phase would need a turn finer than `2π/2^DYADIC_MAX`.
|
||||
TooFine,
|
||||
/// Gate `gate` is not in the program's basis, or not on a connected pair.
|
||||
NotNative { gate: usize },
|
||||
/// The program touches more qubits than this crate simulates.
|
||||
TooWideToVerify { qubits: usize },
|
||||
/// The program's state is not the circuit's: the fixed-point fidelity.
|
||||
Mismatch { fidelity: i64 },
|
||||
}
|
||||
|
||||
impl core::fmt::Display for NativeError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
NativeError::Device(e) => write!(f, "{e}"),
|
||||
NativeError::Coupling(s) => write!(f, "coupling map: {s}"),
|
||||
NativeError::TooFewQubits { need, largest } => {
|
||||
write!(f, "the circuit needs {need} connected qubits; the largest connected part has {largest}")
|
||||
}
|
||||
NativeError::TooFine => write!(f, "a controlled phase would need a turn finer than 2π/2^{DYADIC_MAX}"),
|
||||
NativeError::NotNative { gate } => write!(f, "gate {gate} is not native to the program's processor"),
|
||||
NativeError::TooWideToVerify { qubits } => {
|
||||
write!(f, "the program touches {qubits} qubits; at most {MAX_QUBITS} are simulated")
|
||||
}
|
||||
NativeError::Mismatch { fidelity } => write!(f, "the program's state differs from the circuit's (fidelity {fidelity} of {ONE})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NativeError {}
|
||||
|
||||
impl From<DeviceError> for NativeError {
|
||||
fn from(e: DeviceError) -> Self {
|
||||
NativeError::Device(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// An operation in the `rz`/`sx`/`x`/`cz` set on qubit indices: logical before
|
||||
/// routing, physical after.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Op {
|
||||
Rz(u16, Dyadic),
|
||||
Sx(u16),
|
||||
X(u16),
|
||||
Cz(u16, u16),
|
||||
}
|
||||
|
||||
const fn dy(m: u64, k: u8) -> Dyadic {
|
||||
Dyadic { m, k }
|
||||
}
|
||||
/// `π`, `π/2`, `−π/2`, `π/4`, `−π/4` as reduced turns.
|
||||
const PI: Dyadic = dy(1, 1);
|
||||
const HALF_PI: Dyadic = dy(1, 2);
|
||||
const NEG_HALF_PI: Dyadic = dy(3, 2);
|
||||
const QUARTER_PI: Dyadic = dy(1, 3);
|
||||
const NEG_QUARTER_PI: Dyadic = dy(7, 3);
|
||||
|
||||
fn h(o: &mut Vec<Op>, q: u16) {
|
||||
o.extend([Op::Rz(q, HALF_PI), Op::Sx(q), Op::Rz(q, HALF_PI)]);
|
||||
}
|
||||
|
||||
fn cx(o: &mut Vec<Op>, c: u16, t: u16) {
|
||||
h(o, t);
|
||||
o.push(Op::Cz(c, t));
|
||||
h(o, t);
|
||||
}
|
||||
|
||||
/// `Ry(θ) = Rz(π/2)·Rx(θ)·Rz(−π/2)`, `Rx(θ) = H·Rz(θ)·H`.
|
||||
fn ry(o: &mut Vec<Op>, q: u16, theta: Dyadic) {
|
||||
o.push(Op::Rz(q, NEG_HALF_PI));
|
||||
h(o, q);
|
||||
o.push(Op::Rz(q, theta));
|
||||
h(o, q);
|
||||
o.push(Op::Rz(q, HALF_PI));
|
||||
}
|
||||
|
||||
fn decompose(gates: &[StdGate]) -> Result<Vec<Op>, NativeError> {
|
||||
let mut o = Vec::new();
|
||||
for g in gates {
|
||||
match *g {
|
||||
StdGate::X(q) => o.push(Op::X(q.into())),
|
||||
// Y = i·X·Z.
|
||||
StdGate::Y(q) => o.extend([Op::Rz(q.into(), PI), Op::X(q.into())]),
|
||||
StdGate::Z(q) => o.push(Op::Rz(q.into(), PI)),
|
||||
StdGate::H(q) => h(&mut o, q.into()),
|
||||
StdGate::S(q) => o.push(Op::Rz(q.into(), HALF_PI)),
|
||||
StdGate::Sdg(q) => o.push(Op::Rz(q.into(), NEG_HALF_PI)),
|
||||
StdGate::T(q) => o.push(Op::Rz(q.into(), QUARTER_PI)),
|
||||
StdGate::Tdg(q) => o.push(Op::Rz(q.into(), NEG_QUARTER_PI)),
|
||||
StdGate::P(q, t) => o.push(Op::Rz(q.into(), t.into())),
|
||||
StdGate::Ry(q, t) => ry(&mut o, q.into(), t.into()),
|
||||
StdGate::Cx(c, t) => cx(&mut o, c.into(), t.into()),
|
||||
StdGate::Cy(c, t) => {
|
||||
o.push(Op::Rz(t.into(), NEG_HALF_PI));
|
||||
cx(&mut o, c.into(), t.into());
|
||||
o.push(Op::Rz(t.into(), HALF_PI));
|
||||
}
|
||||
StdGate::Cz(c, t) => o.push(Op::Cz(c.into(), t.into())),
|
||||
// H = Ry(π/4)·Z·Ry(−π/4).
|
||||
StdGate::Ch(c, t) => {
|
||||
ry(&mut o, t.into(), NEG_QUARTER_PI);
|
||||
o.push(Op::Cz(c.into(), t.into()));
|
||||
ry(&mut o, t.into(), QUARTER_PI);
|
||||
}
|
||||
// CP(θ) = P(θ/2)_c · CX · P(−θ/2)_t · CX · P(θ/2)_t.
|
||||
StdGate::Cp(c, t, a) => {
|
||||
let half = Dyadic::from(a).half().ok_or(NativeError::TooFine)?;
|
||||
let (c, t) = (u16::from(c), u16::from(t));
|
||||
o.push(Op::Rz(c, half));
|
||||
cx(&mut o, c, t);
|
||||
o.push(Op::Rz(t, -half));
|
||||
cx(&mut o, c, t);
|
||||
o.push(Op::Rz(t, half));
|
||||
}
|
||||
// The Toffoli through six CX and seven T gates.
|
||||
StdGate::Ccx(a, b, t) => {
|
||||
let (a, b, t) = (u16::from(a), u16::from(b), u16::from(t));
|
||||
h(&mut o, t);
|
||||
cx(&mut o, b, t);
|
||||
o.push(Op::Rz(t, NEG_QUARTER_PI));
|
||||
cx(&mut o, a, t);
|
||||
o.push(Op::Rz(t, QUARTER_PI));
|
||||
cx(&mut o, b, t);
|
||||
o.push(Op::Rz(t, NEG_QUARTER_PI));
|
||||
cx(&mut o, a, t);
|
||||
o.push(Op::Rz(b, QUARTER_PI));
|
||||
o.push(Op::Rz(t, QUARTER_PI));
|
||||
h(&mut o, t);
|
||||
cx(&mut o, a, b);
|
||||
o.push(Op::Rz(a, QUARTER_PI));
|
||||
o.push(Op::Rz(b, NEG_QUARTER_PI));
|
||||
cx(&mut o, a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// Route `ops` (on logical qubits) from `layout`; the physical operations, the
|
||||
/// final layout and the `SWAP` count.
|
||||
fn route(ops: &[Op], layout: &[u16], next: &[Vec<u16>], dist: &[Vec<u16>], phys: usize) -> (Vec<Op>, Vec<u16>, usize) {
|
||||
let mut at: Vec<Option<u16>> = vec![None; phys];
|
||||
let mut on = layout.to_vec();
|
||||
for (l, &p) in layout.iter().enumerate() {
|
||||
at[usize::from(p)] = Some(l as u16);
|
||||
}
|
||||
let mut out = Vec::with_capacity(ops.len());
|
||||
let mut swaps = 0;
|
||||
for op in ops {
|
||||
match *op {
|
||||
Op::Rz(q, a) => out.push(Op::Rz(on[usize::from(q)], a)),
|
||||
Op::Sx(q) => out.push(Op::Sx(on[usize::from(q)])),
|
||||
Op::X(q) => out.push(Op::X(on[usize::from(q)])),
|
||||
Op::Cz(a, b) => {
|
||||
let pb = on[usize::from(b)];
|
||||
let mut pa = on[usize::from(a)];
|
||||
while dist[usize::from(pb)][usize::from(pa)] > 1 {
|
||||
let step = next[usize::from(pb)][usize::from(pa)];
|
||||
// SWAP = three CX.
|
||||
cx(&mut out, pa, step);
|
||||
cx(&mut out, step, pa);
|
||||
cx(&mut out, pa, step);
|
||||
swaps += 1;
|
||||
let moved = at[usize::from(step)];
|
||||
at[usize::from(step)] = Some(a);
|
||||
at[usize::from(pa)] = moved;
|
||||
if let Some(m) = moved {
|
||||
on[usize::from(m)] = pa;
|
||||
}
|
||||
on[usize::from(a)] = step;
|
||||
pa = step;
|
||||
}
|
||||
out.push(Op::Cz(pa, pb));
|
||||
}
|
||||
}
|
||||
}
|
||||
(out, on, swaps)
|
||||
}
|
||||
|
||||
/// Compile `c` for a processor with basis `basis` and connectivity
|
||||
/// `coupling`. See the module documentation.
|
||||
pub fn compile(c: &Circuit, basis: Basis, coupling: &Coupling) -> Result<NativeProgram, NativeError> {
|
||||
let ops = decompose(&lower(c)?)?;
|
||||
let n = usize::from(c.n_qubits);
|
||||
let phys = usize::from(coupling.qubits());
|
||||
let (next, dist) = coupling.paths();
|
||||
// (SWAPs, layout, routed operations, final layout) of the best seed so far.
|
||||
type Routed = (usize, Vec<u16>, Vec<Op>, Vec<u16>);
|
||||
let mut best: Option<Routed> = None;
|
||||
let mut largest = 0;
|
||||
for seed in 0..coupling.qubits() {
|
||||
let order = coupling.bfs(seed);
|
||||
largest = largest.max(order.len());
|
||||
if order.len() < n {
|
||||
continue;
|
||||
}
|
||||
let layout = order[..n].to_vec();
|
||||
let (routed, fin, swaps) = route(&ops, &layout, &next, &dist, phys);
|
||||
if best.as_ref().is_none_or(|b| swaps < b.0) {
|
||||
best = Some((swaps, layout, routed, fin));
|
||||
}
|
||||
}
|
||||
let Some((swaps, layout, routed, final_layout)) = best else {
|
||||
return Err(NativeError::TooFewQubits { need: c.n_qubits, largest });
|
||||
};
|
||||
let (gates, residual) = to_basis(&routed, basis, phys);
|
||||
Ok(NativeProgram { basis, n_qubits: c.n_qubits, layout, final_layout, gates, residual, swaps })
|
||||
}
|
||||
|
||||
fn to_basis(ops: &[Op], basis: Basis, phys: usize) -> (Vec<NativeGate>, Vec<(u16, Dyadic)>) {
|
||||
// The Z rotation pending on each physical qubit: merged into the next `rz`
|
||||
// flush, or carried into the next rotation's phase.
|
||||
let mut frame = vec![Dyadic::ZERO; phys];
|
||||
let mut out = Vec::with_capacity(ops.len());
|
||||
let flush = |out: &mut Vec<NativeGate>, frame: &mut [Dyadic], q: u16| {
|
||||
let f = std::mem::replace(&mut frame[usize::from(q)], Dyadic::ZERO);
|
||||
if !f.is_zero() {
|
||||
out.push(NativeGate::Rz(q, f));
|
||||
}
|
||||
};
|
||||
for op in ops {
|
||||
match (basis, *op) {
|
||||
(_, Op::Rz(q, a)) => frame[usize::from(q)] = frame[usize::from(q)] + a,
|
||||
// CZ = Rz(−π/2)⊗Rz(−π/2)·zz(¼) up to a global phase; the rotations
|
||||
// join the frames.
|
||||
(Basis::GpiZz, Op::Cz(a, b)) => {
|
||||
out.push(NativeGate::Zz(a, b, HALF_PI));
|
||||
frame[usize::from(a)] = frame[usize::from(a)] + NEG_HALF_PI;
|
||||
frame[usize::from(b)] = frame[usize::from(b)] + NEG_HALF_PI;
|
||||
}
|
||||
// A Z rotation commutes with CZ: it waits.
|
||||
(_, Op::Cz(a, b)) => out.push(NativeGate::Cz(a, b)),
|
||||
(Basis::RzSxXCz, Op::Sx(q)) => {
|
||||
flush(&mut out, &mut frame, q);
|
||||
out.push(NativeGate::Sx(q));
|
||||
}
|
||||
(Basis::RzSxXCz, Op::X(q)) => {
|
||||
flush(&mut out, &mut frame, q);
|
||||
out.push(NativeGate::X(q));
|
||||
}
|
||||
// prx(θ, 0)·Rz(α) = Rz(α)·prx(θ, −α): the rotation moves on. SX and X
|
||||
// are Rx(π/2) and Rx(π) up to a global phase.
|
||||
(Basis::PrxCz, Op::Sx(q)) => out.push(NativeGate::Prx(q, HALF_PI, -frame[usize::from(q)])),
|
||||
(Basis::PrxCz, Op::X(q)) => out.push(NativeGate::Prx(q, PI, -frame[usize::from(q)])),
|
||||
(Basis::GpiZz, Op::Sx(q)) => out.push(NativeGate::Gpi2(q, -frame[usize::from(q)])),
|
||||
(Basis::GpiZz, Op::X(q)) => out.push(NativeGate::Gpi(q, -frame[usize::from(q)])),
|
||||
}
|
||||
}
|
||||
let mut residual = Vec::new();
|
||||
for q in 0..phys as u16 {
|
||||
match basis {
|
||||
Basis::RzSxXCz => flush(&mut out, &mut frame, q),
|
||||
Basis::PrxCz | Basis::GpiZz => {
|
||||
if !frame[usize::from(q)].is_zero() {
|
||||
residual.push((q, frame[usize::from(q)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(out, residual)
|
||||
}
|
||||
|
||||
/// `prx(θ, φ) = Rz(φ)·Rx(θ)·Rz(−φ)` on `q[i]`, in the gates the reader takes.
|
||||
fn phased_x(i: usize, theta: Dyadic, phi: Dyadic) -> String {
|
||||
format!("rz({}) q[{i}];\nrx({}) q[{i}];\nrz({}) q[{i}];\n", (-phi).qasm(), theta.qasm(), phi.qasm())
|
||||
}
|
||||
|
||||
/// A [`Dyadic`] as a number of turns, `m/2^k`, written exactly in decimal.
|
||||
pub fn turns(d: Dyadic) -> String {
|
||||
let (m, k) = d.turns();
|
||||
if k == 0 {
|
||||
return "0".into();
|
||||
}
|
||||
// m/2^k = m·5^k/10^k: k decimal digits, exactly.
|
||||
let digits = (u128::from(m) * 5u128.pow(u32::from(k))).to_string();
|
||||
let k = usize::from(k);
|
||||
let padded = format!("{digits:0>k$}");
|
||||
let (int, frac) = padded.split_at(padded.len() - k);
|
||||
format!("{}.{}", if int.is_empty() { "0" } else { int }, frac.trim_end_matches('0'))
|
||||
}
|
||||
|
||||
impl NativeProgram {
|
||||
/// The program as OpenQASM 3 on physical qubits (`$p`), each logical
|
||||
/// qubit `i` measured into `c[i]` where it ends. A phased-X program defines
|
||||
/// `prx` from the standard gates.
|
||||
pub fn qasm3(&self) -> String {
|
||||
let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n");
|
||||
match self.basis {
|
||||
Basis::RzSxXCz => {}
|
||||
Basis::PrxCz => s.push_str("gate prx(theta, phi) a { rz(-phi) a; rx(theta) a; rz(phi) a; }\n"),
|
||||
// The phases and the angle in turns.
|
||||
Basis::GpiZz => s.push_str(
|
||||
"gate gpi(phi) a { rz(-2*pi*phi) a; x a; rz(2*pi*phi) a; }\n\
|
||||
gate gpi2(phi) a { rz(-2*pi*phi) a; rx(pi/2) a; rz(2*pi*phi) a; }\n\
|
||||
gate zz(theta) a, b { cx a, b; rz(2*pi*theta) b; cx a, b; }\n",
|
||||
),
|
||||
}
|
||||
s.push_str(&format!("bit[{}] c;\n", self.n_qubits));
|
||||
for g in &self.gates {
|
||||
s.push_str(&match *g {
|
||||
NativeGate::Rz(q, a) => format!("rz({}) ${q};\n", a.qasm()),
|
||||
NativeGate::Sx(q) => format!("sx ${q};\n"),
|
||||
NativeGate::X(q) => format!("x ${q};\n"),
|
||||
NativeGate::Prx(q, t, p) => format!("prx({}, {}) ${q};\n", t.qasm(), p.qasm()),
|
||||
NativeGate::Cz(a, b) => format!("cz ${a}, ${b};\n"),
|
||||
NativeGate::Gpi(q, p) => format!("gpi({}) ${q};\n", turns(p)),
|
||||
NativeGate::Gpi2(q, p) => format!("gpi2({}) ${q};\n", turns(p)),
|
||||
NativeGate::Zz(a, b, t) => format!("zz({}) ${a}, ${b};\n", turns(t)),
|
||||
});
|
||||
}
|
||||
for (i, p) in self.final_layout.iter().enumerate() {
|
||||
s.push_str(&format!("c[{i}] = measure ${p};\n"));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Check the program against the circuit it was compiled from: every gate
|
||||
/// in the basis, every `cz` on a pair `coupling` connects, and the final
|
||||
/// state, residual rotations applied, equal to the circuit's with each
|
||||
/// logical qubit on its final physical qubit.
|
||||
pub fn verify(&self, c: &Circuit, coupling: &Coupling) -> Result<(), NativeError> {
|
||||
for (i, g) in self.gates.iter().enumerate() {
|
||||
let ok = match (self.basis, *g) {
|
||||
(Basis::RzSxXCz | Basis::PrxCz, NativeGate::Cz(a, b)) | (Basis::GpiZz, NativeGate::Zz(a, b, _)) => coupling.connected(a, b),
|
||||
(Basis::RzSxXCz, NativeGate::Rz(q, _) | NativeGate::Sx(q) | NativeGate::X(q)) => q < coupling.qubits(),
|
||||
(Basis::PrxCz, NativeGate::Prx(q, _, _)) => q < coupling.qubits(),
|
||||
(Basis::GpiZz, NativeGate::Gpi(q, _) | NativeGate::Gpi2(q, _)) => q < coupling.qubits(),
|
||||
_ => false,
|
||||
};
|
||||
if !ok {
|
||||
return Err(NativeError::NotNative { gate: i });
|
||||
}
|
||||
}
|
||||
// The physical qubits the program touches, renumbered from zero.
|
||||
let mut used: Vec<u16> = self.final_layout.clone();
|
||||
for g in &self.gates {
|
||||
match *g {
|
||||
NativeGate::Rz(q, _) | NativeGate::Sx(q) | NativeGate::X(q) | NativeGate::Prx(q, _, _) | NativeGate::Gpi(q, _) | NativeGate::Gpi2(q, _) => used.push(q),
|
||||
NativeGate::Cz(a, b) | NativeGate::Zz(a, b, _) => used.extend([a, b]),
|
||||
}
|
||||
}
|
||||
used.sort_unstable();
|
||||
used.dedup();
|
||||
if used.len() > usize::from(MAX_QUBITS) {
|
||||
return Err(NativeError::TooWideToVerify { qubits: used.len() });
|
||||
}
|
||||
let idx = |p: u16| used.binary_search(&p).expect("every touched qubit is listed");
|
||||
let m = used.len();
|
||||
let mut text = format!("OPENQASM 3.0;\nqubit[{m}] q;\n");
|
||||
for g in &self.gates {
|
||||
text.push_str(&match *g {
|
||||
NativeGate::Rz(q, a) => format!("rz({}) q[{}];\n", a.qasm(), idx(q)),
|
||||
NativeGate::Sx(q) => format!("sx q[{}];\n", idx(q)),
|
||||
NativeGate::X(q) => format!("x q[{}];\n", idx(q)),
|
||||
NativeGate::Prx(q, t, p) => phased_x(idx(q), t, p),
|
||||
NativeGate::Gpi(q, p) => phased_x(idx(q), PI, p),
|
||||
NativeGate::Gpi2(q, p) => phased_x(idx(q), HALF_PI, p),
|
||||
NativeGate::Cz(a, b) => format!("cz q[{}],q[{}];\n", idx(a), idx(b)),
|
||||
// exp(−iπθ·Z⊗Z) = CX·(I⊗Rz(2πθ))·CX.
|
||||
NativeGate::Zz(a, b, t) => {
|
||||
let (a, b) = (idx(a), idx(b));
|
||||
format!("cx q[{a}],q[{b}];\nrz({}) q[{b}];\ncx q[{a}],q[{b}];\n", t.qasm())
|
||||
}
|
||||
});
|
||||
}
|
||||
for &(q, a) in &self.residual {
|
||||
text.push_str(&format!("rz({}) q[{}];\n", a.qasm(), idx(q)));
|
||||
}
|
||||
let program = super::from_qasm(&text).map_err(|e| NativeError::Coupling(format!("re-reading the program: {e}")))?;
|
||||
let mut reference = Circuit::new(m as u8);
|
||||
let at = |l: u8| idx(self.final_layout[usize::from(l)]) as u8;
|
||||
for g in &c.ops {
|
||||
reference.ops.push(Gate {
|
||||
base: g.base,
|
||||
controls: g.controls.iter().map(|&q| at(q)).collect(),
|
||||
target: at(g.target),
|
||||
param: if g.base == BaseGate::P { g.param } else { 0 },
|
||||
});
|
||||
}
|
||||
let a = program.simulate().map_err(|e| NativeError::Device(DeviceError::Invalid(e)))?;
|
||||
let b = reference.simulate().map_err(|e| NativeError::Device(DeviceError::Invalid(e)))?;
|
||||
let fidelity = a.fidelity_fx(&b);
|
||||
// Fixed-point rounding over a long program: a few parts per million.
|
||||
if (fidelity - ONE).abs() > 1 << 14 {
|
||||
return Err(NativeError::Mismatch { fidelity });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn gate(base: BaseGate, controls: &[u8], target: u8, param: u16) -> Gate {
|
||||
Gate { base, controls: controls.to_vec(), target, param }
|
||||
}
|
||||
|
||||
fn prepared(n: u8) -> Circuit {
|
||||
let mut c = Circuit::new(n);
|
||||
for q in 0..n {
|
||||
c.ops.push(gate(BaseGate::H, &[], q, 0));
|
||||
c.ops.push(gate(BaseGate::P, &[], q, u16::from(q) + 2));
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn random_circuit(n: u8, gates: usize, seed: u64) -> Circuit {
|
||||
let mut s = seed;
|
||||
let mut next = || {
|
||||
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
(s >> 33) as u32
|
||||
};
|
||||
let bases = [BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::P];
|
||||
let mut c = prepared(n);
|
||||
for _ in 0..gates {
|
||||
let target = (next() % u32::from(n)) as u8;
|
||||
let mut controls = Vec::new();
|
||||
for _ in 0..next() % 3 {
|
||||
let q = (next() % u32::from(n)) as u8;
|
||||
if q != target && !controls.contains(&q) {
|
||||
controls.push(q);
|
||||
}
|
||||
}
|
||||
let base = bases[(next() as usize) % bases.len()];
|
||||
let param = if base == BaseGate::P { 1 + (next() % 10) as u16 } else { 0 };
|
||||
c.ops.push(gate(base, &controls, target, param));
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dyadic_angles_reduce_add_and_print() {
|
||||
let d = |m, k| Dyadic::new(m, k).unwrap();
|
||||
assert_eq!(d(2, 2), d(1, 1));
|
||||
assert_eq!(d(4, 2), Dyadic::ZERO);
|
||||
assert_eq!(d(1, 2) + d(1, 2), d(1, 1));
|
||||
assert_eq!(d(1, 3) + d(7, 3), Dyadic::ZERO);
|
||||
assert_eq!(-d(3, 3), d(5, 3));
|
||||
assert_eq!(d(1, 1).half(), Some(d(1, 2)));
|
||||
assert_eq!(d(1, 32).half(), None);
|
||||
assert_eq!(Dyadic::new(1, 33), None);
|
||||
assert_eq!(d(0, 0).qasm(), "0");
|
||||
assert_eq!(d(1, 1).qasm(), "pi");
|
||||
assert_eq!(d(1, 3).qasm(), "pi/4");
|
||||
assert_eq!(d(3, 2).qasm(), "-pi/2");
|
||||
assert_eq!(d(13, 4).qasm(), "-3*pi/8");
|
||||
assert_eq!(d(3, 4).qasm(), "3*pi/8");
|
||||
assert_eq!(d(1, 3).turns(), (1, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_gate_compiles_exactly_in_both_bases() {
|
||||
let bases = [
|
||||
(BaseGate::X, 0),
|
||||
(BaseGate::Y, 0),
|
||||
(BaseGate::Z, 0),
|
||||
(BaseGate::H, 0),
|
||||
(BaseGate::S, 0),
|
||||
(BaseGate::Sdg, 0),
|
||||
(BaseGate::T, 0),
|
||||
(BaseGate::Tdg, 0),
|
||||
(BaseGate::P, 1),
|
||||
(BaseGate::P, 5),
|
||||
];
|
||||
let map = Coupling::grid(2, 3);
|
||||
for (base, param) in bases {
|
||||
for k in 0..=3u8 {
|
||||
let controls: Vec<u8> = [3u8, 0, 4].iter().copied().take(usize::from(k)).collect();
|
||||
let mut c = prepared(5);
|
||||
c.ops.push(gate(base, &controls, 1, param));
|
||||
for basis in [Basis::RzSxXCz, Basis::PrxCz, Basis::GpiZz] {
|
||||
let p = compile(&c, basis, &map).unwrap();
|
||||
p.verify(&c, &map).unwrap_or_else(|e| panic!("{base:?} {controls:?} {basis:?}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_circuits_route_onto_a_line_and_a_grid() {
|
||||
for seed in 0..12u64 {
|
||||
let c = random_circuit(5, 16, seed);
|
||||
for map in [Coupling::line(7), Coupling::grid(3, 3)] {
|
||||
for basis in [Basis::RzSxXCz, Basis::PrxCz, Basis::GpiZz] {
|
||||
let p = compile(&c, basis, &map).unwrap();
|
||||
p.verify(&c, &map).unwrap_or_else(|e| panic!("seed {seed} {basis:?}: {e}"));
|
||||
for g in &p.gates {
|
||||
if let NativeGate::Cz(a, b) = *g {
|
||||
assert!(map.connected(a, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_needs_swaps_and_the_program_records_where_qubits_end() {
|
||||
// CZ between the ends of a 4-qubit line chain: at least one SWAP.
|
||||
let mut c = Circuit::new(4);
|
||||
c.ops.push(gate(BaseGate::H, &[], 0, 0));
|
||||
c.ops.push(gate(BaseGate::X, &[0], 1, 0));
|
||||
c.ops.push(gate(BaseGate::X, &[1], 2, 0));
|
||||
c.ops.push(gate(BaseGate::X, &[2], 3, 0));
|
||||
c.ops.push(gate(BaseGate::Z, &[0], 3, 0));
|
||||
let map = Coupling::line(4);
|
||||
let p = compile(&c, Basis::RzSxXCz, &map).unwrap();
|
||||
assert!(p.swaps >= 1);
|
||||
assert_ne!(p.layout, p.final_layout);
|
||||
p.verify(&c, &map).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compilation_is_deterministic() {
|
||||
let c = random_circuit(4, 20, 99);
|
||||
let map = Coupling::grid(3, 3);
|
||||
assert_eq!(compile(&c, Basis::PrxCz, &map), compile(&c, Basis::PrxCz, &map));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrong_program_fails_verification() {
|
||||
let c = random_circuit(4, 12, 7);
|
||||
let map = Coupling::grid(2, 3);
|
||||
for basis in [Basis::RzSxXCz, Basis::PrxCz, Basis::GpiZz] {
|
||||
let good = compile(&c, basis, &map).unwrap();
|
||||
good.verify(&c, &map).unwrap();
|
||||
// A dropped gate.
|
||||
let mut p = good.clone();
|
||||
let i = p.gates.iter().position(|g| !matches!(g, NativeGate::Rz(..))).unwrap();
|
||||
p.gates.remove(i);
|
||||
assert!(matches!(p.verify(&c, &map), Err(NativeError::Mismatch { .. })), "{basis:?}");
|
||||
// A cz on a pair the map does not connect.
|
||||
let mut p = good.clone();
|
||||
p.gates.push(NativeGate::Cz(0, 5));
|
||||
assert_eq!(p.verify(&c, &map), Err(NativeError::NotNative { gate: p.gates.len() - 1 }));
|
||||
// A wrong final layout.
|
||||
let mut p = good.clone();
|
||||
p.final_layout.swap(0, 1);
|
||||
assert!(p.verify(&c, &map).is_err(), "{basis:?}");
|
||||
}
|
||||
// A gate from the other basis.
|
||||
let mut p = compile(&c, Basis::PrxCz, &map).unwrap();
|
||||
p.gates.push(NativeGate::Sx(0));
|
||||
assert!(matches!(p.verify(&c, &map), Err(NativeError::NotNative { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_map_too_small_or_split_is_refused() {
|
||||
let c = prepared(4);
|
||||
let split = Coupling::new(6, &[(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap();
|
||||
assert_eq!(compile(&c, Basis::RzSxXCz, &split), Err(NativeError::TooFewQubits { need: 4, largest: 3 }));
|
||||
assert!(Coupling::new(3, &[(0, 0)]).is_err());
|
||||
assert!(Coupling::new(3, &[(0, 3)]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_qasm_names_physical_qubits_and_measures_where_each_logical_qubit_ends() {
|
||||
let mut c = Circuit::new(3);
|
||||
c.ops.push(gate(BaseGate::H, &[], 0, 0));
|
||||
c.ops.push(gate(BaseGate::X, &[0], 2, 0));
|
||||
let map = Coupling::line(5);
|
||||
let p = compile(&c, Basis::RzSxXCz, &map).unwrap();
|
||||
let text = p.qasm3();
|
||||
assert!(text.starts_with("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nbit[3] c;\n"));
|
||||
for (i, q) in p.final_layout.iter().enumerate() {
|
||||
assert!(text.contains(&format!("c[{i}] = measure ${q};")), "{text}");
|
||||
}
|
||||
for line in text.lines().skip(3) {
|
||||
let name: String = line.chars().take_while(|c| c.is_ascii_alphanumeric()).collect();
|
||||
assert!(["rz", "sx", "x", "cz", "c"].contains(&name.as_str()), "{line}");
|
||||
}
|
||||
let prx = compile(&c, Basis::PrxCz, &map).unwrap().qasm3();
|
||||
assert!(prx.contains("gate prx(theta, phi) a"));
|
||||
let gpi = compile(&c, Basis::GpiZz, &map).unwrap().qasm3();
|
||||
assert!(gpi.contains("gate zz(theta) a, b") && gpi.contains("zz(0.25) $"), "{gpi}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turns_print_exactly_in_decimal() {
|
||||
let d = |m, k| Dyadic::new(m, k).unwrap();
|
||||
assert_eq!(turns(Dyadic::ZERO), "0");
|
||||
assert_eq!(turns(d(1, 1)), "0.5");
|
||||
assert_eq!(turns(d(1, 2)), "0.25");
|
||||
assert_eq!(turns(d(3, 2)), "0.75");
|
||||
assert_eq!(turns(d(5, 4)), "0.3125");
|
||||
assert_eq!(turns(d(1, 32)), "0.00000000023283064365386962890625");
|
||||
// Read back as an f64, each is exact: m/2^k has at most 53 significant bits.
|
||||
for (m, k) in [(1u64, 1u8), (3, 2), (5, 4), (1, 32), (4294967295, 32)] {
|
||||
assert_eq!(turns(d(m, k)).parse::<f64>().unwrap(), m as f64 / (1u64 << k) as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
112
wai/wai-quantum/tests/cuda_sv.rs
Normal file
112
wai/wai-quantum/tests/cuda_sv.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! The dense f64 state vector on the CUDA devices of the machine that runs
|
||||
//! this: bit for bit the CPU's, or the certificate says not.
|
||||
//!
|
||||
//! The machine declares its devices in `WAI_QUANTUM_CUDA_EXPECT` (comma-
|
||||
//! separated keys, or `none`), as for `cuda_member`; `WAI_QUANTUM_CUDA_REPORT`
|
||||
//! names a file the established lines are appended to.
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use std::io::Write;
|
||||
use wai_quantum::gates::Gate;
|
||||
use wai_quantum::quantum_fabric::sv64::{self, MUTANTS, SV_DEFAULT, SV_DEFAULT_PIN, SvCorpus};
|
||||
use wai_quantum::quantum_fabric::{Certification, Parity};
|
||||
use wai_quantum::quantum_fabric_cuda::{CudaMember, discover};
|
||||
use wai_quantum::quantum_sv::StateVector;
|
||||
|
||||
fn report(line: &str) {
|
||||
println!("{line}");
|
||||
if let Some(path) = std::env::var_os("WAI_QUANTUM_CUDA_REPORT") {
|
||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).expect("the report file opens");
|
||||
writeln!(f, "{line}").expect("the report line is written");
|
||||
}
|
||||
}
|
||||
|
||||
fn members() -> Vec<CudaMember> {
|
||||
let d = discover();
|
||||
let mut found: Vec<String> = d.found.iter().map(|m| m.key().to_string()).collect();
|
||||
found.sort();
|
||||
let found_list = if found.is_empty() { "none".to_string() } else { found.join(",") };
|
||||
let expect = std::env::var("WAI_QUANTUM_CUDA_EXPECT").unwrap_or_else(|_| panic!("WAI_QUANTUM_CUDA_EXPECT is not set; found {found_list}"));
|
||||
assert_eq!(found_list, expect, "the devices found are not the declared ones");
|
||||
d.found
|
||||
}
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
/// The CPU certifying itself on `corpus`: the digest a bit-exact device
|
||||
/// reproduces.
|
||||
fn cpu_digest(corpus: SvCorpus) -> String {
|
||||
let mut cert = Certification::new();
|
||||
for case in corpus.cases() {
|
||||
let r = case.reference().unwrap();
|
||||
cert.add(&case.bytes(), &r, &r);
|
||||
}
|
||||
hex(&cert.finish(sv64::KERNEL_SV_RUN, "cpu", "cpu", &corpus.generator()).corpus)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s0_the_state_vector_is_the_cpus_bit_for_bit() {
|
||||
let cpu = cpu_digest(SV_DEFAULT);
|
||||
assert_eq!(cpu, SV_DEFAULT_PIN, "this machine's CPU does not reproduce the pin");
|
||||
for m in members() {
|
||||
let sv = m.sv().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
let c = sv.certify(SV_DEFAULT).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
assert_eq!(c.parity, Parity::BitExact, "{}", m.key());
|
||||
assert_eq!(c.cases, SV_DEFAULT.cases as u64);
|
||||
assert_eq!(hex(&c.corpus), cpu, "{}: the digest is not the CPU's", m.key());
|
||||
report(&format!("cuda sv certified: key={} member={} kernel={} implementation={} parity=bit-exact cases={} digest={}", m.key(), m.id(), c.kernel, c.implementation, c.cases, hex(&c.corpus)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s1_wide_states_match_the_cpu() {
|
||||
for m in members() {
|
||||
let sv = m.sv().unwrap();
|
||||
for (n, width) in [(16u32, 0usize), (20, 3)] {
|
||||
let mut gates = Vec::new();
|
||||
let mut a = 0.1f64;
|
||||
for layer in 0..6u32 {
|
||||
for q in 0..n {
|
||||
a += 0.731;
|
||||
gates.push(match (q + layer) % 5 {
|
||||
0 => Gate::rx(q, a),
|
||||
1 => Gate::ry(q, a),
|
||||
2 => Gate::rz(q, a),
|
||||
3 => Gate::sqrt_w(q),
|
||||
_ => Gate::t(q),
|
||||
});
|
||||
}
|
||||
let mut q = layer % 2;
|
||||
while q + 1 < n {
|
||||
gates.push(if q % 3 == 0 { Gate::fsim(q, q + 1, a, 0.5 * a) } else { Gate::cz(q, q + 1) });
|
||||
q += 2;
|
||||
}
|
||||
gates.push(Gate::iswap(0, n - 1));
|
||||
}
|
||||
let mut cpu = StateVector::zero(n).unwrap();
|
||||
cpu.run(&gates, width).unwrap();
|
||||
let (re, im) = sv.run(n, &gates, width).unwrap();
|
||||
let same = |a: &[f64], b: &[f64]| a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits());
|
||||
assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: n={n} width={width}", m.key());
|
||||
}
|
||||
report(&format!("cuda sv wide: key={} 16 and 20 qubits match the CPU bit for bit", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s2_mutants_are_caught() {
|
||||
let small = SvCorpus { cases: 18, ..SV_DEFAULT };
|
||||
let cpu = cpu_digest(small);
|
||||
for m in members() {
|
||||
let mut killed = 0;
|
||||
for mutant in &MUTANTS {
|
||||
let sv = m.sv_with_source(&sv64::mutate(mutant)).unwrap_or_else(|e| panic!("mutant {}: {e}", mutant.name));
|
||||
let c = sv.certify(small).unwrap();
|
||||
assert!(c.parity != Parity::BitExact || hex(&c.corpus) != cpu, "{}: mutant {} survived", m.key(), mutant.name);
|
||||
killed += 1;
|
||||
}
|
||||
report(&format!("cuda sv mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len()));
|
||||
}
|
||||
}
|
||||
431
wai/wai-quantum/tests/fpga_member.rs
Normal file
431
wai/wai-quantum/tests/fpga_member.rs
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
//! The fabric's simulated FPGA member on the machine that runs this.
|
||||
//!
|
||||
//! The machine declares the devices it must have in `WAI_QUANTUM_FPGA_EXPECT`:
|
||||
//! their keys, comma-separated and sorted, or `none`. Every test that needs a
|
||||
//! member fails unless the devices found are exactly the declared ones, so a
|
||||
//! machine whose GPU or driver went missing, or a run that declared nothing,
|
||||
//! fails instead of passing over no member. Each test prints what it
|
||||
//! established and, when `WAI_QUANTUM_FPGA_REPORT` names a file, appends the
|
||||
//! same lines to it, which a CI step can count per device.
|
||||
//!
|
||||
//! ```text
|
||||
//! WAI_QUANTUM_FPGA_EXPECT=fpga-sim-verilator \
|
||||
//! cargo test --release --features quantum_fabric_fpga --test fpga_member -- --test-threads=1
|
||||
//! ```
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::OnceLock;
|
||||
use wai_quantum::ed25519_dalek::SigningKey;
|
||||
use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset, MAX_QUBITS, QuantumError};
|
||||
use wai_quantum::quantum_fabric::fpga::{self, MUTANTS};
|
||||
use wai_quantum::quantum_fabric::lanes::{self, GateWords, Lane};
|
||||
use wai_quantum::quantum_fabric::plan::{Program, Step};
|
||||
use wai_quantum::quantum_fabric::wgsl::{Detector, Mutant};
|
||||
use wai_quantum::quantum_fabric::{
|
||||
Corpus, CpuReference, FABRIC_DEFAULT, Fabric, FabricClass, FabricReceipt, MemberError, Parity, Presence, REFERENCE, apply_kernel, detect_local, place, ready,
|
||||
};
|
||||
use wai_quantum::quantum_fabric_fpga::{Discovery, FpgaError, FpgaSimMember, discover};
|
||||
|
||||
/// The CPU's corpus digests: random-v1 (seed 7, 200 cases) and fabric-v1
|
||||
/// (seed 7, 200 + 24 cases), per gateset. A bit-exact member reproduces them.
|
||||
const PINS: [(Gateset, &str, &str); 2] = [
|
||||
(Gateset::V1, "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36", "41c726a0ac80f7defef92e15ecf893bdb7a3076b5e7f670fa0b7142e5c033df7"),
|
||||
(Gateset::V2, "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2", "9a84b55af68f1a8b07390b234cf562dd8d85cef3185a697ab85690c8790a7809"),
|
||||
];
|
||||
|
||||
const RANDOM_V1: Corpus = Corpus::Random { seed: 7, cases: 200 };
|
||||
|
||||
static DISCOVERY: OnceLock<Discovery> = OnceLock::new();
|
||||
|
||||
fn discovery() -> &'static Discovery {
|
||||
DISCOVERY.get_or_init(discover)
|
||||
}
|
||||
|
||||
/// Prints `line` and appends it to the file `WAI_QUANTUM_FPGA_REPORT` names.
|
||||
fn report(line: &str) {
|
||||
println!("{line}");
|
||||
if let Some(path) = std::env::var_os("WAI_QUANTUM_FPGA_REPORT") {
|
||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).expect("the report file opens");
|
||||
writeln!(f, "{line}").expect("the report line is written");
|
||||
}
|
||||
}
|
||||
|
||||
/// The declared device set, as written, if it is exactly what was found;
|
||||
/// otherwise why not, with what was found and refused and how to declare it.
|
||||
fn declared(d: &Discovery) -> Result<String, String> {
|
||||
let mut found: Vec<&str> = d.found.iter().map(|m| m.key()).collect();
|
||||
found.sort_unstable();
|
||||
let found_list = if found.is_empty() { "none".to_string() } else { found.join(",") };
|
||||
let refused: Vec<String> = d.refused.iter().map(|r| format!("{}: {}", r.key, r.error)).collect();
|
||||
let refused_list = if refused.is_empty() { "none".to_string() } else { refused.join("; ") };
|
||||
let unavailable = d.unavailable.as_deref().unwrap_or("no");
|
||||
let how = format!("found: {found_list}; refused: {refused_list}; unavailable: {unavailable}. Declare the devices this machine must have: WAI_QUANTUM_FPGA_EXPECT={found_list}");
|
||||
let expect = match std::env::var("WAI_QUANTUM_FPGA_EXPECT") {
|
||||
Ok(v) if !v.is_empty() => v,
|
||||
_ => return Err(format!("WAI_QUANTUM_FPGA_EXPECT is not set. {how}")),
|
||||
};
|
||||
let keys: Vec<&str> = if expect == "none" { Vec::new() } else { expect.split(',').collect() };
|
||||
let well_formed = keys.windows(2).all(|w| w[0] <= w[1]) && keys.iter().all(|k| !k.is_empty() && *k != "none" && k.trim() == *k);
|
||||
if !well_formed {
|
||||
return Err(format!("WAI_QUANTUM_FPGA_EXPECT={expect:?} is not a sorted, comma-separated list of keys, nor none. {how}"));
|
||||
}
|
||||
for k in &keys {
|
||||
if !found.contains(k)
|
||||
&& let Some(r) = d.refused.iter().find(|r| r.key == *k)
|
||||
{
|
||||
return Err(format!("the declared device {k} was refused: {}. {how}", r.error));
|
||||
}
|
||||
}
|
||||
if found != keys {
|
||||
return Err(format!("the devices found are not the declared ones ({expect}). {how}"));
|
||||
}
|
||||
Ok(expect)
|
||||
}
|
||||
|
||||
/// The members, once the declared devices are exactly the ones found.
|
||||
fn members() -> &'static [FpgaSimMember] {
|
||||
let d = discovery();
|
||||
if let Err(why) = declared(d) {
|
||||
panic!("{why}");
|
||||
}
|
||||
&d.found
|
||||
}
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
fn next(s: &mut u64) -> u64 {
|
||||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = *s;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
/// A value in `[-bound, bound]`.
|
||||
fn within(s: &mut u64, bound: i64) -> i32 {
|
||||
((next(s) % (2 * bound as u64 + 1)) as i64 - bound) as i32
|
||||
}
|
||||
|
||||
/// A random circuit of `ops` gates on `n` qubits: every base gate, up to three
|
||||
/// distinct controls, `P(1..=32)`.
|
||||
fn random_circuit(n: u8, ops: usize, seed: u64, gs: Gateset) -> Circuit {
|
||||
let bases = [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P];
|
||||
let mut s = seed;
|
||||
let mut c = Circuit::with_gateset(n, gs);
|
||||
for _ in 0..ops {
|
||||
let base = bases[(next(&mut s) % bases.len() as u64) as usize];
|
||||
let target = (next(&mut s) % u64::from(n)) as u8;
|
||||
let mut controls = Vec::new();
|
||||
for _ in 0..next(&mut s) % 4 {
|
||||
let q = (next(&mut s) % u64::from(n)) as u8;
|
||||
if q != target && !controls.contains(&q) {
|
||||
controls.push(q);
|
||||
}
|
||||
}
|
||||
let param = if base == BaseGate::P { 1 + (next(&mut s) % 32) as u16 } else { 0 };
|
||||
c.ops.push(Gate { base, controls, target, param });
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// Five circuits the native simulator refuses, in each gateset.
|
||||
fn refused_circuits() -> Vec<Circuit> {
|
||||
let gate = |base, controls: Vec<u8>, target, param| Gate { base, controls, target, param };
|
||||
let bad = [
|
||||
(1, vec![gate(BaseGate::H, vec![], 0, 0), gate(BaseGate::P, vec![], 0, 0)]),
|
||||
(1, vec![gate(BaseGate::P, vec![], 0, 33)]),
|
||||
(1, vec![gate(BaseGate::X, vec![], 3, 0)]),
|
||||
(2, vec![gate(BaseGate::X, vec![0], 0, 0)]),
|
||||
(2, vec![gate(BaseGate::X, vec![5], 1, 0)]),
|
||||
];
|
||||
let mut out = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
for (n, ops) in bad.clone() {
|
||||
let mut c = Circuit::with_gateset(n, gs);
|
||||
c.ops = ops;
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `Z` on qubit 0: on `[(0, 0), (i32::MIN, 0)]` its exact result is `2^31`,
|
||||
/// which leaves 32 bits.
|
||||
fn z_words() -> GateWords {
|
||||
lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 0, param: 0 }, Gateset::V2)
|
||||
}
|
||||
|
||||
const Z_STATE: [Lane; 2] = [[0, 0], [i32::MIN, 0]];
|
||||
|
||||
#[test]
|
||||
fn c0_devices_are_declared() {
|
||||
let d = discovery();
|
||||
match declared(d) {
|
||||
Ok(expect) => report(&format!("fpga devices: as declared ({expect})")),
|
||||
Err(why) => panic!("{why}"),
|
||||
}
|
||||
for m in &d.found {
|
||||
assert_eq!(m.class(), FabricClass::Fpga);
|
||||
assert!(m.id().starts_with("local/fpga-sim-"), "{}", m.id());
|
||||
assert_eq!(m.implementation(), fpga::implementation_id());
|
||||
assert_eq!(m.identity().member_id(), m.id());
|
||||
assert!(m.max_qubits() >= 20, "{}: holds only {} qubits", m.key(), m.max_qubits());
|
||||
report(&format!("fpga member: key={} id={} {}", m.key(), m.id(), m.detail()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c1_probe_matches_lanes() {
|
||||
for m in members() {
|
||||
let pairs = lanes::probe_pairs(1 << 16, 0x0cda);
|
||||
let out = m.probe(&pairs).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
assert_eq!(lanes::first_mismatch(&pairs, &out), None, "{}", m.key());
|
||||
report(&format!("fpga probe: key={} {} pairs agree", m.key(), pairs.len()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c2_kernel_matches_run_words() {
|
||||
for m in members() {
|
||||
let mut s = 0x6b65_726e_656c_u64;
|
||||
let (mut flagged, mut clean) = (0, 0);
|
||||
for case in 0..2000 {
|
||||
let n = 1 + (next(&mut s) % 12) as u32;
|
||||
// Three regimes: the whole 32-bit range (nearly always out of
|
||||
// range), small states and entries (never), and states and
|
||||
// entries up to 2^30 (either).
|
||||
let mode = case % 3;
|
||||
let value = |s: &mut u64| -> i32 {
|
||||
match mode {
|
||||
0 => next(s) as i32,
|
||||
1 => within(s, 1 << 22),
|
||||
_ => within(s, 1 << 30),
|
||||
}
|
||||
};
|
||||
let entry = |s: &mut u64| -> u32 {
|
||||
match mode {
|
||||
0 => next(s) as u32,
|
||||
1 => within(s, 1 << 29) as u32,
|
||||
_ => within(s, 1 << 30) as u32,
|
||||
}
|
||||
};
|
||||
let state: Vec<Lane> = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect();
|
||||
let gates: Vec<GateWords> = (0..1 + next(&mut s) % 8)
|
||||
.map(|_| {
|
||||
let t = (next(&mut s) % u64::from(n)) as u32;
|
||||
let ctrl = (0..n).filter(|&b| b != t && next(&mut s).is_multiple_of(4)).fold(0u32, |acc, b| acc | 1 << b);
|
||||
let mut w = [0u32; 8];
|
||||
w.iter_mut().for_each(|x| *x = entry(&mut s));
|
||||
GateWords { m: w, tbit: 1 << t, ctrl }
|
||||
})
|
||||
.collect();
|
||||
let want = lanes::run_words(&state, &gates).expect("valid shapes");
|
||||
let got = m.run_lanes(&state, &gates).unwrap_or_else(|e| panic!("{}: case {case}: {e}", m.key()));
|
||||
assert_eq!(got.out_of_range, want.out_of_range, "{}: case {case}, n = {n}", m.key());
|
||||
assert!(got.amps == want.amps, "{}: case {case}, n = {n}: the lanes differ", m.key());
|
||||
if want.out_of_range.is_some() {
|
||||
flagged += 1;
|
||||
} else {
|
||||
clean += 1;
|
||||
}
|
||||
}
|
||||
assert!(flagged >= 100 && clean >= 100, "{}: {flagged} flagged, {clean} clean", m.key());
|
||||
report(&format!("fpga kernel: key={} ok", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c3_certificates_reproduce_the_pins() {
|
||||
let ms = members();
|
||||
for (gs, random_pin, fabric_pin) in PINS {
|
||||
let cpu = lanes::certify_corpus(gs, "local/test", RANDOM_V1);
|
||||
assert_eq!(hex(&cpu.corpus), random_pin, "{gs:?}");
|
||||
let other = Corpus::Random { seed: 8, cases: 300 };
|
||||
let cpu_other = lanes::certify_corpus(gs, "local/test", other);
|
||||
for m in ms {
|
||||
for (corpus, cases, pin) in [(RANDOM_V1, 200, random_pin), (FABRIC_DEFAULT, 224, fabric_pin)] {
|
||||
let c = m.certify(gs, corpus).unwrap_or_else(|e| panic!("{} {gs:?} {}: {e}", m.key(), corpus.name()));
|
||||
assert_eq!(c.parity, Parity::BitExact, "{} {gs:?} {}", m.key(), corpus.name());
|
||||
assert_eq!(c.provenance, m.identity().provenance(), "earned in a simulator");
|
||||
assert_eq!(c.cases, cases);
|
||||
assert_eq!(hex(&c.corpus), pin, "{} {gs:?} {}", m.key(), corpus.name());
|
||||
assert_eq!((c.kernel.as_str(), c.implementation.as_str(), c.member.as_str()), (apply_kernel(gs), m.implementation(), m.id()));
|
||||
assert_eq!(c.generator, corpus.generator(gs));
|
||||
report(&format!(
|
||||
"fpga certified: key={} member={} kernel={} corpus={} implementation={} parity=bit-exact provenance={} cases={} digest={}",
|
||||
m.key(),
|
||||
m.id(),
|
||||
c.kernel,
|
||||
corpus.name(),
|
||||
c.implementation,
|
||||
c.provenance.label(),
|
||||
c.cases,
|
||||
hex(&c.corpus)
|
||||
));
|
||||
}
|
||||
// A corpus with no pin: the member and the CPU agree on it.
|
||||
let c = m.certify(gs, other).unwrap_or_else(|e| panic!("{} {gs:?} seed 8: {e}", m.key()));
|
||||
assert_eq!((c.parity, c.cases), (Parity::BitExact, 300), "{} {gs:?}", m.key());
|
||||
assert_eq!(c.corpus, cpu_other.corpus, "{} {gs:?}", m.key());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c4_wide_circuits_match_native() {
|
||||
for m in members() {
|
||||
let widest = m.max_qubits().min(22);
|
||||
let mut s = 0x0077_6964_6500_u64;
|
||||
for (k, n) in [12u8, 16, 18, 20, widest].into_iter().enumerate() {
|
||||
for gs in Gateset::ALL {
|
||||
let c = random_circuit(n, 40, 0x1000 + k as u64, gs);
|
||||
let start = (next(&mut s) as usize) % (1usize << n);
|
||||
let want = c.simulate_from(start).expect("the native run");
|
||||
let got = m.simulate_from(&c, start).unwrap_or_else(|e| panic!("{}: n = {n} {gs:?}: {e}", m.key()));
|
||||
assert!(got == want, "{}: n = {n} {gs:?}: the states differ", m.key());
|
||||
}
|
||||
}
|
||||
// The transforms the conformance corpus carries.
|
||||
for n in [3u8, 5, 8, 12] {
|
||||
let qft = Circuit::qft(n);
|
||||
assert!(m.simulate(&qft).unwrap() == qft.simulate().unwrap(), "{}: QFT-{n}", m.key());
|
||||
}
|
||||
report(&format!("fpga wide: key={} up to {widest} qubits match native", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c5_refusals_are_typed() {
|
||||
for m in members() {
|
||||
for c in refused_circuits() {
|
||||
let native = c.simulate_from(0).unwrap_err();
|
||||
assert_eq!(m.simulate_from(&c, 0).unwrap_err(), FpgaError::Circuit(native), "{c:?}");
|
||||
}
|
||||
assert_eq!(m.simulate(&Circuit::new(MAX_QUBITS + 1)).unwrap_err(), FpgaError::Circuit(QuantumError::TooManyQubits(MAX_QUBITS + 1)));
|
||||
let small = m.with_max_qubits(4);
|
||||
assert_eq!(small.simulate(&Circuit::new(5)).unwrap_err(), FpgaError::TooWide { n_qubits: 5, max: 4 });
|
||||
let wide = Program { n_qubits: 5, start: 0, gates: Vec::new(), steps: Vec::new() };
|
||||
assert_eq!(small.run_program(&wide).unwrap_err(), FpgaError::TooWide { n_qubits: 5, max: 4 });
|
||||
// A result that leaves 32 bits is reported at its op, and the lane
|
||||
// holds its low 32 bits.
|
||||
let r = m.run_lanes(&Z_STATE, &[z_words()]).unwrap();
|
||||
assert_eq!(r.out_of_range, Some(0), "{}", m.key());
|
||||
assert_eq!(r.amps[1][0] as u32, 0x8000_0000);
|
||||
// Malformed words, a state the host cannot lay out and a tile step are
|
||||
// host defects, never results.
|
||||
let bad = GateWords { tbit: 3, ..z_words() };
|
||||
assert!(matches!(m.run_lanes(&[[0, 0]; 4], &[bad]), Err(FpgaError::Internal(_))));
|
||||
assert!(matches!(m.run_lanes(&[[0, 0]; 3], &[z_words()]), Err(FpgaError::Internal(_))));
|
||||
let tiled = Program { n_qubits: 2, start: 0, gates: Vec::new(), steps: vec![Step::Tile { first: 0, count: 1, size: 2 }] };
|
||||
assert!(matches!(m.run_program(&tiled), Err(FpgaError::Internal(_))));
|
||||
// The refusals that may still fall back to another member, do.
|
||||
assert!(MemberError::from(FpgaError::TooWide { n_qubits: 5, max: 4 }).falls_back());
|
||||
report(&format!("fpga refusals: key={} typed", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a member running `mutant`'s text is caught by the mutant's
|
||||
/// detector.
|
||||
fn caught(mm: &FpgaSimMember, mutant: &Mutant, key: &str) -> bool {
|
||||
match mutant.detector {
|
||||
Detector::Probe => {
|
||||
let edges = lanes::probe_pairs(0, 0);
|
||||
let out = mm.probe(&edges).expect("the mutant's probe runs");
|
||||
let differing = edges.iter().zip(&out).filter(|&(&(a, b), o)| *o != (lanes::mul_wide(a, b), lanes::fxmul_wide(a, b))).count();
|
||||
println!("fpga mutant {} on {key}: {differing} of 484 edge pairs differ", mutant.name);
|
||||
differing >= 1
|
||||
}
|
||||
Detector::Certificate => Gateset::ALL.into_iter().all(|gs| match mm.certify(gs, RANDOM_V1) {
|
||||
Err(e) => {
|
||||
println!("fpga mutant {} on {key}: {gs:?} certification refused: {e}", mutant.name);
|
||||
true
|
||||
}
|
||||
Ok(c) => {
|
||||
let cpu = lanes::certify_corpus(gs, "local/test", RANDOM_V1);
|
||||
c.parity != Parity::BitExact || c.corpus != cpu.corpus
|
||||
}
|
||||
}),
|
||||
Detector::Range => mm.run_lanes(&Z_STATE, &[z_words()]).map(|r| r.out_of_range) != Ok(Some(0)),
|
||||
other => panic!("no FPGA mutant uses the {other:?} detector"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c6_mutants_are_killed() {
|
||||
assert_eq!(lanes::probe_pairs(0, 0).len(), 484);
|
||||
for m in members() {
|
||||
let mut killed = 0;
|
||||
for mutant in &MUTANTS {
|
||||
let mm = m.with_rtl(&fpga::mutate(mutant)).unwrap_or_else(|e| panic!("{}: mutant {} does not compile: {e}", m.key(), mutant.name));
|
||||
assert_ne!(mm.implementation(), m.implementation());
|
||||
assert!(caught(&mm, mutant, m.key()), "{}: mutant {} survived", m.key(), mutant.name);
|
||||
killed += 1;
|
||||
}
|
||||
assert_eq!(killed, MUTANTS.len());
|
||||
report(&format!("fpga mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c7_receipt_placement_and_the_fabric() {
|
||||
for m in members() {
|
||||
let member = m.member(FABRIC_DEFAULT).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
assert_eq!((member.id.as_str(), member.class, member.presence), (m.id(), m.class(), Presence::Detected));
|
||||
assert_eq!(member.kernels.len(), 2);
|
||||
for (entry, gs) in member.kernels.iter().zip(Gateset::ALL) {
|
||||
assert_eq!((entry.kernel.as_str(), entry.implementation.as_str()), (apply_kernel(gs), m.implementation()));
|
||||
assert!(entry.energy.is_none());
|
||||
assert_eq!(entry.certificate.parity, Parity::BitExact, "{gs:?}");
|
||||
}
|
||||
// A receipt over a member's run.
|
||||
let qft = Circuit::qft(5);
|
||||
let state = m.simulate(&qft).unwrap();
|
||||
assert_eq!(state, qft.simulate().unwrap());
|
||||
let entry = member.kernels.iter().find(|k| k.kernel == apply_kernel(qft.gateset)).unwrap();
|
||||
let signer = SigningKey::from_bytes(&[7; 32]);
|
||||
let receipt = FabricReceipt::seal(&signer, "local/test", state.statevector_hash(), &member, entry, 0, None);
|
||||
assert!(receipt.verify());
|
||||
assert!(receipt.certificate_matches(&entry.certificate));
|
||||
// Placement: beside the CPU reference with no energy figures, the
|
||||
// reference; alone, the member.
|
||||
let local = detect_local();
|
||||
for gs in Gateset::ALL {
|
||||
let both = [local.clone(), member.clone()];
|
||||
let (placed, k) = place(&both, apply_kernel(gs), &Parity::BitExact).unwrap();
|
||||
assert_eq!((placed.id.as_str(), k.implementation.as_str()), (local.id.as_str(), REFERENCE));
|
||||
let (placed, k) = place(std::slice::from_ref(&member), apply_kernel(gs), &Parity::BitExact).unwrap();
|
||||
assert_eq!((placed.id.as_str(), k.implementation.as_str()), (m.id(), m.implementation()));
|
||||
}
|
||||
// On the fabric: the member alone runs the circuit; a pairing with
|
||||
// another member's entries is refused.
|
||||
let mut fabric = Fabric::new();
|
||||
fabric.add(member.clone(), m);
|
||||
let run = ready(fabric.run(&qft, 0, &Parity::BitExact, &signer, "local/test")).expect("resolves").expect("runs");
|
||||
assert_eq!(run.state, state);
|
||||
assert_eq!(run.receipt.member_id, m.id());
|
||||
assert!(run.receipt.verify());
|
||||
let mut mismatched = Fabric::new();
|
||||
mismatched.add(local.clone(), m);
|
||||
assert!(ready(mismatched.run(&qft, 0, &Parity::BitExact, &signer, "local/test")).expect("resolves").is_err());
|
||||
let mut both = Fabric::new();
|
||||
both.add(local.clone(), CpuReference).add(member.clone(), m);
|
||||
let run = ready(both.run(&qft, 0, &Parity::BitExact, &signer, "local/test")).expect("resolves").expect("runs");
|
||||
assert_eq!(run.state, state);
|
||||
report(&format!("fpga receipt: key={} ok", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c8_repeated_runs_agree() {
|
||||
for m in members() {
|
||||
let c = random_circuit(16, 60, 0xabc, Gateset::V2);
|
||||
let first = m.simulate(&c).unwrap();
|
||||
for _ in 0..5 {
|
||||
assert!(m.simulate(&c).unwrap() == first, "{}", m.key());
|
||||
}
|
||||
report(&format!("fpga repeat: key={} ok", m.key()));
|
||||
}
|
||||
}
|
||||
429
wai/wai-quantum/tests/opencl_member.rs
Normal file
429
wai/wai-quantum/tests/opencl_member.rs
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
//! The fabric's OpenCL member on the devices of the machine that runs this.
|
||||
//!
|
||||
//! The machine declares the devices it must have in `WAI_QUANTUM_OPENCL_EXPECT`:
|
||||
//! their keys, comma-separated and sorted, or `none`. Every test that needs a
|
||||
//! member fails unless the devices found are exactly the declared ones, so a
|
||||
//! machine whose GPU or driver went missing, or a run that declared nothing,
|
||||
//! fails instead of passing over no member. Each test prints what it
|
||||
//! established and, when `WAI_QUANTUM_OPENCL_REPORT` names a file, appends the
|
||||
//! same lines to it, which a CI step can count per device.
|
||||
//!
|
||||
//! ```text
|
||||
//! WAI_QUANTUM_OPENCL_EXPECT=opencl-cpu-<vendor>,opencl-gpu-<vendor> \
|
||||
//! cargo test --features quantum_fabric_opencl --test opencl_member -- --test-threads=1
|
||||
//! ```
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::OnceLock;
|
||||
use wai_quantum::ed25519_dalek::SigningKey;
|
||||
use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset, MAX_QUBITS, QuantumError};
|
||||
use wai_quantum::quantum_fabric::opencl::{self, MUTANTS};
|
||||
use wai_quantum::quantum_fabric::lanes::{self, GateWords, Lane};
|
||||
use wai_quantum::quantum_fabric::plan::{Program, Step};
|
||||
use wai_quantum::quantum_fabric::wgsl::{Detector, Mutant};
|
||||
use wai_quantum::quantum_fabric::{
|
||||
Corpus, CpuReference, FABRIC_DEFAULT, Fabric, FabricClass, FabricReceipt, MemberError, Parity, Presence, REFERENCE, apply_kernel, detect_local, place, ready,
|
||||
};
|
||||
use wai_quantum::quantum_fabric_opencl::{Discovery, OpenClError, OpenClMember, discover};
|
||||
|
||||
/// The CPU's corpus digests: random-v1 (seed 7, 200 cases) and fabric-v1
|
||||
/// (seed 7, 200 + 24 cases), per gateset. A bit-exact member reproduces them.
|
||||
const PINS: [(Gateset, &str, &str); 2] = [
|
||||
(Gateset::V1, "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36", "41c726a0ac80f7defef92e15ecf893bdb7a3076b5e7f670fa0b7142e5c033df7"),
|
||||
(Gateset::V2, "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2", "9a84b55af68f1a8b07390b234cf562dd8d85cef3185a697ab85690c8790a7809"),
|
||||
];
|
||||
|
||||
const RANDOM_V1: Corpus = Corpus::Random { seed: 7, cases: 200 };
|
||||
|
||||
static DISCOVERY: OnceLock<Discovery> = OnceLock::new();
|
||||
|
||||
fn discovery() -> &'static Discovery {
|
||||
DISCOVERY.get_or_init(discover)
|
||||
}
|
||||
|
||||
/// Prints `line` and appends it to the file `WAI_QUANTUM_OPENCL_REPORT` names.
|
||||
fn report(line: &str) {
|
||||
println!("{line}");
|
||||
if let Some(path) = std::env::var_os("WAI_QUANTUM_OPENCL_REPORT") {
|
||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).expect("the report file opens");
|
||||
writeln!(f, "{line}").expect("the report line is written");
|
||||
}
|
||||
}
|
||||
|
||||
/// The declared device set, as written, if it is exactly what was found;
|
||||
/// otherwise why not, with what was found and refused and how to declare it.
|
||||
fn declared(d: &Discovery) -> Result<String, String> {
|
||||
let mut found: Vec<&str> = d.found.iter().map(|m| m.key()).collect();
|
||||
found.sort_unstable();
|
||||
let found_list = if found.is_empty() { "none".to_string() } else { found.join(",") };
|
||||
let refused: Vec<String> = d.refused.iter().map(|r| format!("{} ({:?}): {}", r.key, r.device, r.error)).collect();
|
||||
let refused_list = if refused.is_empty() { "none".to_string() } else { refused.join("; ") };
|
||||
let unavailable = d.unavailable.as_deref().unwrap_or("no");
|
||||
let how = format!("found: {found_list}; refused: {refused_list}; unavailable: {unavailable}. Declare the devices this machine must have: WAI_QUANTUM_OPENCL_EXPECT={found_list}");
|
||||
let expect = match std::env::var("WAI_QUANTUM_OPENCL_EXPECT") {
|
||||
Ok(v) if !v.is_empty() => v,
|
||||
_ => return Err(format!("WAI_QUANTUM_OPENCL_EXPECT is not set. {how}")),
|
||||
};
|
||||
let keys: Vec<&str> = if expect == "none" { Vec::new() } else { expect.split(',').collect() };
|
||||
let well_formed = keys.windows(2).all(|w| w[0] <= w[1]) && keys.iter().all(|k| !k.is_empty() && *k != "none" && k.trim() == *k);
|
||||
if !well_formed {
|
||||
return Err(format!("WAI_QUANTUM_OPENCL_EXPECT={expect:?} is not a sorted, comma-separated list of keys, nor none. {how}"));
|
||||
}
|
||||
for k in &keys {
|
||||
if !found.contains(k)
|
||||
&& let Some(r) = d.refused.iter().find(|r| r.key == *k)
|
||||
{
|
||||
return Err(format!("the declared device {k} was refused: {}. {how}", r.error));
|
||||
}
|
||||
}
|
||||
if found != keys {
|
||||
return Err(format!("the devices found are not the declared ones ({expect}). {how}"));
|
||||
}
|
||||
Ok(expect)
|
||||
}
|
||||
|
||||
/// The members, once the declared devices are exactly the ones found.
|
||||
fn members() -> &'static [OpenClMember] {
|
||||
let d = discovery();
|
||||
if let Err(why) = declared(d) {
|
||||
panic!("{why}");
|
||||
}
|
||||
&d.found
|
||||
}
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
fn next(s: &mut u64) -> u64 {
|
||||
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = *s;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
/// A value in `[-bound, bound]`.
|
||||
fn within(s: &mut u64, bound: i64) -> i32 {
|
||||
((next(s) % (2 * bound as u64 + 1)) as i64 - bound) as i32
|
||||
}
|
||||
|
||||
/// A random circuit of `ops` gates on `n` qubits: every base gate, up to three
|
||||
/// distinct controls, `P(1..=32)`.
|
||||
fn random_circuit(n: u8, ops: usize, seed: u64, gs: Gateset) -> Circuit {
|
||||
let bases = [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P];
|
||||
let mut s = seed;
|
||||
let mut c = Circuit::with_gateset(n, gs);
|
||||
for _ in 0..ops {
|
||||
let base = bases[(next(&mut s) % bases.len() as u64) as usize];
|
||||
let target = (next(&mut s) % u64::from(n)) as u8;
|
||||
let mut controls = Vec::new();
|
||||
for _ in 0..next(&mut s) % 4 {
|
||||
let q = (next(&mut s) % u64::from(n)) as u8;
|
||||
if q != target && !controls.contains(&q) {
|
||||
controls.push(q);
|
||||
}
|
||||
}
|
||||
let param = if base == BaseGate::P { 1 + (next(&mut s) % 32) as u16 } else { 0 };
|
||||
c.ops.push(Gate { base, controls, target, param });
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// Five circuits the native simulator refuses, in each gateset.
|
||||
fn refused_circuits() -> Vec<Circuit> {
|
||||
let gate = |base, controls: Vec<u8>, target, param| Gate { base, controls, target, param };
|
||||
let bad = [
|
||||
(1, vec![gate(BaseGate::H, vec![], 0, 0), gate(BaseGate::P, vec![], 0, 0)]),
|
||||
(1, vec![gate(BaseGate::P, vec![], 0, 33)]),
|
||||
(1, vec![gate(BaseGate::X, vec![], 3, 0)]),
|
||||
(2, vec![gate(BaseGate::X, vec![0], 0, 0)]),
|
||||
(2, vec![gate(BaseGate::X, vec![5], 1, 0)]),
|
||||
];
|
||||
let mut out = Vec::new();
|
||||
for gs in Gateset::ALL {
|
||||
for (n, ops) in bad.clone() {
|
||||
let mut c = Circuit::with_gateset(n, gs);
|
||||
c.ops = ops;
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `Z` on qubit 0: on `[(0, 0), (i32::MIN, 0)]` its exact result is `2^31`,
|
||||
/// which leaves 32 bits.
|
||||
fn z_words() -> GateWords {
|
||||
lanes::gate_words(&Gate { base: BaseGate::Z, controls: vec![], target: 0, param: 0 }, Gateset::V2)
|
||||
}
|
||||
|
||||
const Z_STATE: [Lane; 2] = [[0, 0], [i32::MIN, 0]];
|
||||
|
||||
#[test]
|
||||
fn c0_devices_are_declared() {
|
||||
let d = discovery();
|
||||
match declared(d) {
|
||||
Ok(expect) => report(&format!("opencl devices: as declared ({expect})")),
|
||||
Err(why) => panic!("{why}"),
|
||||
}
|
||||
for m in &d.found {
|
||||
assert!(matches!(m.class(), FabricClass::Gpu | FabricClass::Cpu), "{:?}", m.class());
|
||||
assert!(m.id().starts_with("local/opencl-"), "{}", m.id());
|
||||
assert_eq!(m.implementation(), opencl::implementation_id());
|
||||
assert_eq!(m.identity().member_id(), m.id());
|
||||
assert!(m.max_qubits() >= 20, "{}: holds only {} qubits", m.key(), m.max_qubits());
|
||||
report(&format!("opencl member: key={} id={} {}", m.key(), m.id(), m.detail()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c1_probe_matches_lanes() {
|
||||
for m in members() {
|
||||
let pairs = lanes::probe_pairs(1 << 16, 0x0cda);
|
||||
let out = m.probe(&pairs).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
assert_eq!(lanes::first_mismatch(&pairs, &out), None, "{}", m.key());
|
||||
report(&format!("opencl probe: key={} {} pairs agree", m.key(), pairs.len()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c2_kernel_matches_run_words() {
|
||||
for m in members() {
|
||||
let mut s = 0x6b65_726e_656c_u64;
|
||||
let (mut flagged, mut clean) = (0, 0);
|
||||
for case in 0..2000 {
|
||||
let n = 1 + (next(&mut s) % 12) as u32;
|
||||
// Three regimes: the whole 32-bit range (nearly always out of
|
||||
// range), small states and entries (never), and states and
|
||||
// entries up to 2^30 (either).
|
||||
let mode = case % 3;
|
||||
let value = |s: &mut u64| -> i32 {
|
||||
match mode {
|
||||
0 => next(s) as i32,
|
||||
1 => within(s, 1 << 22),
|
||||
_ => within(s, 1 << 30),
|
||||
}
|
||||
};
|
||||
let entry = |s: &mut u64| -> u32 {
|
||||
match mode {
|
||||
0 => next(s) as u32,
|
||||
1 => within(s, 1 << 29) as u32,
|
||||
_ => within(s, 1 << 30) as u32,
|
||||
}
|
||||
};
|
||||
let state: Vec<Lane> = (0..1usize << n).map(|_| [value(&mut s), value(&mut s)]).collect();
|
||||
let gates: Vec<GateWords> = (0..1 + next(&mut s) % 8)
|
||||
.map(|_| {
|
||||
let t = (next(&mut s) % u64::from(n)) as u32;
|
||||
let ctrl = (0..n).filter(|&b| b != t && next(&mut s).is_multiple_of(4)).fold(0u32, |acc, b| acc | 1 << b);
|
||||
let mut w = [0u32; 8];
|
||||
w.iter_mut().for_each(|x| *x = entry(&mut s));
|
||||
GateWords { m: w, tbit: 1 << t, ctrl }
|
||||
})
|
||||
.collect();
|
||||
let want = lanes::run_words(&state, &gates).expect("valid shapes");
|
||||
let got = m.run_lanes(&state, &gates).unwrap_or_else(|e| panic!("{}: case {case}: {e}", m.key()));
|
||||
assert_eq!(got.out_of_range, want.out_of_range, "{}: case {case}, n = {n}", m.key());
|
||||
assert!(got.amps == want.amps, "{}: case {case}, n = {n}: the lanes differ", m.key());
|
||||
if want.out_of_range.is_some() {
|
||||
flagged += 1;
|
||||
} else {
|
||||
clean += 1;
|
||||
}
|
||||
}
|
||||
assert!(flagged >= 100 && clean >= 100, "{}: {flagged} flagged, {clean} clean", m.key());
|
||||
report(&format!("opencl kernel: key={} ok", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c3_certificates_reproduce_the_pins() {
|
||||
let ms = members();
|
||||
for (gs, random_pin, fabric_pin) in PINS {
|
||||
let cpu = lanes::certify_corpus(gs, "local/test", RANDOM_V1);
|
||||
assert_eq!(hex(&cpu.corpus), random_pin, "{gs:?}");
|
||||
let other = Corpus::Random { seed: 8, cases: 300 };
|
||||
let cpu_other = lanes::certify_corpus(gs, "local/test", other);
|
||||
for m in ms {
|
||||
for (corpus, cases, pin) in [(RANDOM_V1, 200, random_pin), (FABRIC_DEFAULT, 224, fabric_pin)] {
|
||||
let c = m.certify(gs, corpus).unwrap_or_else(|e| panic!("{} {gs:?} {}: {e}", m.key(), corpus.name()));
|
||||
assert_eq!(c.parity, Parity::BitExact, "{} {gs:?} {}", m.key(), corpus.name());
|
||||
assert_eq!(c.cases, cases);
|
||||
assert_eq!(hex(&c.corpus), pin, "{} {gs:?} {}", m.key(), corpus.name());
|
||||
assert_eq!((c.kernel.as_str(), c.implementation.as_str(), c.member.as_str()), (apply_kernel(gs), m.implementation(), m.id()));
|
||||
assert_eq!(c.generator, corpus.generator(gs));
|
||||
report(&format!(
|
||||
"opencl certified: key={} member={} kernel={} corpus={} implementation={} parity=bit-exact cases={} digest={}",
|
||||
m.key(),
|
||||
m.id(),
|
||||
c.kernel,
|
||||
corpus.name(),
|
||||
c.implementation,
|
||||
c.cases,
|
||||
hex(&c.corpus)
|
||||
));
|
||||
}
|
||||
// A corpus with no pin: the member and the CPU agree on it.
|
||||
let c = m.certify(gs, other).unwrap_or_else(|e| panic!("{} {gs:?} seed 8: {e}", m.key()));
|
||||
assert_eq!((c.parity, c.cases), (Parity::BitExact, 300), "{} {gs:?}", m.key());
|
||||
assert_eq!(c.corpus, cpu_other.corpus, "{} {gs:?}", m.key());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c4_wide_circuits_match_native() {
|
||||
for m in members() {
|
||||
let widest = m.max_qubits().min(22);
|
||||
let mut s = 0x0077_6964_6500_u64;
|
||||
for (k, n) in [12u8, 16, 18, 20, widest].into_iter().enumerate() {
|
||||
for gs in Gateset::ALL {
|
||||
let c = random_circuit(n, 40, 0x1000 + k as u64, gs);
|
||||
let start = (next(&mut s) as usize) % (1usize << n);
|
||||
let want = c.simulate_from(start).expect("the native run");
|
||||
let got = m.simulate_from(&c, start).unwrap_or_else(|e| panic!("{}: n = {n} {gs:?}: {e}", m.key()));
|
||||
assert!(got == want, "{}: n = {n} {gs:?}: the states differ", m.key());
|
||||
}
|
||||
}
|
||||
// The transforms the conformance corpus carries.
|
||||
for n in [3u8, 5, 8, 12] {
|
||||
let qft = Circuit::qft(n);
|
||||
assert!(m.simulate(&qft).unwrap() == qft.simulate().unwrap(), "{}: QFT-{n}", m.key());
|
||||
}
|
||||
report(&format!("opencl wide: key={} up to {widest} qubits match native", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c5_refusals_are_typed() {
|
||||
for m in members() {
|
||||
for c in refused_circuits() {
|
||||
let native = c.simulate_from(0).unwrap_err();
|
||||
assert_eq!(m.simulate_from(&c, 0).unwrap_err(), OpenClError::Circuit(native), "{c:?}");
|
||||
}
|
||||
assert_eq!(m.simulate(&Circuit::new(MAX_QUBITS + 1)).unwrap_err(), OpenClError::Circuit(QuantumError::TooManyQubits(MAX_QUBITS + 1)));
|
||||
let small = m.with_max_qubits(4);
|
||||
assert_eq!(small.simulate(&Circuit::new(5)).unwrap_err(), OpenClError::TooWide { n_qubits: 5, max: 4 });
|
||||
let wide = Program { n_qubits: 5, start: 0, gates: Vec::new(), steps: Vec::new() };
|
||||
assert_eq!(small.run_program(&wide).unwrap_err(), OpenClError::TooWide { n_qubits: 5, max: 4 });
|
||||
// A result that leaves 32 bits is reported at its op, and the lane
|
||||
// holds its low 32 bits.
|
||||
let r = m.run_lanes(&Z_STATE, &[z_words()]).unwrap();
|
||||
assert_eq!(r.out_of_range, Some(0), "{}", m.key());
|
||||
assert_eq!(r.amps[1][0] as u32, 0x8000_0000);
|
||||
// Malformed words, a state the host cannot lay out and a tile step are
|
||||
// host defects, never results.
|
||||
let bad = GateWords { tbit: 3, ..z_words() };
|
||||
assert!(matches!(m.run_lanes(&[[0, 0]; 4], &[bad]), Err(OpenClError::Internal(_))));
|
||||
assert!(matches!(m.run_lanes(&[[0, 0]; 3], &[z_words()]), Err(OpenClError::Internal(_))));
|
||||
let tiled = Program { n_qubits: 2, start: 0, gates: Vec::new(), steps: vec![Step::Tile { first: 0, count: 1, size: 2 }] };
|
||||
assert!(matches!(m.run_program(&tiled), Err(OpenClError::Internal(_))));
|
||||
// The refusals that may still fall back to another member, do.
|
||||
assert!(MemberError::from(OpenClError::TooWide { n_qubits: 5, max: 4 }).falls_back());
|
||||
report(&format!("opencl refusals: key={} typed", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a member running `mutant`'s text is caught by the mutant's
|
||||
/// detector.
|
||||
fn caught(mm: &OpenClMember, mutant: &Mutant, key: &str) -> bool {
|
||||
match mutant.detector {
|
||||
Detector::Probe => {
|
||||
let edges = lanes::probe_pairs(0, 0);
|
||||
let out = mm.probe(&edges).expect("the mutant's probe runs");
|
||||
let differing = edges.iter().zip(&out).filter(|&(&(a, b), o)| *o != (lanes::mul_wide(a, b), lanes::fxmul_wide(a, b))).count();
|
||||
println!("opencl mutant {} on {key}: {differing} of 484 edge pairs differ", mutant.name);
|
||||
differing >= 1
|
||||
}
|
||||
Detector::Certificate => Gateset::ALL.into_iter().all(|gs| match mm.certify(gs, RANDOM_V1) {
|
||||
Err(e) => {
|
||||
println!("opencl mutant {} on {key}: {gs:?} certification refused: {e}", mutant.name);
|
||||
true
|
||||
}
|
||||
Ok(c) => {
|
||||
let cpu = lanes::certify_corpus(gs, "local/test", RANDOM_V1);
|
||||
c.parity != Parity::BitExact || c.corpus != cpu.corpus
|
||||
}
|
||||
}),
|
||||
Detector::Range => mm.run_lanes(&Z_STATE, &[z_words()]).map(|r| r.out_of_range) != Ok(Some(0)),
|
||||
other => panic!("no OpenCL mutant uses the {other:?} detector"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c6_mutants_are_killed() {
|
||||
assert_eq!(lanes::probe_pairs(0, 0).len(), 484);
|
||||
for m in members() {
|
||||
let mut killed = 0;
|
||||
for mutant in &MUTANTS {
|
||||
let mm = m.with_source(&opencl::mutate(mutant)).unwrap_or_else(|e| panic!("{}: mutant {} does not compile: {e}", m.key(), mutant.name));
|
||||
assert_ne!(mm.implementation(), m.implementation());
|
||||
assert!(caught(&mm, mutant, m.key()), "{}: mutant {} survived", m.key(), mutant.name);
|
||||
killed += 1;
|
||||
}
|
||||
assert_eq!(killed, MUTANTS.len());
|
||||
report(&format!("opencl mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c7_receipt_placement_and_the_fabric() {
|
||||
for m in members() {
|
||||
let member = m.member(FABRIC_DEFAULT).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
assert_eq!((member.id.as_str(), member.class, member.presence), (m.id(), m.class(), Presence::Detected));
|
||||
assert_eq!(member.kernels.len(), 2);
|
||||
for (entry, gs) in member.kernels.iter().zip(Gateset::ALL) {
|
||||
assert_eq!((entry.kernel.as_str(), entry.implementation.as_str()), (apply_kernel(gs), m.implementation()));
|
||||
assert!(entry.energy.is_none());
|
||||
assert_eq!(entry.certificate.parity, Parity::BitExact, "{gs:?}");
|
||||
}
|
||||
// A receipt over a member's run.
|
||||
let qft = Circuit::qft(5);
|
||||
let state = m.simulate(&qft).unwrap();
|
||||
assert_eq!(state, qft.simulate().unwrap());
|
||||
let entry = member.kernels.iter().find(|k| k.kernel == apply_kernel(qft.gateset)).unwrap();
|
||||
let signer = SigningKey::from_bytes(&[7; 32]);
|
||||
let receipt = FabricReceipt::seal(&signer, "local/test", state.statevector_hash(), &member, entry, 0, None);
|
||||
assert!(receipt.verify());
|
||||
assert!(receipt.certificate_matches(&entry.certificate));
|
||||
// Placement: beside the CPU reference with no energy figures, the
|
||||
// reference; alone, the member.
|
||||
let local = detect_local();
|
||||
for gs in Gateset::ALL {
|
||||
let both = [local.clone(), member.clone()];
|
||||
let (placed, k) = place(&both, apply_kernel(gs), &Parity::BitExact).unwrap();
|
||||
assert_eq!((placed.id.as_str(), k.implementation.as_str()), (local.id.as_str(), REFERENCE));
|
||||
let (placed, k) = place(std::slice::from_ref(&member), apply_kernel(gs), &Parity::BitExact).unwrap();
|
||||
assert_eq!((placed.id.as_str(), k.implementation.as_str()), (m.id(), m.implementation()));
|
||||
}
|
||||
// On the fabric: the member alone runs the circuit; a pairing with
|
||||
// another member's entries is refused.
|
||||
let mut fabric = Fabric::new();
|
||||
fabric.add(member.clone(), m);
|
||||
let run = ready(fabric.run(&qft, 0, &Parity::BitExact, &signer, "local/test")).expect("resolves").expect("runs");
|
||||
assert_eq!(run.state, state);
|
||||
assert_eq!(run.receipt.member_id, m.id());
|
||||
assert!(run.receipt.verify());
|
||||
let mut mismatched = Fabric::new();
|
||||
mismatched.add(local.clone(), m);
|
||||
assert!(ready(mismatched.run(&qft, 0, &Parity::BitExact, &signer, "local/test")).expect("resolves").is_err());
|
||||
let mut both = Fabric::new();
|
||||
both.add(local.clone(), CpuReference).add(member.clone(), m);
|
||||
let run = ready(both.run(&qft, 0, &Parity::BitExact, &signer, "local/test")).expect("resolves").expect("runs");
|
||||
assert_eq!(run.state, state);
|
||||
report(&format!("opencl receipt: key={} ok", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c8_repeated_runs_agree() {
|
||||
for m in members() {
|
||||
let c = random_circuit(16, 60, 0xabc, Gateset::V2);
|
||||
let first = m.simulate(&c).unwrap();
|
||||
for _ in 0..5 {
|
||||
assert!(m.simulate(&c).unwrap() == first, "{}", m.key());
|
||||
}
|
||||
report(&format!("opencl repeat: key={} ok", m.key()));
|
||||
}
|
||||
}
|
||||
118
wai/wai-quantum/tests/opencl_sv.rs
Normal file
118
wai/wai-quantum/tests/opencl_sv.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
//! The dense f64 state vector on the OpenCL devices of the machine that runs
|
||||
//! this: bit for bit the CPU's, or the certificate says not. A device without
|
||||
//! binary64 refuses it, and says so.
|
||||
//!
|
||||
//! The machine declares its devices in `WAI_QUANTUM_OPENCL_EXPECT` (comma-
|
||||
//! separated keys, or `none`), as for `opencl_member`; `WAI_QUANTUM_OPENCL_REPORT`
|
||||
//! names a file the established lines are appended to.
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use std::io::Write;
|
||||
use wai_quantum::gates::Gate;
|
||||
use wai_quantum::quantum_fabric::sv64::{self, MUTANTS, SV_DEFAULT, SV_DEFAULT_PIN, SvCorpus};
|
||||
use wai_quantum::quantum_fabric::{Certification, Parity};
|
||||
use wai_quantum::quantum_fabric_opencl::{OpenClError, OpenClMember, discover};
|
||||
use wai_quantum::quantum_sv::StateVector;
|
||||
|
||||
fn report(line: &str) {
|
||||
println!("{line}");
|
||||
if let Some(path) = std::env::var_os("WAI_QUANTUM_OPENCL_REPORT") {
|
||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).expect("the report file opens");
|
||||
writeln!(f, "{line}").expect("the report line is written");
|
||||
}
|
||||
}
|
||||
|
||||
fn members() -> Vec<OpenClMember> {
|
||||
let d = discover();
|
||||
let mut found: Vec<String> = d.found.iter().map(|m| m.key().to_string()).collect();
|
||||
found.sort();
|
||||
let found_list = if found.is_empty() { "none".to_string() } else { found.join(",") };
|
||||
let expect = std::env::var("WAI_QUANTUM_OPENCL_EXPECT").unwrap_or_else(|_| panic!("WAI_QUANTUM_OPENCL_EXPECT is not set; found {found_list}"));
|
||||
assert_eq!(found_list, expect, "the devices found are not the declared ones");
|
||||
d.found
|
||||
}
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
/// The CPU certifying itself on `corpus`: the digest a bit-exact device
|
||||
/// reproduces.
|
||||
fn cpu_digest(corpus: SvCorpus) -> String {
|
||||
let mut cert = Certification::new();
|
||||
for case in corpus.cases() {
|
||||
let r = case.reference().unwrap();
|
||||
cert.add(&case.bytes(), &r, &r);
|
||||
}
|
||||
hex(&cert.finish(sv64::KERNEL_SV_RUN, "cpu", "cpu", &corpus.generator()).corpus)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s0_the_state_vector_is_the_cpus_bit_for_bit() {
|
||||
let cpu = cpu_digest(SV_DEFAULT);
|
||||
assert_eq!(cpu, SV_DEFAULT_PIN, "this machine's CPU does not reproduce the pin");
|
||||
for m in members() {
|
||||
if !m.has_binary64() {
|
||||
assert!(matches!(m.sv(), Err(OpenClError::Unsupported(_))), "{}", m.key());
|
||||
report(&format!("opencl sv refused: key={} the device has no binary64", m.key()));
|
||||
continue;
|
||||
}
|
||||
let sv = m.sv().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
let c = sv.certify(SV_DEFAULT).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
|
||||
assert_eq!(c.parity, Parity::BitExact, "{}", m.key());
|
||||
assert_eq!(c.cases, SV_DEFAULT.cases as u64);
|
||||
assert_eq!(hex(&c.corpus), cpu, "{}: the digest is not the CPU's", m.key());
|
||||
report(&format!("opencl sv certified: key={} member={} kernel={} implementation={} parity=bit-exact cases={} digest={}", m.key(), m.id(), c.kernel, c.implementation, c.cases, hex(&c.corpus)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s1_wide_states_match_the_cpu() {
|
||||
for m in members().into_iter().filter(|m| m.has_binary64()) {
|
||||
let sv = m.sv().unwrap();
|
||||
for (n, width) in [(16u32, 0usize), (20, 3)] {
|
||||
let mut gates = Vec::new();
|
||||
let mut a = 0.1f64;
|
||||
for layer in 0..6u32 {
|
||||
for q in 0..n {
|
||||
a += 0.731;
|
||||
gates.push(match (q + layer) % 5 {
|
||||
0 => Gate::rx(q, a),
|
||||
1 => Gate::ry(q, a),
|
||||
2 => Gate::rz(q, a),
|
||||
3 => Gate::sqrt_w(q),
|
||||
_ => Gate::t(q),
|
||||
});
|
||||
}
|
||||
let mut q = layer % 2;
|
||||
while q + 1 < n {
|
||||
gates.push(if q % 3 == 0 { Gate::fsim(q, q + 1, a, 0.5 * a) } else { Gate::cz(q, q + 1) });
|
||||
q += 2;
|
||||
}
|
||||
gates.push(Gate::iswap(0, n - 1));
|
||||
}
|
||||
let mut cpu = StateVector::zero(n).unwrap();
|
||||
cpu.run(&gates, width).unwrap();
|
||||
let (re, im) = sv.run(n, &gates, width).unwrap();
|
||||
let same = |a: &[f64], b: &[f64]| a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits());
|
||||
assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: n={n} width={width}", m.key());
|
||||
}
|
||||
report(&format!("opencl sv wide: key={} 16 and 20 qubits match the CPU bit for bit", m.key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s2_mutants_are_caught() {
|
||||
let small = SvCorpus { cases: 18, ..SV_DEFAULT };
|
||||
let cpu = cpu_digest(small);
|
||||
for m in members().into_iter().filter(|m| m.has_binary64()) {
|
||||
let mut killed = 0;
|
||||
for mutant in &MUTANTS {
|
||||
let sv = m.sv_with_source(&sv64::mutate_cl(mutant)).unwrap_or_else(|e| panic!("mutant {}: {e}", mutant.name));
|
||||
let c = sv.certify(small).unwrap();
|
||||
assert!(c.parity != Parity::BitExact || hex(&c.corpus) != cpu, "{}: mutant {} survived", m.key(), mutant.name);
|
||||
killed += 1;
|
||||
}
|
||||
report(&format!("opencl sv mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len()));
|
||||
}
|
||||
}
|
||||
2
wai/wai-rs/Cargo.lock
generated
2
wai/wai-rs/Cargo.lock
generated
|
|
@ -4300,7 +4300,7 @@ version = "0.1.0"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ curve25519-dalek = { version = "4", optional = true }
|
|||
# ---- quantum (optional, behind the `quantum*` features) ----
|
||||
# The quantum stack now lives in its own dependency-light crate so it can be used
|
||||
# without the media/codec chain. `wai` re-exports it unchanged.
|
||||
wai-quantum = { version = "0.4.1", path = "../wai-quantum", optional = true, default-features = false }
|
||||
wai-quantum = { version = "0.5.0", path = "../wai-quantum", optional = true, default-features = false }
|
||||
|
||||
# ---- the integer operator set (behind `neural_int`) ----
|
||||
# `wai-ops/1` (extensions/integer-operators.md): an integer decoder that decodes
|
||||
|
|
|
|||
2
wai/wai-web/Cargo.lock
generated
2
wai/wai-web/Cargo.lock
generated
|
|
@ -1231,7 +1231,7 @@ version = "0.1.0"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.4"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
Loading…
Reference in a new issue