diff --git a/.github/workflows/wai-quantum-cuda.yml b/.github/workflows/wai-quantum-cuda.yml index 76f291f16..4a7fc37e2 100644 --- a/.github/workflows/wai-quantum-cuda.yml +++ b/.github/workflows/wai-quantum-cuda.yml @@ -4,19 +4,27 @@ name: wai-quantum CUDA member # each device's own architecture and certified bit-exact against the CPU # reference's pinned corpus digests. # -# - cuda-hw (jetson-hub, the RTX 4050): the device tests must find exactly the +# - cuda-hw, on each host with a CUDA GPU (jetson-hub's laptop GPU, compute +# capability 8.9, on x86-64; thor's integrated GPU, compute capability 11.0, +# on aarch64 through CUDA 13.2): 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; # the tensor-core kernel, every product formed on the tensor cores from 8-bit # limbs, must do all of the same under its own implementation id, with its -# own mutants caught; 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; and jobs placed on the fabric's members found there (the GPU -# by CUDA and by OpenCL, the CPU by OpenCL) must give the CPU's answers byte +# own mutants caught; and the dense f64 state vector, run resident (the state +# on the device for the whole run), must come back with the CPU's bits on its +# pinned corpus, on the wider kernels' own corpus (or say it did not), at 16 +# and 20 qubits and on random circuits from |0...0> and from a given state, +# every state-vector mutant caught; and sparse Pauli dynamics with its sum of +# terms on the GPU must give the CPU's results and final tables bit for bit +# on its pinned corpus and on large sums, every mutant caught; and jobs placed on the fabric's members found there (the GPU +# by CUDA and by OpenCL, the CPU by OpenCL; on thor OpenCL is PoCL's) must give the CPU's answers byte # for byte, asked for by key and by `accelerator`, alone and with one state # split four ways over them and the CPU reference, a state past a member -# refused, and auto keeping every state on the CPU. +# refused, a job of wider gates placed only where the wider kernels +# certified, a pauli job on the GPU, and auto keeping every state on the +# CPU, as the committed measurements say for this host. # - 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`; and placement with no device, the @@ -60,13 +68,37 @@ jobs: echo "run=$r" >> "$GITHUB_OUTPUT" cuda-hw: - name: member · CUDA (jetson-hub) + name: member · CUDA (${{ matrix.host }}) needs: relevant if: needs.relevant.outputs.run == 'true' - runs-on: linux-hw + runs-on: ${{ matrix.runner }} timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + # Each host's devices, declared here: its CUDA GPU, and the fabric's + # members there (sorted), with one state split over them and the CPU + # reference. + - host: jetson-hub + runner: linux-hw + cuda: gpu-cuda-sm89 + fabric: gpu-cuda-sm89,opencl-cpu-8086,opencl-gpu-10de + split: gpu-cuda-sm89,opencl-gpu-10de,opencl-cpu-8086,cpu + pocl_arch: '' + # PoCL's CUDA device compiles to PTX for sm_90 (its LLVM 18 has no + # sm_110), which the driver compiles for the GPU. + - host: thor + runner: linux-arm64-hw + cuda: gpu-cuda-sm110 + fabric: gpu-cuda-sm110,opencl-cpu-13b5,opencl-gpu-10de + split: gpu-cuda-sm110,opencl-gpu-10de,opencl-cpu-13b5,cpu + pocl_arch: sm_90 env: - WAI_QUANTUM_CUDA_EXPECT: gpu-cuda-sm89 + WAI_QUANTUM_CUDA_EXPECT: ${{ matrix.cuda }} + WAI_QUANTUM_HW_FABRIC_KEYS: ${{ matrix.fabric }} + WAI_QUANTUM_HW_FABRIC_SPLIT: ${{ matrix.split }} + POCL_CUDA_GPU_ARCH: ${{ matrix.pocl_arch }} CARGO_PROFILE_DEV_DEBUG: '0' CARGO_BUILD_JOBS: '8' steps: @@ -133,11 +165,28 @@ jobs: 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; } + grep -qE '^test result: ok\. 4 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 -q "^cuda sv wide certified: key=$key .* kernel=wai.quantum.sv/run-wide .* parity=bit-exact cases=120 digest=093614f2386ec5120ab6ccde5187a544518432751591535a6f58c7a44061ebaf$" "$report" || { echo "::error::the wider kernels are 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; } + grep -qx "cuda sv random: key=$key 140 resident runs on every kernel match the CPU bit for bit" "$report" || { echo "::error::a random circuit's resident run is not the CPU's"; exit 1; } + - name: Sparse Pauli dynamics on the GPU, bit for bit, its table included + shell: bash + working-directory: src/wai/wai-quantum + run: | + set -euo pipefail + export PATH="$HOME/.cargo/bin:$PATH" + report="$RUNNER_TEMP/cuda-spd-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_spd --test cuda_spd -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/spd.log" + grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/spd.log" || { echo "::error::not every propagation test ran and passed"; exit 1; } + cat "$report" + key="$WAI_QUANTUM_CUDA_EXPECT" + grep -q "^cuda spd certified: key=$key .* kernel=wai.quantum.spd/propagate implementation=cuda-spd/[0-9a-f]\{16\} parity=bit-exact cases=40 digest=25737ef1c61f056c0830404c64dcb06d5355ea784bd15bebbc201500ae85bb8b$" "$report" || { echo "::error::the propagation is not the CPU's bit for bit"; exit 1; } + grep -q "^cuda spd large: key=$key .* match the CPU bit for bit$" "$report" || { echo "::error::a large sum is not the CPU's"; exit 1; } + grep -qx "cuda spd mutants: key=$key killed 6 of 6" "$report" || { echo "::error::a propagation mutant survived"; exit 1; } - name: Jobs placed on the fabric's members, and a state split over several devices shell: bash working-directory: src/wai/wai-quantum @@ -155,18 +204,22 @@ jobs: keys="${WAI_QUANTUM_HW_FABRIC_KEYS:-gpu-cuda-sm89,opencl-cpu-8086,opencl-gpu-10de}" split="${WAI_QUANTUM_HW_FABRIC_SPLIT:-gpu-cuda-sm89,opencl-gpu-10de,opencl-cpu-8086,cpu}" report="$RUNNER_TEMP/fabric-report.txt"; : > "$report" - WAI_QUANTUM_FABRIC_EXPECT="$keys" WAI_QUANTUM_FABRIC_SPLIT="$split" WAI_QUANTUM_FABRIC_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-cuda" nice -n 10 \ + WAI_QUANTUM_FABRIC_EXPECT="$keys" WAI_QUANTUM_FABRIC_SPLIT="$split" WAI_QUANTUM_FABRIC_PAULI_EXPECT="$WAI_QUANTUM_CUDA_EXPECT" WAI_QUANTUM_FABRIC_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-cuda" nice -n 10 \ cargo test --locked --release --features quantum_engine,quantum_fabric,quantum_fabric_cuda,quantum_fabric_opencl --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log" - grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::not every placement test ran and passed"; exit 1; } + grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::not every placement test ran and passed"; exit 1; } cat "$report" grep -qx "fabric members: as declared ($keys)" "$report" || { echo "::error::the members placement found are not the declared ones"; exit 1; } for key in ${keys//,/ }; do grep -q "^fabric placed: key=$key .* give the CPU's answers byte for byte$" "$report" || { echo "::error::$key: a placed job's answers are not the CPU's"; exit 1; } grep -q "^fabric refused: key=$key " "$report" || { echo "::error::$key: a state past what it holds was not refused"; exit 1; } + grep -qE "^fabric wide: key=$key (placed at fusion 5, the CPU's answers byte for byte|refused at fusion 5: its wider kernels did not certify bit-exact here)$" "$report" || { echo "::error::$key: a job of wider gates was neither placed with the CPU's answers nor refused"; exit 1; } done + grep -qx "fabric wide: key=$WAI_QUANTUM_CUDA_EXPECT placed at fusion 5, the CPU's answers byte for byte" "$report" || { echo "::error::the GPU's wider kernels did not run a job of wider gates"; exit 1; } grep -qx "fabric split: ${split//,/+} over 14 and 19 qubits gives the CPU's answers byte for byte" "$report" || { echo "::error::the state split over the devices is not the CPU's"; exit 1; } - grep -qx "fabric auto: every state on the CPU, its result unnamed" "$report" || { echo "::error::auto placed a state on a member"; exit 1; } + # The measurements show no member here faster (engine::fabric_measured). + grep -qx "fabric auto: no member here measured faster; every state on the CPU, its result unnamed" "$report" || { echo "::error::auto did not follow the measurements"; exit 1; } grep -q "^fabric accelerator: .* the CPU's answers byte for byte" "$report" || { echo "::error::an accelerator request placed no state"; exit 1; } + grep -qE "^fabric pauli: key=$WAI_QUANTUM_CUDA_EXPECT 60 qubits, up to [0-9]+ terms, the CPU's answers byte for byte$" "$report" || { echo "::error::a pauli job on the GPU did not give the CPU's answers"; exit 1; } cuda-absent: name: member · CUDA absent (linux) @@ -200,26 +253,44 @@ jobs: grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/tensor.log" || { echo "::error::the tensor-core tests did not run over no device"; exit 1; } grep -qx 'cuda tensor devices: as declared (none)' "$report" || { echo "::error::the tensor-core 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; } - WAI_QUANTUM_FABRIC_EXPECT=none WAI_QUANTUM_FABRIC_REPORT="$report" cargo test --locked --features quantum_engine,quantum_fabric,quantum_fabric_cuda --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log" - grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::the placement tests did not run over no member"; exit 1; } + grep -qE '^test result: ok\. 4 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::the state-vector tests did not run over no device"; exit 1; } + cargo test --locked --features quantum_fabric_cuda,quantum_spd --test cuda_spd -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/spd.log" + grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/spd.log" || { echo "::error::the propagation tests did not run over no device"; exit 1; } + WAI_QUANTUM_FABRIC_EXPECT=none WAI_QUANTUM_FABRIC_PAULI_EXPECT=none WAI_QUANTUM_FABRIC_REPORT="$report" cargo test --locked --features quantum_engine,quantum_fabric,quantum_fabric_cuda --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log" + grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::the placement tests did not run over no member"; exit 1; } + grep -qx 'fabric pauli: no member here certified for it; accelerator refused' "$report" || { echo "::error::a pauli job was placed with no member"; exit 1; } grep -qx 'fabric members: as declared (none)' "$report" || { echo "::error::the placement's absent case was not established"; exit 1; } # The placement and the split with no device: CPU partitions bit for bit the # CPU's state vector, exchanges included, and every refusal a host gives; - # each test required by name. + # the rule auto places by; the propagation's protocol, run on the host, + # the CPU's bit for bit; each test required by name. - name: Placement and the split, on CPU partitions shell: bash working-directory: wai/wai-quantum run: | set -euo pipefail - cargo test --locked --features full --lib -- engine::fabric:: quantum_fabric::sv64:: 2>&1 | tee "$RUNNER_TEMP/split.log" + cargo test --locked --features full --lib -- engine::fabric:: engine::fabric_measured:: quantum_fabric::sv64:: quantum_fabric::spd:: quantum_spd::device:: 2>&1 | tee "$RUNNER_TEMP/split.log" for t in quantum_fabric::sv64::tests::a_partitioned_run_is_the_cpu_run_bit_for_bit \ + quantum_fabric::sv64::tests::a_launch_table_holds_every_launch_as_it_was_planned \ + quantum_fabric::sv64::tests::each_corpus_certifies_the_kernels_it_names \ + quantum_fabric::sv64::tests::the_cpu_reproduces_the_pinned_digest \ quantum_fabric::sv64::tests::exchanges_and_remaps_move_amplitudes_and_compute_nothing \ quantum_fabric::sv64::tests::a_split_refuses_what_it_cannot_hold \ engine::fabric::tests::a_job_s_fabric_reads_or_is_refused_with_its_code \ engine::fabric::tests::a_placement_changes_where_the_state_runs_and_not_a_byte_of_the_answers \ engine::fabric::tests::a_placement_this_host_cannot_honour_is_refused_never_made_elsewhere \ - engine::fabric::tests::a_placed_result_and_its_provenance_follow_their_schemas; do + engine::fabric::tests::a_placed_result_and_its_provenance_follow_their_schemas \ + engine::fabric_measured::tests::every_row_names_a_measured_host_and_its_spread_is_ordered \ + engine::fabric_measured::tests::the_rules_are_the_rows_faster_tails \ + engine::fabric_measured::tests::a_faster_tail_is_found_on_rows_of_any_shape \ + engine::fabric_measured::tests::the_measured_circuit_reads_and_its_job_plans \ + engine::fabric::tests::a_pauli_job_is_placed_as_asked_and_refused_what_the_host_cannot_honour \ + quantum_fabric::spd::tests::the_implementation_id_follows_the_kernel_and_every_mutant_edits_once \ + quantum_spd::device::tests::the_protocol_on_the_host_is_the_cpu_bit_for_bit \ + quantum_spd::device::tests::the_corpus_reaches_every_decision_of_the_term_set \ + quantum_spd::device::tests::the_cpu_reproduces_the_pinned_digest \ + quantum_spd::device::tests::the_encoded_gates_and_the_table_are_the_cpus_conjugations \ + quantum_spd::device::tests::a_device_s_refusals_are_the_cpus; do grep -qx "test $t ... ok" "$RUNNER_TEMP/split.log" || { echo "::error::$t did not run and pass"; exit 1; } done grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/split.log" || { echo "::error::a test failed"; exit 1; } diff --git a/.github/workflows/wai-quantum-hip.yml b/.github/workflows/wai-quantum-hip.yml index 7bea24f53..31d17c8b1 100644 --- a/.github/workflows/wai-quantum-hip.yml +++ b/.github/workflows/wai-quantum-hip.yml @@ -11,12 +11,16 @@ name: wai-quantum HIP member # kernel mutant; the matrix-instruction kernel, every product formed by the # GPU's bf16 matrix instruction from offset-binary 8-bit limbs, must do all # of the same under its own implementation id, with its own mutants caught; -# 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; -# and jobs placed on the fabric's members found there (the GPU by HIP and by -# OpenCL) must give the CPU's answers byte for byte, asked for by key and by -# `accelerator`, alone and with one state split over them, a state past a -# member refused, and auto keeping every state on the CPU. +# and the dense f64 state vector, run resident (the state on the device for +# the whole run), must come back with the CPU's bits on its pinned corpus, +# on the wider kernels' own corpus, at 16 and 20 qubits and on random +# circuits from |0...0> and from a given state, every state-vector mutant +# caught; and jobs placed on the fabric's members found there (the GPU by +# HIP and by OpenCL) must give the CPU's answers byte for byte, asked for by +# key and by `accelerator`, alone and with one state split over them, a +# state past a member refused, a job of wider gates placed, and auto +# placing a state on the GPU where the committed measurements say it is +# faster here, and on the CPU elsewhere. # - hip-absent (a container with no GPU and no HIP libraries): the same build # finds no member and says why, rather than failing; the device tests run over # no member with the declaration `none`. @@ -135,10 +139,12 @@ jobs: report="$RUNNER_TEMP/hip-sv-report.txt"; : > "$report" WAI_QUANTUM_HIP_EXPECT="$key" WAI_QUANTUM_HIP_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-hip" nice -n 10 \ cargo test --locked --release --features quantum_fabric_hip,quantum_sv --test hip_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 -qE '^test result: ok\. 4 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::not every state-vector test ran and passed"; exit 1; } cat "$report" grep -q "^hip 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 -q "^hip sv wide certified: key=$key .* kernel=wai.quantum.sv/run-wide .* parity=bit-exact cases=120 digest=093614f2386ec5120ab6ccde5187a544518432751591535a6f58c7a44061ebaf$" "$report" || { echo "::error::the wider kernels are not the CPU's bit for bit"; exit 1; } grep -qx "hip sv mutants: key=$key killed 4 of 4" "$report" || { echo "::error::a state-vector mutant survived"; exit 1; } + grep -qx "hip sv random: key=$key 140 resident runs on every kernel match the CPU bit for bit" "$report" || { echo "::error::a random circuit's resident run is not the CPU's"; exit 1; } - name: Jobs placed on the fabric's members, and a state split over them shell: bash working-directory: src/wai/wai-quantum @@ -149,18 +155,30 @@ jobs: # declared here or by the runner; one state is split over them. keys="${WAI_QUANTUM_AMD_FABRIC_KEYS:-gpu-hip-gfx1151,opencl-gpu-1002}" report="$RUNNER_TEMP/fabric-report.txt"; : > "$report" - WAI_QUANTUM_FABRIC_EXPECT="$keys" WAI_QUANTUM_FABRIC_SPLIT="$keys" WAI_QUANTUM_FABRIC_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-hip" nice -n 10 \ + WAI_QUANTUM_FABRIC_EXPECT="$keys" WAI_QUANTUM_FABRIC_SPLIT="$keys" WAI_QUANTUM_FABRIC_PAULI_EXPECT=none WAI_QUANTUM_FABRIC_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-hip" nice -n 10 \ cargo test --locked --release --features quantum_engine,quantum_fabric,quantum_fabric_hip,quantum_fabric_opencl --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log" - grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::not every placement test ran and passed"; exit 1; } + grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::not every placement test ran and passed"; exit 1; } cat "$report" grep -qx "fabric members: as declared ($keys)" "$report" || { echo "::error::the members placement found are not the declared ones"; exit 1; } for key in ${keys//,/ }; do grep -q "^fabric placed: key=$key .* give the CPU's answers byte for byte$" "$report" || { echo "::error::$key: a placed job's answers are not the CPU's"; exit 1; } grep -q "^fabric refused: key=$key " "$report" || { echo "::error::$key: a state past what it holds was not refused"; exit 1; } + grep -qx "fabric wide: key=$key placed at fusion 5, the CPU's answers byte for byte" "$report" || { echo "::error::$key: a job of wider gates did not run with the CPU's answers"; exit 1; } done grep -qx "fabric split: ${keys//,/+} over 14 and 19 qubits gives the CPU's answers byte for byte" "$report" || { echo "::error::the state split over the members is not the CPU's"; exit 1; } - grep -qx "fabric auto: every state on the CPU, its result unnamed" "$report" || { echo "::error::auto placed a state on a member"; exit 1; } + # The measurements show the integrated GPU faster here from 18 qubits at + # fusion 2 and 3 (engine::fabric_measured): auto places the measured job + # on it, and keeps a shallower one on the CPU. + # (A runner whose processor is not the one measured, by its model or its + # thread count, matches no row, and auto keeps every state on its CPU.) + if [ "$(grep -c '^fabric auto host: .* measured=true$' "$report")" = 1 ]; then + grep -qE "^fabric auto: by measurement, 18 qubits at fusion 2 on (gpu-hip-gfx1151|opencl-gpu-1002), the CPU's answers byte for byte; a shallower job on the CPU$" "$report" || { echo "::error::auto did not follow the measurements"; exit 1; } + else + grep -qx "fabric auto: no member here measured faster; every state on the CPU, its result unnamed" "$report" || { echo "::error::auto placed a state on a host not measured"; exit 1; } + fi grep -q "^fabric accelerator: .* the CPU's answers byte for byte" "$report" || { echo "::error::an accelerator request placed no state"; exit 1; } + # The pauli method's members are CUDA's alone: none here, and it says so. + grep -qx "fabric pauli: no member here certified for it; accelerator refused" "$report" || { echo "::error::a pauli job was placed with no member for it"; exit 1; } hip-absent: name: member · HIP absent (linux-arm64) @@ -194,7 +212,7 @@ jobs: grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/wmma.log" || { echo "::error::the matrix-instruction tests did not run over no device"; exit 1; } grep -qx 'hip wmma devices: as declared (none)' "$report" || { echo "::error::the matrix-instruction absent case was not established"; exit 1; } cargo test --locked --features quantum_fabric_hip,quantum_sv --test hip_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; } + grep -qE '^test result: ok\. 4 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::the state-vector tests did not run over no device"; exit 1; } WAI_QUANTUM_FABRIC_EXPECT=none WAI_QUANTUM_FABRIC_REPORT="$report" cargo test --locked --features quantum_engine,quantum_fabric,quantum_fabric_hip --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log" - grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::the placement tests did not run over no member"; exit 1; } + grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::the placement tests did not run over no member"; exit 1; } grep -qx 'fabric members: as declared (none)' "$report" || { echo "::error::the placement's absent case was not established"; exit 1; } diff --git a/.github/workflows/wai-quantum-opencl.yml b/.github/workflows/wai-quantum-opencl.yml index 890f66452..026b12e40 100644 --- a/.github/workflows/wai-quantum-opencl.yml +++ b/.github/workflows/wai-quantum-opencl.yml @@ -4,14 +4,20 @@ name: wai-quantum OpenCL member # 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), each by a key that carries the device's -# vendor id (opencl--). The device +# - opencl-hw, on each host with OpenCL devices (jetson-hub: the vendors' +# runtimes for its CPU, its discrete GPU and its integrated GPU; thor: PoCL +# for its CPU and, through CUDA, its GPU): the devices its matrix entry +# declares, each by a key that carries the device's vendor id +# (opencl--). 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. +# dense f64 state vector, run resident (the state on the device for the +# whole run), must come back with the CPU's bits on each, on its pinned +# corpus and on random circuits, its wider kernels certified on their own +# corpus or said not to be (a member whose wider kernels did not certify +# runs gates on up to three qubits at once). # - opencl-amd (framework-128gb): the same on its integrated GPU, through the # GPU vendor's OpenCL platform, the state vector included. # - opencl-macos (the macOS runner): the same on the platform's GPU, through its @@ -59,16 +65,31 @@ jobs: echo "run=$r" >> "$GITHUB_OUTPUT" opencl-hw: - name: member · OpenCL GPU and CPU (jetson-hub) + name: member · OpenCL GPU and CPU (${{ matrix.host }}) needs: relevant if: needs.relevant.outputs.run == 'true' - runs-on: linux-hw + runs-on: ${{ matrix.runner }} timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + # Each host's devices, sorted, as discovery names them. A key carries + # the device's vendor id (opencl--); a device + # with no binary64 has its state vector refused, and says so. + - host: jetson-hub + runner: linux-hw + opencl: opencl-cpu-8086,opencl-gpu-10de,opencl-gpu-8086 + pocl_arch: '' + # PoCL's CUDA device compiles to PTX for sm_90 (its LLVM 18 has no + # sm_110), which the driver compiles for the GPU. + - host: thor + runner: linux-arm64-hw + opencl: opencl-cpu-13b5,opencl-gpu-10de + pocl_arch: sm_90 env: - # The devices are declared by the host, in its runner's environment - # (WAI_QUANTUM_HW_OPENCL_KEYS); with no declaration, the CPU and the - # discrete GPU. A key carries the device's vendor id - # (opencl--). + WAI_QUANTUM_HW_OPENCL_KEYS: ${{ matrix.opencl }} + POCL_CUDA_GPU_ARCH: ${{ matrix.pocl_arch }} CARGO_PROFILE_DEV_DEBUG: '0' CARGO_BUILD_JOBS: '8' steps: @@ -129,12 +150,16 @@ jobs: 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; } + grep -qE '^test result: ok\. 4 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; } + # The wider kernels certify on their own corpus, or the member says + # they did not (it then runs gates on up to three qubits at once). + grep -qE "^opencl sv wide certified: key=$key .* kernel=wai.quantum.sv/run-wide .* parity=(bit-exact cases=120 digest=093614f2386ec5120ab6ccde5187a544518432751591535a6f58c7a44061ebaf|not-bit-exact cases=120 digest=[0-9a-f]{64})$" "$report" || { echo "::error::$key: the wider kernels' certificate was not established"; exit 1; } + grep -qE "^opencl sv random: key=$key [0-9]+ resident runs on (every kernel|the narrow kernels) match the CPU bit for bit$" "$report" || { echo "::error::$key: a random circuit's resident run is not the CPU's"; exit 1; } done opencl-amd: @@ -200,12 +225,16 @@ jobs: 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; } + grep -qE '^test result: ok\. 4 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; } + # The wider kernels certify on their own corpus, or the member says + # they did not (it then runs gates on up to three qubits at once). + grep -qE "^opencl sv wide certified: key=$key .* kernel=wai.quantum.sv/run-wide .* parity=(bit-exact cases=120 digest=093614f2386ec5120ab6ccde5187a544518432751591535a6f58c7a44061ebaf|not-bit-exact cases=120 digest=[0-9a-f]{64})$" "$report" || { echo "::error::$key: the wider kernels' certificate was not established"; exit 1; } + grep -qE "^opencl sv random: key=$key [0-9]+ resident runs on (every kernel|the narrow kernels) match the CPU bit for bit$" "$report" || { echo "::error::$key: a random circuit's resident run is not the CPU's"; exit 1; } done opencl-macos: @@ -245,7 +274,7 @@ jobs: 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 -qE '^test result: ok\. 4 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: @@ -277,4 +306,4 @@ jobs: 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; } + grep -qE '^test result: ok\. 4 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::the state-vector tests did not run over no device"; exit 1; } diff --git a/.github/workflows/wai-quantum-webgpu.yml b/.github/workflows/wai-quantum-webgpu.yml index b28391928..dc9e9db84 100644 --- a/.github/workflows/wai-quantum-webgpu.yml +++ b/.github/workflows/wai-quantum-webgpu.yml @@ -250,14 +250,23 @@ jobs: member-hw: needs: relevant if: needs.relevant.outputs.run == 'true' - name: member · discrete GPU, Vulkan (jetson-hub) - runs-on: linux-hw + name: member · GPU, Vulkan (${{ matrix.host }}) + runs-on: ${{ matrix.runner }} timeout-minutes: 150 + strategy: + fail-fast: false + matrix: + include: + # Each host's adapters, declared here: its GPUs and the software + # Vulkan driver. The wide sizes run on the GPU only. + - host: jetson-hub + runner: linux-hw + adapters: gpu-vulkan-10de,gpu-vulkan-8086,swgpu-vulkan-10005 + - host: thor + runner: linux-arm64-hw + adapters: gpu-vulkan-10de,swgpu-vulkan-10005 env: - # 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_EXPECT: ${{ matrix.adapters }} WAI_QUANTUM_WEBGPU_WIDE: '1' CARGO_PROFILE_DEV_DEBUG: '0' CARGO_BUILD_JOBS: '8' @@ -280,7 +289,6 @@ jobs: working-directory: src 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 diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index bb7bf2cf6..da8b71de9 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -363,6 +363,79 @@ wire format may change and such changes are recorded here. implementation, `channel_lineup_verify.py`, from the text alone, agrees on every vector, and a CI step runs both. +### wai-quantum: the state vector resident on the GPU, auto by measurement, and Pauli propagation on the GPU + +- **Resident** (`quantum_fabric::sv64`, host layout 2): a member runs a whole plan with the state on the device. + The state is zeroed there (or uploaded once, for a partition), every launch's parameters go up once as a + `LaunchTable`, each launch is enqueued in the CPU's order on one stream or in-order queue with no wait between + them, and the state is read back once (on CUDA through two page-locked buffers in turn). The kernels are the + same formulas; a gate on two to five qubits takes the kernel compiled for its group size (`sv64::KERNELS`, + `sv64::many_kernel`), its group in registers. The host layout is part of every implementation id, so each + member's certificate is a certificate of the resident path. `CudaSv`, `HipSv` and `OpenClSv` gain + `run_from_zero`; `run_launches` keeps its signature. `max_qubits` now follows the device's memory + (`sv64::qubits_in`, at most 32) rather than the dyadic core's 26; 27 on the laptop GPU here. +- **The wider kernels' own certificate** (`sv64::KERNEL_SV_RUN_WIDE`, `sv64::SV_WIDE`, 120 cases at fusion 4 to 6, + pinned as `SV_WIDE_PIN`): the pinned corpus reaches only the kernels of gates on up to three qubits at once + (`sv64::NARROW`), so a member is placed for a job whose gates act on more than three at once only where these + kernels certified bit-exact too; otherwise such a job on it is refused (`J013`), and `accelerator` passes over + it. A ground-truth test caught one CPU's OpenCL runtime reordering the five-qubit kernel's sums on about one input + in six, which a corpus of 36 cases let through; at 120 cases its wider kernels do not certify, and it runs gates + on up to three qubits at once. The receipt's `fabric` carries the member's `wide` certificate when the job used + those kernels, and `wai-quantum fabric` lists each member's (`fabric_wide` in `wai.quantum.job-result/1` and + `wai.quantum.tools/1`, optional). +- **Ground truth first**: the table a member uploads decodes to the launches planned and replays to the CPU's + state, over the pinned corpus and random circuits reaching every kernel; on each device, random circuits at + fusion 0 to 6 (`sv64::random_cases`), from `|0…0⟩` and from a state given as a partition runs, are the CPU's + bit for bit. Placed on the fleet's laptop GPU (by CUDA and by OpenCL), the CPU beside it by OpenCL, and its + integrated GPU (by HIP and by OpenCL), with the split over them, the CPU's answers byte for byte. +- **`auto` by measurement** (`engine::fabric_measured`): whole jobs of one circuit family (ten layers of + arbitrary-angle rotations and brickwork entangling pairs, 1000 shots) timed on the CPU and on each member, five runs + of each alternating, at fusion 3, 2 and 1, from 18 qubits to as many as half the host's memory held, on three hosts + (2026-10-30 and 31); the rows, each host's load and what discovering its members took are committed, and + `examples/fabric_timing.rs` measures them again. A row is faster when every run on the member beat every run on + the CPU. `auto` places a state on a member from the smallest measured size from which every row up to the largest + is faster, up to that largest, at the fusion width measured, for a plan of at least the measured launches + (`fabric::launches`); a process that has not discovered its members yet does so only where the saving also covers + the measured discovery. Hosts are named by `fabric::host` (the processor's model, architecture and threads, + hashed), members by id. Results: the laptop GPU is never faster from 22 qubits up at any fusion width (26 qubits, + fusion 3: 3.74 s on the CPU, 4.44 s by CUDA, 4.29 s by OpenCL), so `auto` stays on its CPU; the integrated GPU + beat its CPU in every run at fusion 3 and 2 from 18 to 28 qubits (22 qubits, fusion 3: 205 ms against 108 ms by + HIP and 102 ms by OpenCL; 28: 9.81 s against 8.47 s and 8.14 s) and lost from 22 qubits at fusion 1, so `auto` + places fused jobs there (from 18 qubits once discovered, at 28 in a fresh process, discovery taking 1.2 s). On + an aarch64 board (14 cores sharing 122 GiB with an integrated GPU of compute capability 11.0, by CUDA and by + OpenCL) the GPU won some rows below 24 qubits and lost every row from 26 qubits at every fusion width, by either + (28 qubits, fusion 3: 22.5 s on the CPU against 27.7 s), so `auto` stays on its CPU. + A member `auto` chose that fails during the run hands the job back to the CPU. The result's `fabric` carries + `auto` (the rule, the host, the day, the load, the discovery and the measured row it rests on), and the plan's + reasons say so; `wai-quantum fabric` prints the rule, where it places here and every measured row (`auto_rule`). + New optional `fabric_auto` and `fabric_spread` in `wai.quantum.job-result/1`, `auto_rule` in + `wai.quantum.tools/1`. A job on any other host, member or fusion width runs as it did, its result unnamed. +- **Sparse Pauli dynamics on a GPU, with the CPU's bits** (`quantum_spd::device`): the sum of terms lives on a + device (`SpdDevice`), which runs each step's arithmetic on every term at once (one multiply by `sin`, one by `cos`, + one addition where a new term meets an old one, sign changes; no fused multiply-add), while the host keeps what + decides the term set: a rotation's new terms found in the table (dead slots included) and the rest appended in the + order of the terms they came from by an order-preserving prefix sum; dead and live terms counted, and the table + compacted, exactly when the CPU does; the magnitudes a truncation dropped handed back in table order and the bound + and the value added on the host in table order (`expectation_on`). Ground truth first: `HostDevice` runs the + protocol on the CPU and is the CPU's propagation bit for bit, final table included, over 100 cases at one thread + and many. The CUDA member (`CudaSpd`, kernels `quantum_fabric::spd`, implementation `cuda-spd`) is certified on + `SPD_DEFAULT` (40 cases on one to four words, term and weight caps, layered circuits whose dropped strings come + back and are revived in place, and an exact cancellation; pinned as `SPD_DEFAULT_PIN`), its output every value, + bound, threshold reached, count and the final table slot for slot, so a device that keeps the numbers but not the + term set or its order is caught (six mutants, each killed). On the laptop GPU: certified, 24 wider random cases up + to two million terms and the 127-qubit kicked-Ising circuit the CPU's result bit for bit. +- **The `pauli` method on the fabric**: a job asks with `fabric` (`accelerator`, or a member's id or key) and its + sum of terms runs on that member, the CPU's answers byte for byte, the result's `fabric` naming the member and its + certificate (kernel `wai.quantum.spd/propagate`); `cpu` names the CPU; a split is refused for it (`J013`); `auto` + keeps it on the CPU, no member being measured faster for it. Its members are discovered apart from the state + vector's, the first time a job asks for one, and `wai-quantum fabric` lists them (`pauli` in + `wai.quantum.tools/1`, optional). +- **CI**: the state-vector legs run four tests (the random circuits added) and check the wider kernels' + certificate; the placement legs run seven (a job of wider gates placed or refused, a `pauli` job on each member) + and hold `auto` to the measurements on each host (the laptop's GPU never placed; the integrated GPU placed at 18 + qubits and fusion 2, a shallower job kept on the CPU); the CUDA leg certifies the propagation and kills its + mutants; the container legs name the table's, the corpora's, the rule's and the protocol's tests. + ### wai-quantum: simulator scale-out, the state vector on the fabric's members - **Placement** (`engine::fabric`): a job's new `fabric` field says where the `statevector` method's state runs: diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 36f5cbd00..705c4cd1f 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -101,7 +101,7 @@ wai-quantum instance ghz-family.qasm --input n=3 `wai-quantum methods` lists each method's reach in a job's memory here (`--memory-gb`, default 16). -`--fabric` says where the `statevector` method's state runs: `auto` (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), `cpu`, `accelerator` (the first accelerator member certified bit-exact here that holds the state, the next taking over if it fails during the run), or a member's id or key; given more than once, the state is split by its top qubits into one partition on each member named (a power of two of them; `cpu` is a member, and one may be named twice). Every member placed gives the CPU's answers byte for byte, and the result names where its state ran, with the member's certificate, in its receipt. `wai-quantum fabric` lists the members this host can place on, each with the most qubits it holds, and the devices found and not placed, with why. A placement this host cannot honour is refused (`J013`), never made elsewhere. +`--fabric` says where the `statevector` method's state runs: `auto` (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU; a job placed that way carries the rule and the measured row it rests on in its receipt), `cpu`, `accelerator` (the first accelerator member certified bit-exact here that holds the state, the next taking over if it fails during the run), or a member's id or key; given more than once, the state is split by its top qubits into one partition on each member named (a power of two of them; `cpu` is a member, and one may be named twice). Every member placed gives the CPU's answers byte for byte, and the result names where its state ran, with the member's certificate, in its receipt. `wai-quantum fabric` lists the members this host can place on, each with the most qubits it holds (and whether its kernels for gates on more than three qubits at once certified), the devices found and not placed, with why, `auto`'s rule, where it places on this host, and every measured row. A placement this host cannot honour is refused (`J013`), never made elsewhere. A `pauli` job takes `--fabric accelerator` or a member's id or key too: its sum of terms then runs on that member (the CUDA member, certified bit-exact against the CPU for it), with the CPU's answers byte for byte; `auto` keeps it on the CPU. ```sh runnable wai-quantum fabric diff --git a/wai/wai-quantum-cli/README.zh-CN.md b/wai/wai-quantum-cli/README.zh-CN.md index cb165ba5f..73134a7cf 100644 --- a/wai/wai-quantum-cli/README.zh-CN.md +++ b/wai/wai-quantum-cli/README.zh-CN.md @@ -104,7 +104,7 @@ wai-quantum instance ghz-family.qasm --input n=3 `wai-quantum methods` 列出每种方法在本机一个作业的内存(`--memory-gb`,默认 16)内能达到的规模。 -`--fabric` 指定 `statevector` 方法的态在哪里运行:`auto`(默认:在每台主机上都放在 CPU 上,直到测得某个计算结构成员比 CPU 更快)、`cpu`、`accelerator`(本机第一个经认证逐位一致且能容纳该态的加速器成员;若它在运行中失败,由下一个接替),或成员的 id 或键;多次给出时,态按最高位量子比特划分为若干分区,每个所列成员一个分区(数量为 2 的幂;`cpu` 也是成员,同一成员可列两次)。放置到的每个成员都给出与 CPU 逐字节相同的答案,结果在其收据中指明态在哪里运行以及该成员的证书。`wai-quantum fabric` 列出本机可放置的成员(各附其可容纳的最多量子比特数),以及找到但未放置的设备及原因。主机无法兑现的放置会被拒绝(`J013`),绝不改放别处。 +`--fabric` 指定 `statevector` 方法的态在哪里运行:`auto`(默认:只在已提交的测量结果表明某个成员对该作业的规模和融合宽度比本机 CPU 更快时才放到该成员上,否则放在 CPU 上;这样放置的作业在其收据中带有规则及其所依据的测量行)、`cpu`、`accelerator`(本机第一个经认证逐位一致且能容纳该态的加速器成员;若它在运行中失败,由下一个接替),或成员的 id 或键;多次给出时,态按最高位量子比特划分为若干分区,每个所列成员一个分区(数量为 2 的幂;`cpu` 也是成员,同一成员可列两次)。放置到的每个成员都给出与 CPU 逐字节相同的答案,结果在其收据中指明态在哪里运行以及该成员的证书。`wai-quantum fabric` 列出本机可放置的成员(各附其可容纳的最多量子比特数,以及其一次作用于三个以上量子比特的门的内核是否通过认证)、找到但未放置的设备及原因、`auto` 的规则、它在本机的放置范围以及每个测量行。主机无法兑现的放置会被拒绝(`J013`),绝不改放别处。`pauli` 作业同样可以用 `--fabric accelerator` 或成员的 id 或键:其项之和随后在该成员上运行(CUDA 成员,已对照 CPU 认证为逐位一致),答案与 CPU 逐字节相同;`auto` 把它留在 CPU 上。 ```sh runnable wai-quantum fabric diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index f60b34090..dc5710f73 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -58,7 +58,7 @@ Parameters: - `options` (object): Method options: max_bond, fusion, trajectories, threshold, memory_gb. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's "compiled" states its distance from the program. - `target` (any): A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it. -- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013). +- `fabric` (any): Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; list_fabric states the rule and its numbers), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013). - `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt. - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. @@ -82,7 +82,7 @@ Parameters: - `options` (object): Method options: max_bond, fusion, trajectories, threshold, memory_gb. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's "compiled" states its distance from the program. - `target` (any): A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it. -- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013). +- `fabric` (any): Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; list_fabric states the rule and its numbers), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013). - `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt. - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. @@ -430,7 +430,7 @@ Parameters: - `options` (object): Method options: max_bond, fusion, trajectories, threshold, memory_gb. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's "compiled" states its distance from the program. - `target` (any, required): A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it. -- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013). +- `fabric` (any): Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; list_fabric states the rule and its numbers), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013). - `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt. - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. @@ -613,7 +613,7 @@ Parameters: - `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program. - `target` (any): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing. -- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. +- `fabric` (any): Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. - `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt. - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. @@ -636,7 +636,7 @@ Parameters: - `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program. - `target` (any): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing. -- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. +- `fabric` (any): Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. - `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt. - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. @@ -660,7 +660,7 @@ Parameters: - `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program. - `target` (any): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing. -- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. +- `fabric` (any): Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. - `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt. - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. @@ -802,7 +802,7 @@ Parameters: - `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program. - `target` (any, required): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing. -- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. +- `fabric` (any): Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU. - `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt. - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. @@ -1131,7 +1131,7 @@ A program that measures a qubit and then acts on it, resets one after its first - On this machine: `wai-quantum run`, or the local MCP server (`wai-quantum mcp`). Results are signed with the user's key and nothing leaves the machine. - On the free job service, for a program too large for this machine: `wai-quantum submit`, `POST https://wai.transaction.science/quantum-jobs/v1/run`, or its MCP server at https://wai.transaction.science/quantum-jobs/mcp. No account and no key. - auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, stabilizer_rank, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, stabilizer_rank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen; stabilizer_rank is chosen by its T count, its exact sum taken when its branches are within one job's work. -- The statevector method's state runs where the job's `fabric` says: auto (the default) places it on the CPU, on every host, until a member of the fabric is measured faster than the CPU; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere. +- The statevector method's state runs where the job's `fabric` says: auto (the default) places it on a member only where the committed measurements show that member faster than this host's CPU for the job's size and fusion width, and otherwise on the CPU (the result's `fabric` then names nothing), a placement by measurement saying in its receipt which rule and which measured row it rests on; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere. A pauli job (sparse Pauli dynamics) takes accelerator or a member's id or key too: its sum of terms then runs on that member, certified bit-exact against the CPU for the propagation (result and final table), with the CPU's answers byte for byte; auto keeps it on the CPU, no member being measured faster for it. ### `wai-quantum://docs/diagnostics`: the reader's diagnostics @@ -1307,7 +1307,7 @@ The accuracy classes: auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, stabilizer_rank, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, stabilizer_rank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen; stabilizer_rank is chosen by its T count, its exact sum taken when its branches are within one job's work. -The statevector method's state runs where the job's `fabric` says: auto (the default) places it on the CPU, on every host, until a member of the fabric is measured faster than the CPU; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere. +The statevector method's state runs where the job's `fabric` says: auto (the default) places it on a member only where the committed measurements show that member faster than this host's CPU for the job's size and fusion width, and otherwise on the CPU (the result's `fabric` then names nothing), a placement by measurement saying in its receipt which rule and which measured row it rests on; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere. A pauli job (sparse Pauli dynamics) takes accelerator or a member's id or key too: its sum of terms then runs on that member, certified bit-exact against the CPU for the propagation (result and final table), with the CPU's answers byte for byte; auto keeps it on the CPU, no member being measured faster for it. ### `wai-quantum://examples`: worked examples, each with what it must do @@ -1793,12 +1793,15 @@ Options: the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes --fabric - Where the state vector runs (the statevector method): auto (the default: the CPU, until a - member is measured faster), cpu, accelerator (the first certified accelerator member that - holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the - state is split by its top qubits into one partition on each member named, a power of two - of them. A member certified bit-exact gives the CPU's answers; a placement this host - cannot honour is refused (J013) + Where the state vector runs (the statevector method): auto (the default: on a member only + where the committed measurements show it faster than this host's CPU for the job's size + and fusion, otherwise the CPU; `wai-quantum fabric` states the rule and the numbers), cpu, + accelerator (the first certified accelerator member that holds the state), or a member's + id or key (`wai-quantum fabric` lists them); for a pauli job, accelerator or a member's id + or key runs its sum of terms on that member, with the CPU's answers; repeated, the state + is split by its top qubits into one partition on each member named, a power of two of + them. A member certified bit-exact gives the CPU's answers; a placement this host cannot + honour is refused (J013) --json Print the job as the service returns it: request, plan, result and receipt @@ -1939,12 +1942,15 @@ Options: the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes --fabric - Where the state vector runs (the statevector method): auto (the default: the CPU, until a - member is measured faster), cpu, accelerator (the first certified accelerator member that - holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the - state is split by its top qubits into one partition on each member named, a power of two - of them. A member certified bit-exact gives the CPU's answers; a placement this host - cannot honour is refused (J013) + Where the state vector runs (the statevector method): auto (the default: on a member only + where the committed measurements show it faster than this host's CPU for the job's size + and fusion, otherwise the CPU; `wai-quantum fabric` states the rule and the numbers), cpu, + accelerator (the first certified accelerator member that holds the state), or a member's + id or key (`wai-quantum fabric` lists them); for a pauli job, accelerator or a member's id + or key runs its sum of terms on that member, with the CPU's answers; repeated, the state + is split by its top qubits into one partition on each member named, a power of two of + them. A member certified bit-exact gives the CPU's answers; a placement this host cannot + honour is refused (J013) --json Print the plan as JSON @@ -3022,12 +3028,15 @@ Options: the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes --fabric - Where the state vector runs (the statevector method): auto (the default: the CPU, until a - member is measured faster), cpu, accelerator (the first certified accelerator member that - holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the - state is split by its top qubits into one partition on each member named, a power of two - of them. A member certified bit-exact gives the CPU's answers; a placement this host - cannot honour is refused (J013) + Where the state vector runs (the statevector method): auto (the default: on a member only + where the committed measurements show it faster than this host's CPU for the job's size + and fusion, otherwise the CPU; `wai-quantum fabric` states the rule and the numbers), cpu, + accelerator (the first certified accelerator member that holds the state), or a member's + id or key (`wai-quantum fabric` lists them); for a pauli job, accelerator or a member's id + or key runs its sum of terms on that member, with the CPU's answers; repeated, the state + is split by its top qubits into one partition on each member named, a power of two of + them. A member certified bit-exact gives the CPU's answers; a placement this host cannot + honour is refused (J013) --json Print the job as the service returns it: request, plan, result and receipt diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index 953f5a046..f0cdc5cea 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -161,7 +161,7 @@ "description": "A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it." }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; list_fabric states the rule and its numbers), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", "anyOf": [ { "type": "string", @@ -1115,7 +1115,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -2350,7 +2350,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -2431,6 +2431,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -2500,6 +2559,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -2594,6 +2712,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -2601,6 +2778,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -4406,7 +4698,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -5639,7 +5931,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -5720,6 +6012,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -5789,6 +6140,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -5883,6 +6293,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -5890,6 +6359,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -7939,7 +8523,7 @@ "description": "A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it." }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; list_fabric states the rule and its numbers), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", "anyOf": [ { "type": "string", @@ -26714,7 +27298,301 @@ }, "auto": { "type": "string", - "description": "Where auto places a state: on the CPU, until a member is measured faster than the CPU." + "description": "Where auto places a state: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion; everywhere else on the CPU." + }, + "auto_rule": { + "type": "object", + "description": "The rule auto places by and what it rests on: this host (as the measurements name it), whether it was measured, where the rule places on it for the members found here, and every measured host with its rows (each with the spread of the CPU's and the member's times and whether the member was faster).", + "required": [ + "rule", + "host", + "places", + "hosts" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string" + }, + "measured_here": { + "type": "boolean" + }, + "places": { + "type": "array", + "items": { + "type": "object", + "required": [ + "member", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "fusion": { + "type": "integer" + }, + "from_qubits": { + "type": "integer" + }, + "to_qubits": { + "type": "integer" + }, + "min_launches": { + "type": [ + "integer", + "null" + ] + } + } + } + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "required": [ + "host", + "measured", + "rows" + ], + "properties": { + "host": { + "type": "string" + }, + "about": { + "type": "string" + }, + "measured": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number" + }, + "rows": { + "type": "array", + "items": { + "type": "object", + "required": [ + "member", + "fusion", + "qubits", + "cpu_ms", + "member_ms", + "faster" + ], + "properties": { + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "fusion": { + "type": "integer" + }, + "qubits": { + "type": "integer" + }, + "launches": { + "type": "integer" + }, + "runs": { + "type": "integer" + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "faster": { + "type": "boolean" + } + } + } + } + } + } + } + } + }, + "pauli": { + "type": "object", + "description": "The members the pauli method's sum of terms can be placed on here (wai.quantum.spd/propagate): the CPU and each accelerator certified bit-exact against it on the pinned corpus, result and final table alike; the devices found and not placed, with why; and where auto places it (on the CPU: no member is measured faster for it).", + "required": [ + "kernel", + "members" + ], + "properties": { + "kernel": { + "type": "string" + }, + "auto": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "required": [ + "member", + "implementation", + "parity" + ], + "properties": { + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "class": { + "type": "string" + }, + "implementation": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "detail": { + "type": "object", + "properties": { + "backend": { + "type": "string" + } + } + } + } + } + }, + "refused": { + "type": "array", + "items": { + "type": "object", + "properties": { + "backend": { + "type": "string" + }, + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "why": { + "type": "string" + } + } + } + }, + "unavailable": { + "type": "array", + "items": { + "type": "object", + "properties": { + "backend": { + "type": "string" + }, + "why": { + "type": "string" + } + } + } + }, + "backends": { + "type": "array", + "items": { + "type": "string" + } + } + } }, "members": { "type": "array", @@ -26782,6 +27660,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "detail": { "type": "object", "description": "The most qubits it holds and what bounds it; for an accelerator, its backend and what discovery found.", @@ -28476,7 +29413,7 @@ "description": "A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it." }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; list_fabric states the rule and its numbers), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", "anyOf": [ { "type": "string", @@ -29433,7 +30370,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -30668,7 +31605,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -30749,6 +31686,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -30818,6 +31814,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -30912,6 +31967,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -30919,6 +32033,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -32724,7 +33953,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -33957,7 +35186,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -34038,6 +35267,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -34107,6 +35395,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -34201,6 +35548,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -34208,6 +35614,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -48249,7 +49770,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -49484,7 +51005,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -49565,6 +51086,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -49634,6 +51214,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -49728,6 +51367,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -49735,6 +51433,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -51540,7 +53353,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -52773,7 +54586,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -52854,6 +54667,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -52923,6 +54795,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -53017,6 +54948,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -53024,6 +55014,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index 0af8ca9bd..f77583118 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -133,7 +133,7 @@ "description": "A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing." }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", "anyOf": [ { "type": "string", @@ -1121,7 +1121,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -2356,7 +2356,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -2437,6 +2437,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -2506,6 +2565,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -2600,6 +2718,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -2607,6 +2784,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -4412,7 +4704,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -5645,7 +5937,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -5726,6 +6018,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -5795,6 +6146,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -5889,6 +6299,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -5896,6 +6365,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -9261,7 +9845,7 @@ "description": "A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing." }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", "anyOf": [ { "type": "string", @@ -10253,7 +10837,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -11488,7 +12072,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -11569,6 +12153,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -11638,6 +12281,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -11732,6 +12434,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -11739,6 +12500,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -13544,7 +14420,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -14777,7 +15653,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -14858,6 +15734,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -14927,6 +15862,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -15021,6 +16015,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -15028,6 +16081,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -18393,7 +19561,7 @@ "description": "A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing." }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", "anyOf": [ { "type": "string", @@ -20415,7 +21583,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -21650,7 +22818,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -21731,6 +22899,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -21800,6 +23027,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -21894,6 +23180,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -21901,6 +23246,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -23706,7 +25166,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -24939,7 +26399,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -25020,6 +26480,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -25089,6 +26608,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -25183,6 +26761,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -25190,6 +26827,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -40452,7 +42204,7 @@ "description": "A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing." }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", "anyOf": [ { "type": "string", @@ -41433,7 +43185,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -42668,7 +44420,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -42749,6 +44501,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -42818,6 +44629,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -42912,6 +44782,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -42919,6 +44848,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -44724,7 +46768,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", @@ -45957,7 +48001,7 @@ }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -46038,6 +48082,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -46107,6 +48210,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -46201,6 +48363,65 @@ "type": "integer", "minimum": 0 }, + "wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "type": "object", + "description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.", + "required": [ + "tier" + ], + "properties": { + "tier": { + "enum": [ + "bit-exact", + "within", + "statistical", + "uncertified" + ] + }, + "max_ulp": { + "type": "integer", + "minimum": 0 + }, + "max_abs": { + "type": "number" + }, + "fidelity": { + "type": "number" + }, + "shots": { + "type": "integer", + "minimum": 0 + } + } + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." @@ -46208,6 +48429,121 @@ } } }, + "auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "member_ms": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + } + } + } + } + }, "energy": { "description": "The CPU energy of the job's process as the kernel counts it, with its uncertainty and what it leaves out, or unmetered with the reason.", "anyOf": [ @@ -48195,7 +50531,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index dd4d22415..bca06b7c6 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -541,7 +541,7 @@ struct Job { /// A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes #[arg(long = "param", value_name = "NAME=VALUE", value_parser = variational::param_arg)] params: Vec<(String, f64)>, - /// Where the state vector runs (the statevector method): auto (the default: the CPU, until a member is measured faster), cpu, accelerator (the first certified accelerator member that holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the state is split by its top qubits into one partition on each member named, a power of two of them. A member certified bit-exact gives the CPU's answers; a placement this host cannot honour is refused (J013) + /// Where the state vector runs (the statevector method): auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; `wai-quantum fabric` states the rule and the numbers), cpu, accelerator (the first certified accelerator member that holds the state), or a member's id or key (`wai-quantum fabric` lists them); for a pauli job, accelerator or a member's id or key runs its sum of terms on that member, with the CPU's answers; repeated, the state is split by its top qubits into one partition on each member named, a power of two of them. A member certified bit-exact gives the CPU's answers; a placement this host cannot honour is refused (J013) #[arg(long = "fabric", value_name = "PLACEMENT")] fabric: Vec, } @@ -611,6 +611,19 @@ fn option_arg(s: &str) -> Result<(String, Value), String> { fn fabric_text(f: &Value) -> String { let s = |v: &Value| v.as_str().unwrap_or("").to_string(); let mut o = format!("kernel {}\nauto {}\n", s(&f["kernel"]), s(&f["auto"])); + let rule = &f["auto_rule"]; + let places = rule["places"].as_array().cloned().unwrap_or_default(); + for p in &places { + o.push_str(&format!("auto places {} from {} to {} qubits at fusion {}, for a plan of at least {} launches\n", s(&p["key"]), p["from_qubits"], p["to_qubits"], p["fusion"], p["min_launches"])); + } + if places.is_empty() { + let why = if rule["measured_here"].as_bool() == Some(true) { "no member found here is measured faster than this host's CPU" } else { "this host is not among the measured ones" }; + o.push_str(&format!("auto places nothing here: {why}; every state on the CPU\n")); + } + for h in rule["hosts"].as_array().into_iter().flatten() { + let rows = h["rows"].as_array().cloned().unwrap_or_default(); + o.push_str(&format!("measured {}, {} ({}), {} rows, {} faster; discovery {} ms once per process\n", s(&h["host"]), s(&h["measured"]), s(&h["about"]), rows.len(), rows.iter().filter(|r| r["faster"] == true).count(), h["discovery_ms"])); + } for m in f["members"].as_array().into_iter().flatten() { let corpus = m["corpus"].as_str().map(|c| format!(", corpus {}… over {} cases", &c[..16.min(c.len())], m["cases"])).unwrap_or_default(); o.push_str(&format!("member {:<18} {:<4} up to {} qubits, {} {}{corpus}\n", s(&m["key"]), s(&m["class"]), m["detail"]["max_qubits"], s(&m["implementation"]), s(&m["parity"]["tier"]))); @@ -970,6 +983,9 @@ fn print_job(body: &Value, top: usize) { Some(parts) => println!("fabric {} partitions on {}, {} exchanges, {}", parts.len(), parts.iter().map(|p| s(&p["key"])).collect::>().join(", "), f["exchanges"], s(&f["parity"]["tier"])), None => println!("fabric {} ({}), {}, certificate {}", s(&f["key"]), s(&f["implementation"]), s(&f["parity"]["tier"]), s(&f["certificate"])), } + if f.get("auto").is_some() { + println!(" {}", s(&f["why"])); + } } if let Some(w) = r.get("warning").and_then(Value::as_str) { println!("warning {w}"); diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index c8296a2ef..83f7ca3f1 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -129,7 +129,7 @@ fn job_schema() -> Value { /// What a job's `fabric` takes: where the statevector method's state runs. pub(crate) fn fabric_schema() -> Value { json!({ - "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion, otherwise the CPU; list_fabric states the rule and its numbers), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).", "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "array", "minItems": 2, "maxItems": 64, "items": { "type": "string", "minLength": 1 } }] }) } diff --git a/wai/wai-quantum-service/src/mcp.rs b/wai/wai-quantum-service/src/mcp.rs index d578deee8..b5b6d42ed 100644 --- a/wai/wai-quantum-service/src/mcp.rs +++ b/wai/wai-quantum-service/src/mcp.rs @@ -228,7 +228,7 @@ fn tool_list() -> Value { "options": { "type": "object", "description": "Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories." }, "compile": { "type": "object", "description": "Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.", "properties": compile_props() }, "target": { "description": "A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing." }, - "fabric": { "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "array", "minItems": 2, "maxItems": 64, "items": { "type": "string", "minLength": 1 } }] }, + "fabric": { "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than the host's CPU for the job's size and fusion, otherwise the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each; a pauli job's sum of terms runs on the member accelerator or its id or key names. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "array", "minItems": 2, "maxItems": 64, "items": { "type": "string", "minLength": 1 } }] }, "intent": intent_schema(), "repair": { "description": "Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {\"max_iterations\": N, \"dyadic_bits\": K, \"sizes\": {\"q\": 3}}. The result's `repaired` holds the program that ran and each repair." }, "mitigate": wai_quantum::engine::mitigate::schema(), diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index e97252f7d..b2e7f679d 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -269,6 +269,10 @@ required-features = ["quantum_fabric_metal"] name = "cuda_sv" required-features = ["quantum_fabric_cuda", "quantum_sv"] +[[test]] +name = "cuda_spd" +required-features = ["quantum_fabric_cuda", "quantum_spd"] + [[test]] name = "hip_wmma" required-features = ["quantum_fabric_hip"] @@ -341,6 +345,10 @@ required-features = ["quantum_frame", "quantum_ldpc"] name = "stablehlo_gate" required-features = ["quantum_fabric"] +[[example]] +name = "fabric_timing" +required-features = ["quantum_engine", "quantum_fabric"] + [[example]] name = "rv32_listing" required-features = ["quantum_fabric_dataflow"] diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index 69b4a1dd4..c4f54b8c0 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -110,7 +110,7 @@ never creates information. | `quantum_vml`, `quantum_phasor`, `quantum_qfhrr`, `quantum_kernel`, `quantum_qdata`, `quantum_phasor_meter` | the interference / phasor ML layer | | `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::stablehlo` writes the gate as a portable tensor program (StableHLO), every value a 64-bit integer that never overflows, for any tensor compiler to build for its own devices; the `stablehlo_gate` example writes it with a batch of inputs and the exact outputs a build must give, and `ci/tensor-program/check.sh` runs a build and holds it to them | | `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. On a device of compute capability 8.0 or later, a **second kernel runs every product on the tensor cores** (`CudaMember::tensor`): each value as four 8-bit limbs in bf16, accumulated in f32, which holds every partial sum exactly; probed and certified under its own implementation id, with its own mutants. 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_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. On a device of compute capability 8.0 or later, a **second kernel runs every product on the tensor cores** (`CudaMember::tensor`): each value as four 8-bit limbs in bf16, accumulated in f32, which holds every partial sum exactly; probed and certified under its own implementation id, with its own mutants. 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. With `quantum_spd`, **sparse Pauli dynamics' sum of terms lives on the device** (`CudaMember::spd`, `quantum_spd::device`), certified against the CPU's propagation, result and final table alike | | `quantum_fabric_metal` | **the fabric's Metal member**, opt-in and not in `full`, macOS: the gate kernel in the Metal shading language with exact 64-bit integers, compiled at run time by the operating system's own compiler, one serial dispatch per gate in one command buffer, the state in memory the GPU and the host share. Elsewhere the build finds no member and says 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, keyed by its GPU family number (`gpu-msl-`). On a GPU with SIMD-group matrices, a **second kernel forms every product with the matrix multiply** (`MetalMember::matrix`): each value as four 8-bit limbs in bf16, accumulated in f32, which holds every partial sum exactly; probed and certified under its own implementation id, with its own mutants. Under operating system 26 or later, a **third kernel forms every product with the shading language's tensor operation** (`MetalMember::tensor_ops`), which the platform maps to the GPU's matrix hardware where it has it: offset-binary 8-bit limbs in bf16, accumulated in f32, through per-threadgroup scratch | | `quantum_fabric_hip` | **the fabric's HIP member**, opt-in and not in `full`, Linux: the CUDA member's kernel text, unchanged, compiled at run time by HIP's runtime compiler for each device's own architecture, one launch per gate. The runtime and its compiler are loaded when first used, so a build with the feature runs on a host without them and finds no member there, saying why; the device's properties are read in the runtime's frozen layout or the device is refused. Probed and certified as the CUDA member is, under its own implementation id, and with `quantum_sv` the dense f64 state vector runs bit for bit as on the CPU. On a gfx11 GPU, a **second kernel forms every product with the bf16 matrix instruction** (`HipMember::wmma`): each value offset-binary, as four unsigned 8-bit limbs, because the instruction's accumulation is not exact once a negative term enters a sum; the offset is removed in 64-bit integers. Probed and certified under its own implementation id, with its own mutants | | `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, and keyed by its vendor id (`opencl--`). 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 | @@ -158,6 +158,25 @@ published references. `quantum_bptn` computes the same circuits as tensor networks (`--example utility_bptn`), so a result can be checked by a second method that does not share the first one's approximation. +**On a GPU, with the CPU's bits** (`quantum_spd::device`). The sum of terms can +live on a device: the device runs each step's arithmetic on every term at once +(one multiply by `sin`, one by `cos`, one addition where a new term meets an +old one, sign changes; built without fused multiply-add), and the host keeps +what decides the term set. A rotation's new terms are found in the table, dead +slots included, and the rest appended in the order of the terms they came from +by an order-preserving prefix sum; the host counts dead and live terms and +compacts exactly when the CPU does; truncation hands back what it dropped in +table order, and the bound and the value are added on the host in table order. +`HostDevice` runs that protocol on the CPU and is held to the CPU's +propagation bit for bit, table included. The CUDA member (`CudaSpd`, with +`quantum_fabric_cuda`) is certified against the CPU on a pinned corpus +(`SPD_DEFAULT`: 40 cases on one to four words, with term and weight caps, +revived and killed terms), every value, bound, threshold reached, count and +final table the same; a `pauli` job asks for it with `fabric` (`accelerator`, +or its id or key) and gets the CPU's answers byte for byte, its result naming +the member. `auto` keeps the `pauli` method on the CPU: no member is measured +faster for it. + ## Fault-tolerant resources What a program would need on a machine that does not exist yet is a resource @@ -542,11 +561,20 @@ A job's `fabric` places the `statevector` method's state on the fabric's members (`engine::fabric`). With the CUDA, HIP or OpenCL member compiled in (`quantum_fabric_cuda`, `quantum_fabric_hip`, `quantum_fabric_opencl`, with `quantum_engine`), each device this process finds runs the dense f64 state -vector (`quantum_fabric::sv64`) and is certified against the CPU on the pinned -corpus; only a member certified bit-exact is placed, so a job placed on one -gives the CPU's answers byte for byte. `auto`, the default, places the state on -the CPU, on every host, until a member is measured faster than the CPU for a -job; a member runs a job only when the job asks for one. `accelerator` places +vector (`quantum_fabric::sv64`) resident: the state is zeroed on the device +(or uploaded once, for a partition), every gate's parameters go up once as one +table, every gate runs there in the CPU's launch order with no wait between +them, and the state comes back once. A gate on two to five qubits takes a +kernel compiled for its group size, its group held in registers. Each member +is certified against the CPU on the pinned corpus, and its kernels for gates +on more than three qubits at once (fusion 4 to 6, or a wider gate) on a corpus +of their own (`sv64::SV_WIDE`, 120 cases); only a member certified bit-exact +is placed, and only for the kernels it certified, so a job placed on one gives +the CPU's answers byte for byte. (One CPU's OpenCL runtime reorders the sums +of the five-qubit kernel on about one input in six; its wider kernels do not +certify, and it runs gates on up to three qubits at once.) `auto`, the default, places the state on +a member only by measurement (below); otherwise it stays on the CPU, and a +member runs a job when the job asks for one. `accelerator` places it on the first certified member that holds it, GPUs first, the next taking over if one fails during the run; `cpu` or a member's id or key place it there; and a list of members, a power of two of them, splits the @@ -559,11 +587,50 @@ partition's, with its certificate's parity tier, digest and corpus, and the receipt seals it; with no member asked for and none placed a result is the one it always was. Each member states the most qubits it holds, and a state or a partition past that is refused, as is any placement the host cannot honour -(`J013`). Measured on the fleet's GPUs (a laptop GPU and an integrated one, -each beside a CPU), with one launch per gate and the state copied to the -device and back, a whole job took about as long either way, or longer on the -GPU, between 18 and 22 qubits: the copies cost what the passes save. That is -why `auto` stays on the CPU. +(`J013`). + +**`auto` by measurement** (`engine::fabric_measured`). Whole jobs of one +family of circuits (ten layers of arbitrary-angle rotations on every qubit and +entangling pairs in brickwork, 1000 shots) were timed on the CPU and on each +member, five runs of each alternating, at fusion widths 3, 2 and 1, from 18 +qubits to as many as half the host's memory held; the table, with each host, +its load and the day, is in the source, and +`cargo run --release --example fabric_timing` measures it again. A row is +faster when every run on the member beat every run on the CPU. `auto` places a +state on a member from the smallest measured size from which every measured +size up to the largest is faster, up to that largest, at the fusion width +measured, for a plan of at least the measured launches; a process that has not +yet discovered its members does so only where the saving also covers the +discovery the table states. The receipt of a job `auto` placed carries the +rule and the row it rests on (`fabric.auto`), and `wai-quantum fabric` prints +the rule, where it places on this host and every measured row. What the rows +say (medians, 2026-10-30): + +| host | fusion | qubits | CPU | CUDA | HIP | OpenCL | auto | +|---|---|---|---|---|---|---|---| +| a laptop: 20-thread CPU, GPU of compute capability 8.9 | 3 | 22 | 207 ms | 228 ms | | 223 ms | CPU | +| | 3 | 26 | 3.74 s | 4.44 s | | 4.29 s | CPU | +| | 1 | 26 | 12.3 s | 14.2 s | | 14.1 s | CPU | +| 32-thread CPU sharing 128 GiB with its integrated GPU (gfx1151) | 3 | 18 | 16.5 ms | | 8.3 ms | 8.1 ms | GPU | +| | 3 | 22 | 205 ms | | 108 ms | 102 ms | GPU | +| | 3 | 28 | 9.81 s | | 8.47 s | 8.14 s | GPU | +| | 2 | 28 | 10.1 s | | 8.46 s | 8.32 s | GPU | +| | 1 | 24 | 1.51 s | | 2.30 s | 2.39 s | CPU | +| 14-core aarch64 CPU sharing 122 GiB with its integrated GPU (compute capability 11.0) | 3 | 22 | 575 ms | 366 ms | | 363 ms | CPU | +| | 3 | 28 | 22.5 s | 27.7 s | | 27.7 s | CPU | +| | 1 | 28 | 62.5 s | 73.0 s | | 72.9 s | CPU | + +The laptop's GPU is never faster as a whole job from 22 qubits up, at any +fusion width (its binary64 rate, not the copies, bounds it now), so `auto` +keeps every state on its CPU. The integrated GPU beat the CPU in every run at +fusion 3 and 2 from 18 to 28 qubits, and lost from 22 qubits at fusion 1, so +`auto` places fused jobs there; discovering its members takes 1.2 s once per +process, more than the saving below 28 qubits, so a process places one there +by `auto` from 18 qubits once its members are discovered (a service, a session, +or a job that asked for one) and a fresh one only at 28. On the aarch64 board +(2026-10-31) the GPU, by CUDA and by OpenCL alike, won some rows below 24 +qubits and lost every row from 26 qubits at every fusion width, so no size has +a faster tail and `auto` keeps its states on the CPU. Every other host, member or fusion width stays on the CPU. ## Tensor networks diff --git a/wai/wai-quantum/README.zh-CN.md b/wai/wai-quantum/README.zh-CN.md index ec60075d1..4a66677f0 100644 --- a/wai/wai-quantum/README.zh-CN.md +++ b/wai/wai-quantum/README.zh-CN.md @@ -93,7 +93,7 @@ wai-quantum lab qutrit # d = 3,不只是量子比特 | `quantum_vml`, `quantum_phasor`, `quantum_qfhrr`, `quantum_kernel`, `quantum_qdata`, `quantum_phasor_meter` | 干涉 / 相量机器学习层 | | `quantum_fabric` | **计算结构**:成员(CPU 和各类加速器,其中包括量子处理器),每个内核实现都在使用它的进程中对照 CPU 参考实现进行认证(逐位精确、在给定容差内,或在给定采样次数上按经典保真度进行统计认证),证书注明认证是在何处获得的(硅片、仿真器或模拟器);按一致性(parity)、来源(provenance)和计量能耗进行放置;签名回执写明结果背后的成员与实现。两个内核:apply(线路的态)和 sample(其测量计数),后者配有 CPU 参考采样器、为量子处理器设计的语料库,以及一个虚拟量子处理器,其给定的噪声模型使处理器路径上的每一步都可以在没有真实处理器的情况下运行和测试;其证书注明是在模拟器中获得的。`quantum_fabric::stablehlo` 把门写成可移植的张量程序(StableHLO),每个值都是永不溢出的 64 位整数,供任何张量编译器为其自己的设备构建;`stablehlo_gate` 示例把它连同一批输入以及构建结果必须给出的精确输出一起写出,`ci/tensor-program/check.sh` 运行一次构建并以这些输出对其检验 | | `quantum_fabric_webgpu` | **计算结构的 WebGPU 成员**,需显式启用,不包含在 `full` 中:以 WGSL 编写的 32 位通道门内核,每个门一次调度(dispatch),原生环境下经由 Metal、Vulkan 或 DX12,在 `wasm32-unknown-unknown` 上经由浏览器的 WebGPU;第二种实现按分块(tile)逐块施加一串门,每串一次调度,并单独认证。每个适配器都在使用它的进程中对照 CPU 的算术进行探测,并通过复现 CPU 参考实现固定的语料库摘要被认证为逐位精确;超出 32 位的结果会被拒绝,绝不返回。属于触及 crate 之外的特性之一 | -| `quantum_fabric_cuda` | **计算结构的 CUDA 成员**,需显式启用,不包含在 `full` 中,仅限原生目标:以 CUDA C 编写、使用精确 64 位整数的门内核,在运行时为每个设备自身的架构编译,每个门一次启动(launch)。CUDA 驱动和运行时编译器在首次使用时加载,因此启用该特性的构建可以在没有 CUDA 的宿主上运行,此时找不到成员,并说明原因。每个设备都在使用它的进程中对照 CPU 的算术进行探测,并通过复现 CPU 参考实现固定的语料库摘要被认证为逐位精确;超出 32 位的结果会被拒绝,绝不返回。在计算能力 8.0 或更高的设备上,**第二个内核在张量核心上计算每个乘积**(`CudaMember::tensor`):每个值表示为 bf16 中的四个 8 位分段(limb),在 f32 中累加,f32 能精确容纳每个部分和;以自己的实现 id 进行探测和认证,并有自己的变异体(mutant)。同时启用 `quantum_sv` 时,**稠密 f64 态矢量**在设备上的运行与在 CPU 上逐位相同:即 CPU 自己的那些启动(对角、单量子比特和分组公式),每一次都是不含融合乘加的 IEEE binary64 运算,并对照固定的语料库摘要进行认证 | +| `quantum_fabric_cuda` | **计算结构的 CUDA 成员**,需显式启用,不包含在 `full` 中,仅限原生目标:以 CUDA C 编写、使用精确 64 位整数的门内核,在运行时为每个设备自身的架构编译,每个门一次启动(launch)。CUDA 驱动和运行时编译器在首次使用时加载,因此启用该特性的构建可以在没有 CUDA 的宿主上运行,此时找不到成员,并说明原因。每个设备都在使用它的进程中对照 CPU 的算术进行探测,并通过复现 CPU 参考实现固定的语料库摘要被认证为逐位精确;超出 32 位的结果会被拒绝,绝不返回。在计算能力 8.0 或更高的设备上,**第二个内核在张量核心上计算每个乘积**(`CudaMember::tensor`):每个值表示为 bf16 中的四个 8 位分段(limb),在 f32 中累加,f32 能精确容纳每个部分和;以自己的实现 id 进行探测和认证,并有自己的变异体(mutant)。同时启用 `quantum_sv` 时,**稠密 f64 态矢量**在设备上的运行与在 CPU 上逐位相同:即 CPU 自己的那些启动(对角、单量子比特和分组公式),每一次都是不含融合乘加的 IEEE binary64 运算,并对照固定的语料库摘要进行认证。同时启用 `quantum_spd` 时,**稀疏 Pauli 动力学的项之和驻留在设备上**(`CudaMember::spd`、`quantum_spd::device`),对照 CPU 的传播进行认证,结果和最终的表都相同 | | `quantum_fabric_metal` | **计算结构的 Metal 成员**,需显式启用,不包含在 `full` 中,macOS:以 Metal 着色语言编写、使用精确 64 位整数的门内核,在运行时由操作系统自带的编译器编译,在一个命令缓冲区中每个门一次串行调度,态存放在 GPU 与宿主共享的内存中。在其他平台上,构建找不到成员并说明原因。每个设备都在使用它的进程中对照 CPU 的算术进行探测,并通过复现 CPU 参考实现固定的语料库摘要被认证为逐位精确,以其 GPU 家族编号为键(`gpu-msl-`)。在支持 SIMD 组矩阵的 GPU 上,**第二个内核用矩阵乘法计算每个乘积**(`MetalMember::matrix`):每个值表示为 bf16 中的四个 8 位分段,在 f32 中累加,f32 能精确容纳每个部分和;以自己的实现 id 进行探测和认证,并有自己的变异体。在操作系统 26 或更高版本上,**第三个内核用着色语言的张量运算计算每个乘积**(`MetalMember::tensor_ops`),平台会在 GPU 具备矩阵硬件时将其映射到该硬件上:bf16 中的偏移二进制 8 位分段,在 f32 中累加,经由每个线程组的暂存区 | | `quantum_fabric_hip` | **计算结构的 HIP 成员**,需显式启用,不包含在 `full` 中,Linux:CUDA 成员的内核源文本,原封不动,在运行时由 HIP 的运行时编译器为每个设备自身的架构编译,每个门一次启动。运行时及其编译器在首次使用时加载,因此启用该特性的构建可以在没有它们的宿主上运行,此时找不到成员,并说明原因;设备属性按运行时冻结的布局读取,否则拒绝该设备。与 CUDA 成员一样进行探测和认证,使用自己的实现 id;同时启用 `quantum_sv` 时,稠密 f64 态矢量的运行与在 CPU 上逐位相同。在 gfx11 GPU 上,**第二个内核用 bf16 矩阵指令计算每个乘积**(`HipMember::wmma`):每个值采用偏移二进制,表示为四个无符号 8 位分段,因为一旦和中出现负项,该指令的累加就不再精确;偏移量在 64 位整数中去除。以自己的实现 id 进行探测和认证,并有自己的变异体 | | `quantum_fabric_opencl` | **计算结构的 OpenCL 成员**,需显式启用,不包含在 `full` 中,仅限原生目标:同样精确的 64 位内核,以 OpenCL C 编写,在运行时由每个设备自己的编译器构建,每个门一次启动,可运行在任何 OpenCL GPU 或 CPU 上(OpenCL 加速器会被拒绝:没有任何计算结构类别对应它)。ICD 加载器在首次使用时打开,因此启用该特性的构建可以在没有 OpenCL 的宿主上运行,此时找不到成员,并说明原因。每个设备都像 CUDA 成员的设备那样进行探测和认证,并以其厂商 id 为键(`opencl--`)。同时启用 `quantum_sv` 时,在每个支持 binary64 的设备上(关闭浮点收缩(contraction)),稠密 f64 态矢量的运行与在 CPU 上逐位相同;不支持 binary64 的设备会拒绝,并说明这一点 | @@ -127,6 +127,8 @@ cargo run --release --example utility --features quantum_spd -- 0.2 20 1e-4 它在不到一秒内算出 127 量子比特 heavy-hex 踢动 Ising 线路经过 20 个 Trotter 步之后的 ⟨Z₆₂⟩,并给出截断可能使其偏移多远的上界。`--example utility_mz` 针对每个角度,把 5 步磁化强度与精确计算值进行核对,`tests/utility_reference.rs` 保存了已发表的参考值。`quantum_bptn` 以张量网络计算同样的线路(`--example utility_bptn`),因此结果可以用第二种方法核对,而这种方法并不共享第一种方法的近似。 +**在 GPU 上,与 CPU 逐位一致**(`quantum_spd::device`)。项之和可以驻留在设备上:设备对所有项同时执行每一步的算术(乘以 `sin` 一次、乘以 `cos` 一次、新项遇到旧项时相加一次、改变符号;构建时不使用融合乘加),而决定项集合的一切由主机掌握。一次旋转产生的新项在表中查找(包括已失效的槽位),其余的按其来源项的顺序、经保序前缀和追加到表尾;主机清点失效项和存活项,并在与 CPU 完全相同的时刻压缩;截断按表序交回所丢弃的数值,误差上界和结果值在主机上按表序相加。`HostDevice` 在 CPU 上执行这一协议,并与 CPU 的传播逐位一致(包括最终的表)。CUDA 成员(`CudaSpd`,配合 `quantum_fabric_cuda`)在固定语料(`SPD_DEFAULT`:40 个用例,占一到四个字,含项数与权重上限,以及被复活和被抵消的项)上对照 CPU 认证,每个值、上界、所达到的阈值、计数和最终的表都相同;`pauli` 作业用 `fabric`(`accelerator`,或成员的 id 或键)请求它,得到与 CPU 逐字节相同的答案,结果写明该成员。`auto` 把 `pauli` 方法留在 CPU 上:没有任何成员被测得对它更快。 + ## 容错资源 一个程序在尚不存在的机器上需要什么,就是一份资源估算;而关于那台机器的种种说法,都以这样一份估算为依据。`quantum_resource` 使估算成为其输入的确定性函数: @@ -350,7 +352,9 @@ wai-quantum fabric # 本机上态矢量可放 wai-quantum run circuit.qasm --method statevector --fabric cpu --fabric cpu --fabric cpu --fabric cpu --shots 100 --json > split.json ``` -作业的 `fabric` 把 `statevector` 方法的态放到计算结构的成员上(`engine::fabric`)。编译进 CUDA、HIP 或 OpenCL 成员(`quantum_fabric_cuda`、`quantum_fabric_hip`、`quantum_fabric_opencl`,配合 `quantum_engine`)后,本进程找到的每个设备都运行稠密 f64 态矢量(`quantum_fabric::sv64`),并在固定语料上对照 CPU 认证;只有认证为逐位一致的成员才会被放置,因此放到其上的作业给出与 CPU 逐字节相同的答案。默认的 `auto` 在每台主机上都把态放在 CPU 上,直到测得某个成员对作业比 CPU 更快;只有作业请求成员时,成员才运行它。`accelerator` 把它放到第一个能容纳它的已认证成员上(GPU 优先),若该成员在运行中失败,由下一个接替;`cpu` 或成员的 id 或键把它放到那里;成员列表(数量为 2 的幂)按最高位量子比特把态划分为若干分区,每个成员一个分区。每个分区在自己的振幅上运行 CPU 规划的同一组启动;作用于分区量子比特的门,先经主机上的态副本把该量子比特与一个本地量子比特交换(`quantum_fabric::sv64::run_split`),因此拼合后的态与 CPU 的逐位相同。结果的 `fabric` 指明成员(或每个分区的成员)及其证书的一致性等级、摘要和语料,并由收据封存;未请求成员、也未放置时,结果与以往相同。每个成员说明它能容纳的最多量子比特数,超过此数的态或分区会被拒绝,主机无法兑现的任何放置也会被拒绝(`J013`)。在本集群的 GPU(一块笔记本 GPU 和一块集成 GPU,各与一颗 CPU 相邻)上测得:每个门一次启动、态在设备与主机之间往返拷贝时,在 18 到 22 个量子比特之间,整个作业两种方式耗时相近,或在 GPU 上更长,拷贝的开销与节省的遍历相当。这就是 `auto` 留在 CPU 上的原因。 +作业的 `fabric` 把 `statevector` 方法的态放到计算结构的成员上(`engine::fabric`)。编译进 CUDA、HIP 或 OpenCL 成员(`quantum_fabric_cuda`、`quantum_fabric_hip`、`quantum_fabric_opencl`,配合 `quantum_engine`)后,本进程找到的每个设备都以驻留方式运行稠密 f64 态矢量(`quantum_fabric::sv64`):态在设备上清零(分区时则上传一次),所有门的参数作为一张表一次上传,每个门按 CPU 的启动顺序在设备上运行、其间不等待,态最后读回一次。每个成员在固定语料上对照 CPU 认证,其一次作用于三个以上量子比特的门(融合宽度 4 到 6,或更宽的门)所用的内核另有自己的语料(`sv64::SV_WIDE`,120 个用例);只有认证为逐位一致的成员才会被放置,且只用于它已认证的内核,因此放到其上的作业给出与 CPU 逐字节相同的答案。默认的 `auto` 只按测量结果把态放到成员上(见下文),否则留在 CPU 上;作业请求成员时,成员运行它。`accelerator` 把它放到第一个能容纳它的已认证成员上(GPU 优先),若该成员在运行中失败,由下一个接替;`cpu` 或成员的 id 或键把它放到那里;成员列表(数量为 2 的幂)按最高位量子比特把态划分为若干分区,每个成员一个分区。每个分区在自己的振幅上运行 CPU 规划的同一组启动;作用于分区量子比特的门,先经主机上的态副本把该量子比特与一个本地量子比特交换(`quantum_fabric::sv64::run_split`),因此拼合后的态与 CPU 的逐位相同。结果的 `fabric` 指明成员(或每个分区的成员)及其证书的一致性等级、摘要和语料,并由收据封存;未请求成员、也未放置时,结果与以往相同。每个成员说明它能容纳的最多量子比特数,超过此数的态或分区会被拒绝,主机无法兑现的任何放置也会被拒绝(`J013`)。 + +**按测量结果的 `auto`**(`engine::fabric_measured`)。在 CPU 和每个成员上对同一族线路(每个量子比特十层任意角度旋转,加砖墙式纠缠对,1000 次采样)的完整作业计时,二者交替各运行五次,融合宽度为 3、2 和 1,从 18 个量子比特到主机一半内存所能容纳的规模;该表连同每台主机、其负载和日期都在源代码中,`cargo run --release --example fabric_timing` 可以重新测量。若成员上的每次运行都快于 CPU 上的每次运行,则该行记为“更快”。`auto` 从这样一个最小的测量规模开始把态放到成员上:从它到最大测量规模的每一行都更快;上限为最大测量规模,只用于所测的融合宽度,且作业的规划至少有所测线路的启动次数;尚未发现其成员的进程,只在节省的时间还能抵消表中所列的发现开销时才这样放置。由 `auto` 放置的作业,其收据带有规则及其所依据的那一行(`fabric.auto`),`wai-quantum fabric` 打印规则、它在本机的放置范围以及每个测量行。测量结果(中位数,2026-10-30):在笔记本上(20 线程 CPU,计算能力 8.9 的 GPU),从 22 个量子比特起,在任何融合宽度下,GPU 作为完整作业都不更快(融合宽度 3、26 个量子比特:CPU 3.74 秒,CUDA 4.44 秒,OpenCL 4.29 秒),因此 `auto` 把每个态都留在其 CPU 上。在与集成 GPU(gfx1151)共享 128 GiB 内存的 32 线程 CPU 上,融合宽度 3 和 2 时,从 18 到 28 个量子比特,GPU 的每次运行都快于 CPU(融合宽度 3、22 个量子比特:CPU 205 毫秒,HIP 108 毫秒,OpenCL 102 毫秒;28 个量子比特:9.81 秒对 8.47 秒和 8.14 秒),融合宽度 1 时从 22 个量子比特起则更慢;发现其成员每个进程需 1.2 秒,超过 28 个量子比特以下的节省,因此已发现成员的进程从 18 个量子比特起由 `auto` 放到 GPU 上,新进程只在 28 个量子比特时这样做。在与集成 GPU(计算能力 11.0)共享 122 GiB 内存的 14 核 aarch64 CPU 上(2026-10-31),GPU(经 CUDA 和经 OpenCL 都一样)在 24 个量子比特以下赢了部分测量行,但从 26 个量子比特起在每个融合宽度下都更慢(融合宽度 3、28 个量子比特:CPU 22.5 秒,CUDA 和 OpenCL 都是 27.7 秒),因此没有任何规模之后的各行都更快,`auto` 把其态留在 CPU 上。其他主机、成员或融合宽度都留在 CPU 上。 ## 张量网络 diff --git a/wai/wai-quantum/examples/fabric_timing.rs b/wai/wai-quantum/examples/fabric_timing.rs new file mode 100644 index 000000000..701450dde --- /dev/null +++ b/wai/wai-quantum/examples/fabric_timing.rs @@ -0,0 +1,151 @@ +//! The measurements `auto` places a state vector by +//! (`engine::fabric_measured`), taken again on this host: whole jobs of the +//! measured circuit on the CPU and on each member found here, their runs +//! alternating, at each fusion width and qubit count asked, printed as the +//! table's rows. +//! +//! ```sh +//! cargo run --release --example fabric_timing \ +//! --features quantum_engine,quantum_fabric,quantum_fabric_cuda,quantum_fabric_opencl -- \ +//! --runs 5 --qubits 18,20,22,24,26 --fusion 2,3 [--members key,key] +//! ``` +//! +//! Every placed job's answers are checked against the CPU's, byte for byte, +//! before its time is kept. A size whose state would not fit in half the +//! host's free memory, or past what a member holds, is left out and said so. + +use serde_json::Value; +use std::time::Instant; +use wai_quantum::engine::{catalog, fabric, fabric_measured, receipt, run}; + +fn arg(name: &str) -> Option { + let args: Vec = std::env::args().collect(); + args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned()) +} + +fn list(name: &str, default: &str) -> Vec { + arg(name).unwrap_or_else(|| default.to_string()).split(',').filter(|s| !s.is_empty()).map(|s| s.parse().unwrap_or_else(|_| panic!("{name}: {s}"))).collect() +} + +/// `MemAvailable` from the kernel, in bytes, or 8 GiB where it cannot be read. +fn available() -> u64 { + std::fs::read_to_string("/proc/meminfo") + .ok() + .and_then(|m| m.lines().find(|l| l.starts_with("MemAvailable")).and_then(|l| l.split_whitespace().nth(1)).and_then(|k| k.parse::().ok())) + .map_or(8 << 30, |kib| kib << 10) +} + +fn load() -> String { + std::fs::read_to_string("/proc/loadavg").map(|l| l.split_whitespace().take(3).collect::>().join(" ")).unwrap_or_else(|_| "unread".into()) +} + +/// The answers, without where the state ran and how long it took. +fn answers(out: &Value) -> Vec { + let mut o = out.clone(); + if let Some(m) = o.as_object_mut() { + m.remove("fabric"); + m.remove("elapsed_ms"); + } + receipt::canonical(&o) +} + +/// One timed job, and its answers. +fn timed(v: &Value, cap: u64) -> (f64, Vec) { + let t = Instant::now(); + let p = catalog::plan(v, cap).unwrap_or_else(|e| panic!("plan: {e}")); + let out = run::run(v, &p).unwrap_or_else(|e| panic!("run: {e}")); + (t.elapsed().as_secs_f64() * 1e3, answers(&out)) +} + +fn spread(v: &[f64]) -> (f64, f64, f64) { + let mut s = v.to_vec(); + s.sort_by(f64::total_cmp); + let median = if s.len() % 2 == 1 { s[s.len() / 2] } else { (s[s.len() / 2 - 1] + s[s.len() / 2]) / 2.0 }; + (s[0], median, s[s.len() - 1]) +} + +fn main() { + let runs: usize = arg("--runs").map_or(5, |r| r.parse().expect("--runs")); + let qubits: Vec = list("--qubits", "18,20,22,24,26"); + let fusions: Vec = list("--fusion", "3"); + let only: Vec = list("--members", ""); + let cap = available() / 2; + let t = Instant::now(); + let listed = fabric::list(cap); + let discovery_ms = t.elapsed().as_secs_f64() * 1e3; + let host = fabric::host(); + println!("host {host} discovery_ms {discovery_ms:.0} load {} memory_available {} MiB", load(), available() >> 20); + let members: Vec<(String, String, u64)> = listed["members"] + .as_array() + .into_iter() + .flatten() + .filter(|m| m["key"] != "cpu") + .map(|m| (m["member"].as_str().unwrap_or("").to_string(), m["key"].as_str().unwrap_or("").to_string(), m["detail"]["max_qubits"].as_u64().unwrap_or(0))) + .filter(|(_, key, _)| only.is_empty() || only.contains(key)) + .collect(); + for (id, key, max) in &members { + println!("member {key} {id} up to {max} qubits"); + } + let mut rows = Vec::new(); + let mut warmed = false; + for &fusion in &fusions { + for &n in &qubits { + // The CPU's state, a member's copy back and the job's own work, + // with room to spare. + if (1u64 << n) * 16 * 4 > cap { + println!("skip fusion={fusion} qubits={n}: the state does not fit in half the free memory"); + continue; + } + let here: Vec<&(String, String, u64)> = members.iter().filter(|m| m.2 >= u64::from(n)).collect(); + if here.is_empty() { + println!("skip fusion={fusion} qubits={n}: no member holds it"); + continue; + } + let cpu_job = fabric_measured::job(n, fusion, "cpu"); + let launches = fabric::launches(&cpu_job).expect("the job plans"); + if !warmed { + timed(&cpu_job, cap); + for m in &here { + timed(&fabric_measured::job(n, fusion, &m.0), cap); + } + warmed = true; + } + let before = load(); + let mut cpu = Vec::new(); + let mut per: Vec> = vec![Vec::new(); here.len()]; + for _ in 0..runs { + let (ms, want) = timed(&cpu_job, cap); + cpu.push(ms); + for (i, m) in here.iter().enumerate() { + let (ms, got) = timed(&fabric_measured::job(n, fusion, &m.0), cap); + assert_eq!(got, want, "{}: fusion {fusion}, {n} qubits: the answers are not the CPU's", m.1); + per[i].push(ms); + } + } + let after = load(); + let c = spread(&cpu); + for (i, m) in here.iter().enumerate() { + let g = spread(&per[i]); + println!( + "row key={} fusion={fusion} qubits={n} launches={launches} runs={runs} cpu={:.1}/{:.1}/{:.1} member={:.1}/{:.1}/{:.1} faster={} load {before} -> {after}", + m.1, + c.0, + c.1, + c.2, + g.0, + g.1, + g.2, + g.2 < c.0 + ); + rows.push(format!( + " Row {{ host: {host:?}, member: {:?}, key: {:?}, fusion: {fusion}, qubits: {n}, launches: {launches}, runs: {runs}, cpu_ms: Spread {{ min: {:.1}, median: {:.1}, max: {:.1} }}, member_ms: Spread {{ min: {:.1}, median: {:.1}, max: {:.1} }} }},", + m.0, m.1, c.0, c.1, c.2, g.0, g.1, g.2 + )); + } + } + } + println!("rows:"); + for r in rows { + println!("{r}"); + } +} diff --git a/wai/wai-quantum/schemas/job-request.v1.json b/wai/wai-quantum/schemas/job-request.v1.json index 04ccfb74f..96c48c65a 100644 --- a/wai/wai-quantum/schemas/job-request.v1.json +++ b/wai/wai-quantum/schemas/job-request.v1.json @@ -173,7 +173,7 @@ ] }, "fabric": { - "description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", + "description": "Where the statevector method's state runs: auto (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.", "anyOf": [ { "type": "string", diff --git a/wai/wai-quantum/schemas/job-result.v1.json b/wai/wai-quantum/schemas/job-result.v1.json index 963d668e4..23d4156a7 100644 --- a/wai/wai-quantum/schemas/job-result.v1.json +++ b/wai/wai-quantum/schemas/job-result.v1.json @@ -1121,15 +1121,150 @@ "type": "integer", "minimum": 0 }, + "wide": { + "$ref": "#/$defs/fabric_wide" + }, "why": { "type": "string", "description": "For a member the job fell back from: how it failed." } } }, + "fabric_wide": { + "type": "object", + "description": "A member's certificate for gates on more than three qubits at once (wai.quantum.sv/run-wide): a fusion width of 4 to 6, or a wider gate, runs kernels the pinned corpus does not reach, certified on a corpus of their own (sv64-wide-v1). A member is placed for such a job only when this certificate is bit-exact; one that is not, or has none, says why and runs only gates on up to three qubits at once.", + "required": [ + "parity" + ], + "properties": { + "kernel": { + "type": "string" + }, + "parity": { + "$ref": "#/$defs/fabric_parity" + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "why": { + "type": "string" + } + } + }, + "fabric_auto": { + "type": "object", + "description": "Why auto placed the state where it ran: the rule (a member is placed only where the committed measurements show it faster than this host's CPU for the job's size and fusion width) and the measured row it rests on, with the host, the day and the load it was measured under. Present only when auto placed a state on a member.", + "required": [ + "rule", + "host", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string", + "description": "The host's processor as the measurements name it (cpu--)." + }, + "measured": { + "type": "string", + "description": "The day the host was measured (UTC)." + }, + "about": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number", + "description": "What discovering and certifying the host's members took, once per process, as measured." + }, + "fusion": { + "type": "integer", + "minimum": 1 + }, + "from_qubits": { + "type": "integer", + "minimum": 0 + }, + "to_qubits": { + "type": "integer", + "minimum": 0 + }, + "min_launches": { + "type": "integer", + "minimum": 0 + }, + "basis": { + "type": "object", + "description": "The measured row the job is judged by: the largest measured size at or below its own, the launches of its plan, the runs of each, and the spread of the CPU's and the member's times in milliseconds.", + "required": [ + "qubits", + "launches", + "runs", + "cpu_ms", + "member_ms" + ], + "properties": { + "qubits": { + "type": "integer", + "minimum": 0 + }, + "launches": { + "type": "integer", + "minimum": 0 + }, + "runs": { + "type": "integer", + "minimum": 0 + }, + "cpu_ms": { + "$ref": "#/$defs/fabric_spread" + }, + "member_ms": { + "$ref": "#/$defs/fabric_spread" + } + } + } + } + }, + "fabric_spread": { + "type": "object", + "description": "The least, the median and the most of a measured row's runs, in milliseconds.", + "required": [ + "min", + "median", + "max" + ], + "properties": { + "min": { + "type": "number" + }, + "median": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, "fabric": { "type": "object", - "description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", + "description": "Where the state vector, or the pauli method's sum of terms, ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement, or when auto placed the state on a member by measurement (`auto`); a job auto leaves on the CPU names nothing; the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.", "required": [ "kernel", "asked", @@ -1182,6 +1317,9 @@ "type": "integer", "minimum": 0 }, + "wide": { + "$ref": "#/$defs/fabric_wide" + }, "partitions": { "type": "array", "description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.", @@ -1214,6 +1352,9 @@ "$ref": "#/$defs/fabric_member" } }, + "auto": { + "$ref": "#/$defs/fabric_auto" + }, "energy": { "$ref": "#/$defs/energy" }, diff --git a/wai/wai-quantum/schemas/tools.v1.json b/wai/wai-quantum/schemas/tools.v1.json index 7746a3812..2a89d287f 100644 --- a/wai/wai-quantum/schemas/tools.v1.json +++ b/wai/wai-quantum/schemas/tools.v1.json @@ -2241,7 +2241,239 @@ }, "auto": { "type": "string", - "description": "Where auto places a state: on the CPU, until a member is measured faster than the CPU." + "description": "Where auto places a state: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion; everywhere else on the CPU." + }, + "auto_rule": { + "type": "object", + "description": "The rule auto places by and what it rests on: this host (as the measurements name it), whether it was measured, where the rule places on it for the members found here, and every measured host with its rows (each with the spread of the CPU's and the member's times and whether the member was faster).", + "required": [ + "rule", + "host", + "places", + "hosts" + ], + "properties": { + "rule": { + "type": "string" + }, + "host": { + "type": "string" + }, + "measured_here": { + "type": "boolean" + }, + "places": { + "type": "array", + "items": { + "type": "object", + "required": [ + "member", + "fusion", + "from_qubits", + "to_qubits" + ], + "properties": { + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "fusion": { + "type": "integer" + }, + "from_qubits": { + "type": "integer" + }, + "to_qubits": { + "type": "integer" + }, + "min_launches": { + "type": [ + "integer", + "null" + ] + } + } + } + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "required": [ + "host", + "measured", + "rows" + ], + "properties": { + "host": { + "type": "string" + }, + "about": { + "type": "string" + }, + "measured": { + "type": "string" + }, + "load": { + "type": "string" + }, + "discovery_ms": { + "type": "number" + }, + "rows": { + "type": "array", + "items": { + "type": "object", + "required": [ + "member", + "fusion", + "qubits", + "cpu_ms", + "member_ms", + "faster" + ], + "properties": { + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "fusion": { + "type": "integer" + }, + "qubits": { + "type": "integer" + }, + "launches": { + "type": "integer" + }, + "runs": { + "type": "integer" + }, + "cpu_ms": { + "$ref": "job-result.v1.json#/$defs/fabric_spread" + }, + "member_ms": { + "$ref": "job-result.v1.json#/$defs/fabric_spread" + }, + "faster": { + "type": "boolean" + } + } + } + } + } + } + } + } + }, + "pauli": { + "type": "object", + "description": "The members the pauli method's sum of terms can be placed on here (wai.quantum.spd/propagate): the CPU and each accelerator certified bit-exact against it on the pinned corpus, result and final table alike; the devices found and not placed, with why; and where auto places it (on the CPU: no member is measured faster for it).", + "required": [ + "kernel", + "members" + ], + "properties": { + "kernel": { + "type": "string" + }, + "auto": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "required": [ + "member", + "implementation", + "parity" + ], + "properties": { + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "class": { + "type": "string" + }, + "implementation": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "corpus": { + "type": "string" + }, + "generator": { + "type": "string" + }, + "parity": { + "$ref": "job-result.v1.json#/$defs/fabric_parity" + }, + "cases": { + "type": "integer", + "minimum": 0 + }, + "detail": { + "type": "object", + "properties": { + "backend": { + "type": "string" + } + } + } + } + } + }, + "refused": { + "type": "array", + "items": { + "type": "object", + "properties": { + "backend": { + "type": "string" + }, + "member": { + "type": "string" + }, + "key": { + "type": "string" + }, + "why": { + "type": "string" + } + } + } + }, + "unavailable": { + "type": "array", + "items": { + "type": "object", + "properties": { + "backend": { + "type": "string" + }, + "why": { + "type": "string" + } + } + } + }, + "backends": { + "type": "array", + "items": { + "type": "string" + } + } + } }, "members": { "type": "array", @@ -2281,6 +2513,9 @@ "type": "integer", "minimum": 0 }, + "wide": { + "$ref": "job-result.v1.json#/$defs/fabric_wide" + }, "detail": { "type": "object", "description": "The most qubits it holds and what bounds it; for an accelerator, its backend and what discovery found.", diff --git a/wai/wai-quantum/src/engine/fabric.rs b/wai/wai-quantum/src/engine/fabric.rs index 937ef9a69..d1de7a484 100644 --- a/wai/wai-quantum/src/engine/fabric.rs +++ b/wai/wai-quantum/src/engine/fabric.rs @@ -17,13 +17,17 @@ //! named more than once, and `cpu` is a member). A placement this host //! cannot honour is refused ([`super::refusal::FABRIC`]), never made //! somewhere else. -//! - **Auto stays on the CPU** until a member is measured faster than the CPU -//! for a job. With one launch per gate and the state copied to the device -//! and back, a whole job measured about as fast, or slower, either way -//! between 18 and 22 qubits on the fleet's GPUs, so `auto` places nothing -//! and looks for no member: a job that does not ask runs as it always did, -//! on every host, and its result has no `fabric`. A member runs a job only -//! when the job asks for one. +//! - **Auto by measurement** ([`super::fabric_measured`]). `auto` places a +//! state on a member only where the committed measurements show that member +//! faster than this host's CPU (every run on it quicker than every run on +//! the CPU) for the job's size and fusion width, and the job's plan has at +//! least the launches of the measured circuit (a process that has not yet +//! discovered its members places one only where the saving also covers that +//! discovery, as measured); the result's `fabric` then says so, with the rule and the measured row it rests on, and a member +//! that fails during the run hands the job back to the CPU. Everywhere else, +//! on every host the table does not name, `auto` keeps the state on the CPU +//! and looks for no member: such a job runs as it always did, and its +//! result has no `fabric`. //! - **Accelerator.** `accelerator` takes the first certified member that //! holds the state, GPUs first. One that fails during the run hands the job //! to the next that holds it, and the receipt lists each that failed @@ -41,6 +45,14 @@ //! rest of the result. //! - **Memory.** Each member states the most qubits it holds ([`list`]); a //! state or a partition past that is refused. +//! - **The `pauli` method.** A job of sparse Pauli dynamics asks +//! for a member the same way (`accelerator`, or a member's id or key): its +//! sum of terms then lives on the member +//! ([`crate::quantum_spd::device`]), which is placed only when certified +//! bit-exact against the CPU on the pinned corpus, result and table alike, +//! so the answers are the CPU's byte for byte. `auto` keeps it on the CPU: +//! no member is measured faster for it. A split names the statevector +//! method's partitions, and is refused for it. use super::catalog::{Plan, Request}; use super::refusal::{self, tag}; @@ -49,9 +61,22 @@ use serde_json::{Value, json}; /// The kernel a placement runs: a gate list on a dense f64 state. pub const KERNEL: &str = "wai.quantum.sv/run"; -/// Where `auto` places a state: on the CPU, on every host, until a member -/// is measured faster than the CPU for a job. -pub const AUTO: &str = "cpu: auto places on the CPU until a member is measured faster than the CPU; a member runs a job that asks for one"; +/// The kernel a `pauli` job's placement runs: an observable propagated +/// through a rotation circuit, truncated. +pub const KERNEL_PAULI: &str = crate::quantum_spd::device::KERNEL_SPD; + +/// Where `auto` places a `pauli` job's sum of terms. +pub const AUTO_PAULI: &str = "cpu: no member is measured faster than the CPU for the pauli method, so auto keeps its sum of terms on the CPU; a member runs it when the job asks for one"; + +/// The propagation a `pauli` job's run calls for each observable and +/// threshold: the CPU's, or a member's, with the CPU's bits. +pub(crate) type Propagator<'a> = dyn FnMut(&crate::quantum_spd::RotCircuit, &[(f64, crate::quantum_spd::PauliString)], &crate::quantum_spd::SpdConfig, usize) -> Result<(crate::quantum_spd::SpdResult, f64), String> + 'a; + +/// Where `auto` places a state. +pub const AUTO: &str = "measured: auto places a state on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion (every run on it quicker than every run on the CPU, for a plan of at least the measured launches); everywhere else, and on any host not measured, on the CPU"; + +/// The rule `auto` places by, in a sentence. +pub const RULE: &str = "a member is placed from the smallest measured qubit count from which every measured size up to the largest is faster on it than on this host's CPU (its slowest run beats the CPU's quickest), up to that largest, at the fusion width measured, for a job whose plan has at least the launches of the measured circuit at the nearest measured size below; a process that has not yet discovered its members places one only where that saving also covers the discovery the measurements state"; /// The most partitions a state may be split into. pub const MAX_PARTITIONS: usize = 64; @@ -122,6 +147,17 @@ fn widest(r: &Request) -> u32 { /// [`refusal::FABRIC`], when this host cannot honour it. pub(crate) fn admit(v: &Value, r: &Request, plan: &mut Plan) -> Result<(), String> { let ask = asked(v)?; + if plan.method == "pauli" { + return match ask { + Ask::Auto | Ask::Cpu => Ok(()), + Ask::Split(_) => Err(tag(refusal::FABRIC, "fabric: a list of members splits the statevector method's state; the pauli method's sum of terms runs on one member: give accelerator, or a member's id or key")), + _ => { + let said = imp::admit_pauli(&ask)?; + plan.why = format!("{}; {said}", plan.why); + Ok(()) + } + }; + } let placed_method = plan.method == "statevector" && r.program.dynamic.is_none(); if !placed_method { return match ask { @@ -129,10 +165,10 @@ pub(crate) fn admit(v: &Value, r: &Request, plan: &mut Plan) -> Result<(), Strin Ask::Cpu if matches!(plan.method.as_str(), "statevector_single" | "cutrank_single") => Err(tag(refusal::FABRIC, format!("fabric cpu: {} runs on the GPU in single precision; give another method, or leave fabric out", plan.method))), Ask::Cpu => Ok(()), _ if plan.method == "statevector" => Err(tag(refusal::FABRIC, "fabric: a program that measures in the middle, resets or acts on measured bits runs on the CPU; give fabric cpu, or leave it out")), - _ => Err(tag(refusal::FABRIC, format!("fabric: only the statevector method is placed on the fabric's members; this job runs {}. Give method statevector, or leave fabric out", plan.method))), + _ => Err(tag(refusal::FABRIC, format!("fabric: only the statevector and pauli methods are placed on the fabric's members; this job runs {}. Give method statevector or pauli, or leave fabric out", plan.method))), }; } - let placed = imp::resolve(&ask, r.program.n_qubits, widest(r))?; + let placed = imp::resolve(&ask, r.program.n_qubits, widest(r), fusion_of(r), &|| r.program.matrices())?; if let Some(said) = imp::said(&ask, &placed) { plan.why = format!("{}; {said}", plan.why); } @@ -145,10 +181,66 @@ pub(crate) fn admit(v: &Value, r: &Request, plan: &mut Plan) -> Result<(), Strin /// ran. pub(crate) fn statevector(v: &Value, n: u32, gates: &[crate::gates::Gate], width: usize, widest: u32) -> Result<(crate::quantum_sv::StateVector, Option), String> { let ask = asked(v)?; - let placed = imp::resolve(&ask, n, widest)?; + let placed = imp::resolve(&ask, n, widest, width as u32, &|| gates.to_vec())?; imp::run(&ask, &placed, n, gates, width) } +/// The `pauli` method's `job` run where the job `v` asks (as [`admit`] +/// admitted it): `job` is handed the propagation to call, the CPU's or a +/// member's, and the fabric's receipt comes back with its answer when the +/// result is to carry one. A member that fails during the run hands the job +/// to the next certified one (for `accelerator`); when none is left, the job +/// is refused. +pub(crate) fn pauli(v: &Value, threads: usize, job: &mut dyn FnMut(&mut Propagator<'_>) -> Result) -> Result<(T, Option), String> { + let ask = asked(v)?; + imp::pauli(&ask, threads, job) +} + +/// A job's fusion width, as the state vector runs it. +fn fusion_of(r: &Request) -> u32 { + r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 5) as u32 +} + +/// This host's processor as the measurements name it +/// ([`super::fabric_measured`]): `cpu--<12 hex>`, a hash of the +/// processor's model as the operating system reports it, the architecture and +/// the threads it runs. Read once per process. +pub fn host() -> String { + static HOST: std::sync::OnceLock = std::sync::OnceLock::new(); + HOST.get_or_init(|| { + let arch = std::env::consts::ARCH; + let threads = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, |t| t.get()) }; + let mut h = blake3::Hasher::new(); + h.update(b"wai:quantum-host\x01"); + for part in [arch.as_bytes(), cpu_model().as_bytes(), threads.to_string().as_bytes()] { + h.update(&(part.len() as u64).to_be_bytes()); + h.update(part); + } + let d: String = h.finalize().as_bytes()[..6].iter().map(|b| format!("{b:02x}")).collect(); + format!("cpu-{arch}-{d}") + }) + .clone() +} + +/// The processor's model as the operating system reports it, or nothing. +fn cpu_model() -> String { + #[cfg(target_os = "linux")] + if let Ok(info) = std::fs::read_to_string("/proc/cpuinfo") + && let Some(line) = info.lines().find(|l| l.starts_with("model name")) + { + return line.split_once(':').map(|(_, m)| m.trim().to_string()).unwrap_or_default(); + } + String::new() +} + +/// The launches a `statevector` job's plan makes on a member, as `auto`'s +/// rule counts them: its gates planned at its fusion width. +#[cfg(feature = "quantum_fabric")] +pub fn launches(v: &Value) -> Result { + let r = super::catalog::read(v)?; + crate::quantum_fabric::sv64::plan(r.program.n_qubits, &r.program.matrices(), fusion_of(&r) as usize).map(|l| l.len()).map_err(|e| format!("{e:?}")) +} + /// The widest launch the job `r` can make, for [`statevector`]. pub(crate) fn widest_of(r: &Request) -> u32 { widest(r) @@ -162,6 +254,26 @@ pub fn list(job_bytes: u64) -> Value { imp::list(job_bytes) } +/// The rule `auto` places by and what it rests on: this host, where the rule +/// places on it (`places`, for the members found here), and every measured +/// host, with its rows. +fn auto_rule(here: &str, places: Vec) -> Value { + use super::fabric_measured::{HOSTS, ROWS, Spread}; + let spread = |s: &Spread| json!({ "min": s.min, "median": s.median, "max": s.max }); + let hosts: Vec = HOSTS + .iter() + .map(|h| { + let rows: Vec = ROWS + .iter() + .filter(|r| r.host == h.id) + .map(|r| json!({ "member": r.member, "key": r.key, "fusion": r.fusion, "qubits": r.qubits, "launches": r.launches, "runs": r.runs, "cpu_ms": spread(&r.cpu_ms), "member_ms": spread(&r.member_ms), "faster": r.faster() })) + .collect(); + json!({ "host": h.id, "about": h.about, "measured": h.date, "load": h.load, "discovery_ms": h.discovery_ms, "rows": rows }) + }) + .collect(); + json!({ "rule": RULE, "host": here, "measured_here": HOSTS.iter().any(|h| h.id == here), "places": places, "hosts": hosts }) +} + fn cpu_detail(job_bytes: u64) -> Value { json!({ "max_qubits": super::catalog::statevector_max_qubits(job_bytes), "memory": "the job's memory on this host" }) } @@ -185,7 +297,7 @@ mod imp { Cpu, } - pub(super) fn resolve(ask: &Ask, _n: u32, _widest: u32) -> Result { + pub(super) fn resolve(ask: &Ask, _n: u32, _widest: u32, _fusion: u32, _gates: &dyn Fn() -> Vec) -> Result { match ask { Ask::Auto => Ok(Placed::Cpu), _ => Err(tag(refusal::FABRIC, "fabric: this build has no fabric (the engine's quantum_fabric feature); leave fabric out")), @@ -196,6 +308,18 @@ mod imp { None } + pub(super) fn admit_pauli(_ask: &Ask) -> Result { + Err(tag(refusal::FABRIC, "fabric: this build has no fabric (the engine's quantum_fabric feature); leave fabric out")) + } + + pub(super) fn pauli(ask: &Ask, threads: usize, job: &mut dyn FnMut(&mut Propagator<'_>) -> Result) -> Result<(T, Option), String> { + if *ask != Ask::Auto { + return Err(admit_pauli(ask).unwrap_err()); + } + let mut cpu = |c: &crate::quantum_spd::RotCircuit, o: &[(f64, crate::quantum_spd::PauliString)], cfg: &crate::quantum_spd::SpdConfig, max_terms: usize| crate::quantum_spd::expectation_capped(c, o, &[], cfg, threads, max_terms).map_err(|e| e.to_string()); + Ok((job(&mut cpu)?, None)) + } + pub(super) fn run(_ask: &Ask, _p: &Placed, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(crate::quantum_sv::StateVector, Option), String> { let mut sv = crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?; sv.run(gates, width).map_err(|e| format!("{e:?}"))?; @@ -206,6 +330,8 @@ mod imp { json!({ "kernel": KERNEL, "auto": AUTO, + "auto_rule": super::auto_rule(&super::host(), Vec::new()), + "pauli": { "kernel": KERNEL_PAULI, "auto": AUTO_PAULI, "members": [{ "member": "cpu", "key": "cpu", "class": "cpu", "implementation": "reference", "parity": { "tier": "bit-exact" } }], "refused": [], "unavailable": [{ "backend": "fabric", "why": "this build has no fabric (the engine's quantum_fabric feature)" }], "backends": [] }, "members": [{ "member": "cpu", "key": "cpu", "class": "cpu", "implementation": "reference", "parity": { "tier": "bit-exact" }, "detail": cpu_detail(job_bytes) }], "refused": [], "unavailable": [{ "backend": "fabric", "why": "this build has no fabric (the engine's quantum_fabric feature)" }], @@ -221,10 +347,17 @@ mod imp { use crate::quantum_fabric::{Certificate, FabricClass, Parity}; use std::sync::OnceLock; - /// An accelerator that runs the plan's launches on a state it is given. + /// An accelerator that runs the plan's launches on a state it is given, + /// or on `|0…0⟩`, the state on the device for the whole run. pub(super) trait Device: Send + Sync { fn max_qubits(&self) -> u32; fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec, Vec), String>; + fn run_zero(&self, n: u32, launches: &[Launch]) -> Result<(Vec, Vec), String> { + let len = 1usize << n; + let mut re = vec![0.0; len]; + re[0] = 1.0; + self.run(&re, &vec![0.0; len], launches) + } } /// A certified member. @@ -234,6 +367,9 @@ mod imp { class: FabricClass, backend: &'static str, certificate: Certificate, + /// Its certificate for gates on more than [`sv64::NARROW`] qubits at + /// once ([`sv64::KERNEL_SV_RUN_WIDE`]), or why it has none. + wide: Result, detail: String, device: Box, } @@ -242,7 +378,19 @@ mod imp { /// A member for the tests, on a device of their own. #[cfg(test)] pub(super) fn for_tests(key: &str, certificate: Certificate, class: FabricClass, device: Box) -> Accelerator { - Accelerator { id: key.into(), key: key.into(), class, backend: "test", certificate, detail: String::new(), device } + Accelerator { id: key.into(), key: key.into(), class, backend: "test", certificate, wide: Err("not certified in the tests".into()), detail: String::new(), device } + } + + /// Whether its wider kernels certified bit-exact in this process. + fn wide_exact(&self) -> bool { + matches!(&self.wide, Ok(c) if c.parity == Parity::BitExact && c.cases > 0 && c.kernel == sv64::KERNEL_SV_RUN_WIDE && c.member == self.id && c.implementation == self.certificate.implementation) + } + + /// Whether it may run a state of `n` qubits whose launches act on up + /// to `widest` qubits at once: it holds the state, and it is + /// certified bit-exact for every kernel those launches take. + fn holds(&self, n: u32, widest: u32) -> bool { + self.device.max_qubits() >= n && (widest <= sv64::NARROW || self.wide_exact()) } } @@ -252,9 +400,15 @@ mod imp { unavailable: Vec, } + static REGISTRY: OnceLock = OnceLock::new(); + fn registry() -> &'static Registry { - static R: OnceLock = OnceLock::new(); - R.get_or_init(discover) + REGISTRY.get_or_init(discover) + } + + /// Whether this process has discovered its members yet. + fn discovered() -> bool { + REGISTRY.get().is_some() } /// The backends this build reaches. @@ -274,10 +428,10 @@ mod imp { // Unused in a build that reaches no accelerator. #[allow(clippy::too_many_arguments, dead_code)] - fn enrol(r: &mut Registry, backend: &'static str, id: &str, key: &str, class: FabricClass, detail: &str, implementation: &str, cert: Result, device: Box) { + fn enrol(r: &mut Registry, backend: &'static str, id: &str, key: &str, class: FabricClass, detail: &str, implementation: &str, cert: Result, wide: Result, device: Box) { match cert { Ok(c) if c.parity == Parity::BitExact && c.cases > 0 && c.kernel == KERNEL && c.member == id && c.implementation == implementation => { - r.members.push(Accelerator { id: id.into(), key: key.into(), class, backend, certificate: c, detail: detail.into(), device }); + r.members.push(Accelerator { id: id.into(), key: key.into(), class, backend, certificate: c, wide, detail: detail.into(), device }); } Ok(c) => r.refused.push(json!({ "backend": backend, "member": id, "key": key, "why": format!("certified {}, not bit-exact: never placed", tier(&c.parity)["tier"].as_str().unwrap_or("uncertified")) })), Err(e) => r.refused.push(json!({ "backend": backend, "member": id, "key": key, "why": format!("the state vector did not certify: {e}") })), @@ -297,6 +451,9 @@ mod imp { fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec, Vec), String> { self.0.run_launches(re, im, launches).map_err(|e| e.to_string()) } + fn run_zero(&self, n: u32, launches: &[Launch]) -> Result<(Vec, Vec), String> { + self.0.run_from_zero(n, launches).map_err(|e| e.to_string()) + } } let d = crate::quantum_fabric_cuda::discover(); if let Some(why) = d.unavailable { @@ -309,7 +466,8 @@ mod imp { match m.sv() { Ok(sv) => { let (implementation, cert) = (sv.implementation().to_string(), sv.certify(sv64::SV_DEFAULT).map_err(|e| e.to_string())); - enrol(&mut r, "cuda", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, Box::new(Cuda(sv))); + let wide = sv.certify_wide(sv64::SV_WIDE).map_err(|e| e.to_string()); + enrol(&mut r, "cuda", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, wide, Box::new(Cuda(sv))); } Err(e) => r.refused.push(json!({ "backend": "cuda", "member": m.id(), "key": m.key(), "why": e.to_string() })), } @@ -325,6 +483,9 @@ mod imp { fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec, Vec), String> { self.0.run_launches(re, im, launches).map_err(|e| e.to_string()) } + fn run_zero(&self, n: u32, launches: &[Launch]) -> Result<(Vec, Vec), String> { + self.0.run_from_zero(n, launches).map_err(|e| e.to_string()) + } } let d = crate::quantum_fabric_hip::discover(); if let Some(why) = d.unavailable { @@ -337,7 +498,8 @@ mod imp { match m.sv() { Ok(sv) => { let (implementation, cert) = (sv.implementation().to_string(), sv.certify(sv64::SV_DEFAULT).map_err(|e| e.to_string())); - enrol(&mut r, "hip", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, Box::new(Hip(sv))); + let wide = sv.certify_wide(sv64::SV_WIDE).map_err(|e| e.to_string()); + enrol(&mut r, "hip", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, wide, Box::new(Hip(sv))); } Err(e) => r.refused.push(json!({ "backend": "hip", "member": m.id(), "key": m.key(), "why": e.to_string() })), } @@ -353,6 +515,9 @@ mod imp { fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec, Vec), String> { self.0.run_launches(re, im, launches).map_err(|e| e.to_string()) } + fn run_zero(&self, n: u32, launches: &[Launch]) -> Result<(Vec, Vec), String> { + self.0.run_from_zero(n, launches).map_err(|e| e.to_string()) + } } let d = crate::quantum_fabric_opencl::discover(); if let Some(why) = d.unavailable { @@ -369,7 +534,8 @@ mod imp { match m.sv() { Ok(sv) => { let (implementation, cert) = (sv.implementation().to_string(), sv.certify(sv64::SV_DEFAULT).map_err(|e| e.to_string())); - enrol(&mut r, "opencl", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, Box::new(OpenCl(sv))); + let wide = sv.certify_wide(sv64::SV_WIDE).map_err(|e| e.to_string()); + enrol(&mut r, "opencl", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, wide, Box::new(OpenCl(sv))); } Err(e) => r.refused.push(json!({ "backend": "opencl", "member": m.id(), "key": m.key(), "why": e.to_string() })), } @@ -382,6 +548,186 @@ mod imp { r } + /// A member that runs the `pauli` method's propagation on a sum of terms + /// it holds ([`crate::quantum_spd::device`]). + pub(super) trait SpdDev: Send + Sync { + fn run(&self, c: &crate::quantum_spd::RotCircuit, o: &[(f64, crate::quantum_spd::PauliString)], cfg: &crate::quantum_spd::SpdConfig, max_terms: usize) -> Result<(crate::quantum_spd::SpdResult, f64), String>; + } + + /// A member certified for the `pauli` method. + pub(super) struct SpdAccel { + id: String, + key: String, + class: FabricClass, + backend: &'static str, + certificate: Certificate, + device: Box, + } + + struct SpdRegistry { + members: Vec, + refused: Vec, + unavailable: Vec, + } + + /// The `pauli` method's members, discovered and certified the first time + /// a job asks for one (or the fabric is listed), apart from the state + /// vector's, so a state-vector job never pays for them. + fn spd_registry() -> &'static SpdRegistry { + static R: OnceLock = OnceLock::new(); + R.get_or_init(discover_spd) + } + + #[allow(unused_mut)] + fn discover_spd() -> SpdRegistry { + let mut r = SpdRegistry { members: Vec::new(), refused: Vec::new(), unavailable: Vec::new() }; + #[cfg(all(feature = "quantum_fabric_cuda", not(target_arch = "wasm32")))] + { + use crate::quantum_spd::device::{KERNEL_SPD, SPD_DEFAULT, expectation_on}; + struct Cuda(crate::quantum_fabric_cuda::CudaSpd); + impl SpdDev for Cuda { + fn run(&self, c: &crate::quantum_spd::RotCircuit, o: &[(f64, crate::quantum_spd::PauliString)], cfg: &crate::quantum_spd::SpdConfig, max_terms: usize) -> Result<(crate::quantum_spd::SpdResult, f64), String> { + expectation_on(&mut self.0.run(), c, o, &[], cfg, max_terms).map_err(|e| e.to_string()) + } + } + let d = crate::quantum_fabric_cuda::discover(); + if let Some(why) = d.unavailable { + r.unavailable.push(json!({ "backend": "cuda", "why": why })); + } + for x in d.refused { + r.refused.push(json!({ "backend": "cuda", "member": x.device, "key": x.key, "why": x.error.to_string() })); + } + for m in d.found { + match m.spd() { + Ok(spd) => match spd.certify(SPD_DEFAULT) { + Ok(c) if c.parity == Parity::BitExact && c.cases > 0 && c.kernel == KERNEL_SPD && c.member == m.id() && c.implementation == spd.implementation() => { + r.members.push(SpdAccel { id: m.id().into(), key: m.key().into(), class: m.class(), backend: "cuda", certificate: c, device: Box::new(Cuda(spd)) }); + } + Ok(c) => r.refused.push(json!({ "backend": "cuda", "member": m.id(), "key": m.key(), "why": format!("certified {}, not bit-exact: never placed", tier(&c.parity)["tier"].as_str().unwrap_or("uncertified")) })), + Err(e) => r.refused.push(json!({ "backend": "cuda", "member": m.id(), "key": m.key(), "why": format!("the propagation did not certify: {e}") })), + }, + Err(e) => r.refused.push(json!({ "backend": "cuda", "member": m.id(), "key": m.key(), "why": e.to_string() })), + } + } + } + r.members.sort_by(|a, b| (a.class != FabricClass::Gpu, &a.id).cmp(&(b.class != FabricClass::Gpu, &b.id))); + r + } + + /// The backends that run the `pauli` method in this build. + fn spd_backends() -> Vec<&'static str> { + if cfg!(all(feature = "quantum_fabric_cuda", not(target_arch = "wasm32"))) { vec!["cuda"] } else { Vec::new() } + } + + /// The members a `pauli` job's ask names, in the order it tries them. + fn spd_members(ask: &Ask) -> Result, String> { + let reg = spd_registry(); + let here = || { + let mut parts = vec![format!("the pauli method's members are cpu{}", reg.members.iter().map(|m| format!(", {} ({})", m.key, m.id)).collect::())]; + for u in ®.unavailable { + parts.push(format!("{}: {}", u["backend"].as_str().unwrap_or(""), u["why"].as_str().unwrap_or(""))); + } + for x in ®.refused { + parts.push(format!("{} {} not placed: {}", x["backend"].as_str().unwrap_or(""), x["key"].as_str().unwrap_or(""), x["why"].as_str().unwrap_or(""))); + } + if spd_backends().is_empty() { + parts.push("this build reaches no accelerator for it (the engine's quantum_fabric_cuda feature)".into()); + } + parts.join("; ") + }; + match ask { + Ask::Accelerator if reg.members.is_empty() => Err(tag(refusal::FABRIC, format!("fabric accelerator: no member certified for the pauli method here; {}", here()))), + Ask::Accelerator => Ok(reg.members.iter().collect()), + Ask::Member(name) => { + if let Some(m) = reg.members.iter().find(|m| m.id == *name) { + return Ok(vec![m]); + } + let by_key: Vec<&SpdAccel> = reg.members.iter().filter(|m| m.key == *name).collect(); + match by_key[..] { + [m] => Ok(vec![m]), + [] => Err(tag(refusal::FABRIC, format!("fabric: no member {name} for the pauli method here; {}", here()))), + _ => Err(tag(refusal::FABRIC, format!("fabric: {} members have the key {name}; name one by its id ({})", by_key.len(), by_key.iter().map(|m| m.id.as_str()).collect::>().join(", ")))), + } + } + _ => Err(tag(refusal::FABRIC, "fabric: the pauli method's sum of terms runs on the CPU or on one member")), + } + } + + pub(super) fn admit_pauli(ask: &Ask) -> Result { + if matches!(ask, Ask::Member(n) if *n == cpu_id()) { + return Ok("its sum of terms on the CPU, as the job asked".into()); + } + let ms = spd_members(ask)?; + Ok(format!("its sum of terms on {} ({}), certified bit-exact against the CPU", ms[0].key, ms[0].id)) + } + + fn spd_json(m: &SpdAccel) -> Value { + let c = &m.certificate; + json!({ "member": m.id, "key": m.key, "class": m.class.label(), "implementation": c.implementation, "parity": tier(&c.parity), "certificate": hex(&c.hash()), "corpus": hex(&c.corpus), "generator": c.generator, "cases": c.cases }) + } + + fn cpu_spd_json() -> Value { + let id = cpu_id(); + let c = Certificate::reference(KERNEL_PAULI, &id); + json!({ "member": id, "key": "cpu", "class": "cpu", "implementation": crate::quantum_fabric::REFERENCE, "parity": tier(&c.parity), "certificate": hex(&c.hash()), "generator": c.generator, "cases": c.cases }) + } + + pub(super) fn pauli(ask: &Ask, threads: usize, job: &mut dyn FnMut(&mut Propagator<'_>) -> Result) -> Result<(T, Option), String> { + let receipt = |mut body: Value, why: String| -> Value { + body["kernel"] = json!(KERNEL_PAULI); + body["asked"] = ask.json(); + body["energy"] = unmetered(); + body["why"] = json!(why); + body + }; + let mut cpu = |c: &crate::quantum_spd::RotCircuit, o: &[(f64, crate::quantum_spd::PauliString)], cfg: &crate::quantum_spd::SpdConfig, max_terms: usize| crate::quantum_spd::expectation_capped(c, o, &[], cfg, threads, max_terms).map_err(|e| e.to_string()); + let on_cpu = matches!(ask, Ask::Cpu) || matches!(ask, Ask::Member(n) if n == "cpu" || *n == cpu_id()); + match ask { + Ask::Auto => Ok((job(&mut cpu)?, None)), + _ if on_cpu => Ok((job(&mut cpu)?, Some(receipt(cpu_spd_json(), "its sum of terms on the CPU, as the job asked".into())))), + _ => { + let mut failed: Vec<(&SpdAccel, String)> = Vec::new(); + for m in spd_members(ask)? { + let mut dev = |c: &crate::quantum_spd::RotCircuit, o: &[(f64, crate::quantum_spd::PauliString)], cfg: &crate::quantum_spd::SpdConfig, max_terms: usize| m.device.run(c, o, cfg, max_terms); + match job(&mut dev) { + Ok(t) => { + let mut why = format!("its sum of terms on {} ({}), certified bit-exact against the CPU", m.key, m.id); + if !failed.is_empty() { + why = format!("{why}; {} failed during the run first", failed.iter().map(|(f, _)| f.key.as_str()).collect::>().join(", ")); + } + let mut body = receipt(spd_json(m), why); + if !failed.is_empty() { + body["fell_back_from"] = failed + .iter() + .map(|(f, e)| { + let mut j = spd_json(f); + j["why"] = json!(e); + j + }) + .collect(); + } + return Ok((t, Some(body))); + } + Err(e) => failed.push((m, e)), + } + } + Err(tag(refusal::FABRIC, format!("fabric: {}", failed.iter().map(|(f, e)| format!("{} failed during the run: {e}", f.key)).collect::>().join("; ")))) + } + } + } + + /// The `pauli` method's members, as [`super::list`] states them. + fn spd_list() -> Value { + let reg = spd_registry(); + let mut members = vec![cpu_spd_json()]; + for m in ®.members { + let mut j = spd_json(m); + j["detail"] = json!({ "backend": m.backend }); + members.push(j); + } + json!({ "kernel": KERNEL_PAULI, "auto": AUTO_PAULI, "members": members, "refused": reg.refused, "unavailable": reg.unavailable, "backends": spd_backends() }) + } + fn tier(p: &Parity) -> Value { match *p { Parity::BitExact => json!({ "tier": "bit-exact" }), @@ -402,6 +748,8 @@ mod imp { pub(super) enum Placed<'a> { Cpu, + /// Where `auto`'s rule placed a state of so many qubits, and the rule. + Auto(&'a Accelerator, super::super::fabric_measured::Rule, u32), /// The member the state runs on, then those it falls back to in /// turn (for `accelerator`; a member named is alone here). Members(Vec<&'a Accelerator>), @@ -444,17 +792,20 @@ mod imp { parts.join("; ") } - pub(super) fn resolve<'a>(ask: &Ask, n: u32, widest: u32) -> Result, String> { + pub(super) fn resolve<'a>(ask: &Ask, n: u32, widest: u32, fusion: u32, gates: &dyn Fn() -> Vec) -> Result, String> { match ask { - // Auto stays on the CPU and looks for no member (see the module). - Ask::Cpu | Ask::Auto => Ok(Placed::Cpu), + Ask::Cpu => Ok(Placed::Cpu), + Ask::Auto => Ok(auto(n, widest, fusion, gates)), Ask::Accelerator => { let reg = registry(); if reg.members.is_empty() { return Err(tag(refusal::FABRIC, format!("fabric accelerator: no certified accelerator member here; {}", here(reg)))); } - let holds: Vec<&Accelerator> = reg.members.iter().filter(|m| m.device.max_qubits() >= n).collect(); + let holds: Vec<&Accelerator> = reg.members.iter().filter(|m| m.holds(n, widest)).collect(); if holds.is_empty() { + if reg.members.iter().any(|m| m.device.max_qubits() >= n) { + return Err(tag(refusal::FABRIC, format!("fabric accelerator: {}", narrow_only(widest)))); + } return Err(tag(refusal::FABRIC, format!("fabric accelerator: a state of {n} qubits; the largest member here holds {} (give a list of members to split it)", reg.members.iter().map(|m| m.device.max_qubits()).max().unwrap_or(0)))); } Ok(Placed::Members(holds)) @@ -463,7 +814,8 @@ mod imp { let reg = registry(); match named(reg, name)? { Part::Cpu => Ok(Placed::Cpu), - Part::Member(m) if m.device.max_qubits() >= n => Ok(Placed::Members(vec![m])), + Part::Member(m) if m.holds(n, widest) => Ok(Placed::Members(vec![m])), + Part::Member(m) if m.device.max_qubits() >= n => Err(tag(refusal::FABRIC, format!("fabric {name}: {}", narrow_only(widest)))), Part::Member(m) => Err(tag(refusal::FABRIC, format!("fabric {name}: a state of {n} qubits; {} holds at most {} (give a list of members to split it)", m.key, m.device.max_qubits()))), } } @@ -489,6 +841,11 @@ mod imp { { return Err(tag(refusal::FABRIC, format!("fabric: a partition of {local} qubits; {} holds at most {}", m.key, m.device.max_qubits()))); } + if let Part::Member(m) = &part + && !m.holds(local, widest) + { + return Err(tag(refusal::FABRIC, format!("fabric {}: {}", m.key, narrow_only(widest)))); + } parts.push(part); } Ok(Placed::Split(parts)) @@ -496,6 +853,75 @@ mod imp { } } + /// Why no member here runs a job whose gates act on `widest` qubits at once. + fn narrow_only(widest: u32) -> String { + format!( + "this job's gates act on up to {widest} qubits at once (its fusion or its widest gate), and a member runs gates on more than {} at once only where its kernels for them ({}) certified bit-exact here; give fusion {} or less, or another member (wai-quantum fabric lists each member's)", + sv64::NARROW, + sv64::KERNEL_SV_RUN_WIDE, + sv64::NARROW + ) + } + + /// Where `auto` places a state of `n` qubits at `fusion`: on a member + /// only where a rule of this host covers the job (see the module). The + /// table is read before anything is discovered, so a host it does not + /// name looks for no member. + fn auto<'a>(n: u32, widest: u32, fusion: u32, gates: &dyn Fn() -> Vec) -> Placed<'a> { + use super::super::fabric_measured::rules_for; + let rules: Vec<_> = rules_for(&super::host()).into_iter().filter(|r| r.fusion == fusion && (r.from..=r.to).contains(&n)).collect(); + if rules.is_empty() { + return Placed::Cpu; + } + let Ok(launches) = sv64::plan(n, &gates(), fusion as usize).map(|l| l.len()) else { return Placed::Cpu }; + // A process that has not discovered its members yet would pay for + // that first: only a measured saving that covers it is a saving. + let fresh = !discovered(); + let discovery = super::super::fabric_measured::discovery_ms(&super::host()).unwrap_or(f64::INFINITY); + let covered = |r: &super::super::fabric_measured::Rule| r.basis(n).is_some_and(|b| b.saving_ms() >= discovery); + let rules: Vec<_> = rules.into_iter().filter(|r| r.covers(n, launches)).collect(); + if rules.is_empty() || (fresh && !rules.iter().any(covered)) { + return Placed::Cpu; + } + let reg = registry(); + // Of the rules that cover it and whose member is here and holds it, + // the one whose member was quickest at the size it is judged by. + rules + .into_iter() + .filter_map(|r| reg.members.iter().find(|m| m.id == r.member && m.holds(n, widest)).map(|m| (m, r))) + .min_by(|(_, a), (_, b)| { + let t = |r: &super::super::fabric_measured::Rule| r.basis(n).map_or(f64::INFINITY, |b| b.member_ms.median); + t(a).total_cmp(&t(b)) + }) + .map_or(Placed::Cpu, |(m, r)| Placed::Auto(m, r, n)) + } + + /// What `auto`'s rule rests on for a job of `n` qubits, for the receipt. + fn auto_json(rule: &super::super::fabric_measured::Rule, n: u32) -> Value { + use super::super::fabric_measured::{HOSTS, Spread}; + let spread = |s: &Spread| json!({ "min": s.min, "median": s.median, "max": s.max }); + let host = HOSTS.iter().find(|h| h.id == rule.host); + let mut j = json!({ "rule": RULE, "host": rule.host, "fusion": rule.fusion, "from_qubits": rule.from, "to_qubits": rule.to }); + if let Some(h) = host { + j["measured"] = json!(h.date); + j["about"] = json!(h.about); + j["load"] = json!(h.load); + j["discovery_ms"] = json!(h.discovery_ms); + } + if let Some(b) = rule.basis(n) { + j["min_launches"] = json!(b.launches); + j["basis"] = json!({ "qubits": b.qubits, "launches": b.launches, "runs": b.runs, "cpu_ms": spread(&b.cpu_ms), "member_ms": spread(&b.member_ms) }); + } + j + } + + /// The rule and its basis, in a sentence. + fn auto_said(m: &Accelerator, rule: &super::super::fabric_measured::Rule, n: u32) -> String { + let basis = rule.basis(n).map(|b| format!(" (at {} qubits, {} launches: {:.1} ms against the CPU's {:.1} ms, medians of {} runs each)", b.qubits, b.launches, b.member_ms.median, b.cpu_ms.median, b.runs)).unwrap_or_default(); + let date = super::super::fabric_measured::HOSTS.iter().find(|h| h.id == rule.host).map(|h| format!(", measured {}", h.date)).unwrap_or_default(); + format!("auto: its state on {} ({}), measured faster than this host's CPU from {} to {} qubits at fusion {}{basis}{date}; certified bit-exact against the CPU", m.key, m.id, rule.from, rule.to, rule.fusion) + } + fn label(p: &Part<'_>) -> String { match p { Part::Cpu => "cpu".into(), @@ -507,6 +933,7 @@ mod imp { match p { Placed::Cpu if *ask == Ask::Auto => None, Placed::Cpu => Some("on the CPU, as the job asked".into()), + Placed::Auto(m, rule, n) => Some(auto_said(m, rule, *n)), Placed::Members(ms) => ms.first().map(|m| format!("its state on {} ({}), certified bit-exact against the CPU", m.key, m.id)), Placed::Split(parts) => Some(format!("its state split by its top {} qubits into {} partitions, on {}, each certified bit-exact against the CPU", parts.len().trailing_zeros(), parts.len(), parts.iter().map(label).collect::>().join(", "))), } @@ -523,13 +950,42 @@ mod imp { json!({ "member": m.id, "key": m.key, "class": m.class.label(), "implementation": c.implementation, "parity": tier(&c.parity), "certificate": hex(&c.hash()), "corpus": hex(&c.corpus), "generator": c.generator, "cases": c.cases }) } - fn part_json(p: &Part<'_>) -> Value { + /// A member's certificate for the wider kernels, or why it has none. + fn wide_json(m: &Accelerator) -> Value { + match &m.wide { + Ok(c) => { + let mut j = json!({ "kernel": c.kernel, "parity": tier(&c.parity), "certificate": hex(&c.hash()), "corpus": hex(&c.corpus), "generator": c.generator, "cases": c.cases }); + if !m.wide_exact() { + j["why"] = json!(format!("not bit-exact here: placed only for gates on up to {} qubits at once", sv64::NARROW)); + } + j + } + Err(e) => json!({ "kernel": sv64::KERNEL_SV_RUN_WIDE, "parity": { "tier": "uncertified" }, "why": format!("{e}: placed only for gates on up to {} qubits at once", sv64::NARROW) }), + } + } + + /// A member that ran a job, with its wide certificate when the job's + /// launches took the wider kernels. + fn ran_json(m: &Accelerator, wide: bool) -> Value { + let mut j = member_json(m); + if wide { + j["wide"] = wide_json(m); + } + j + } + + fn part_json(p: &Part<'_>, wide: bool) -> Value { match p { Part::Cpu => cpu_json(), - Part::Member(m) => member_json(m), + Part::Member(m) => ran_json(m, wide), } } + /// Whether any launch takes a wider kernel. + fn takes_wide(launches: &[Launch]) -> bool { + launches.iter().any(|l| sv64::launch_bits(l).len() as u32 > sv64::NARROW) + } + fn cpu_run(n: u32, gates: &[crate::gates::Gate], width: usize) -> Result { let mut sv = crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?; sv.run(gates, width).map_err(|e| format!("{e:?}"))?; @@ -556,29 +1012,49 @@ mod imp { let r = (*ask != Ask::Auto).then(|| receipt(cpu_json(), said(ask, p))); Ok((sv, r)) } + Placed::Auto(m, rule, _) => { + let launches = sv64::plan(n, gates, width).map_err(|e| format!("{e:?}"))?; + let wide = takes_wide(&launches); + let said = auto_said(m, rule, n); + match m.device.run_zero(n, &launches) { + Ok((re, im)) => { + let mut body = receipt(ran_json(m, wide), Some(said)); + body["auto"] = auto_json(rule, n); + Ok((state(n, re, im), Some(body))) + } + // The CPU is always there: a member auto chose that fails + // hands the job back to it, and the receipt says so. + Err(e) => { + let sv = cpu_run(n, gates, width)?; + let mut body = receipt(cpu_json(), Some(format!("{said}; {} failed during the run, and the CPU ran it", m.key))); + let mut f = ran_json(m, wide); + f["why"] = json!(e); + body["fell_back_from"] = json!([f]); + body["auto"] = auto_json(rule, n); + Ok((sv, Some(body))) + } + } + } Placed::Members(ms) => { let launches = sv64::plan(n, gates, width).map_err(|e| format!("{e:?}"))?; - let len = 1usize << n; - let mut re = vec![0.0; len]; - re[0] = 1.0; - let im = vec![0.0; len]; + let wide = takes_wide(&launches); // Each in turn until one runs it: a member that fails hands the // job to the next, and the receipt lists each that failed. let mut failed: Vec<(&Accelerator, String)> = Vec::new(); for m in ms { - match m.device.run(&re, &im, &launches) { + match m.device.run_zero(n, &launches) { Ok((re, im)) => { let mut why = said(ask, &Placed::Members(vec![m])); if !failed.is_empty() { why = Some(format!("{}; {} failed during the run first", why.unwrap_or_default(), failed.iter().map(|(f, _)| f.key.as_str()).collect::>().join(", "))); } - let mut body = receipt(member_json(m), why); + let mut body = receipt(ran_json(m, wide), why); if !failed.is_empty() { body["fell_back_from"] = Value::Array( failed .iter() .map(|(f, e)| { - let mut j = member_json(f); + let mut j = ran_json(f, wide); j["why"] = json!(e); j }) @@ -595,6 +1071,7 @@ mod imp { } Placed::Split(parts) => { let launches = sv64::plan(n, gates, width).map_err(|e| format!("{e:?}"))?; + let wide = takes_wide(&launches); // One runner for the CPU and one for each member named, so a // member's partitions run one after another. let mut keys: Vec> = Vec::new(); @@ -634,7 +1111,7 @@ mod imp { let done = sv64::run_split(&mut re, &mut im, &launches, &owner, &runners).map_err(|e| tag(refusal::FABRIC, format!("fabric: {e}")))?; let body = receipt( json!({ - "partitions": parts.iter().map(part_json).collect::>(), + "partitions": parts.iter().map(|p| part_json(p, wide)).collect::>(), "partition_qubits": parts.len().trailing_zeros(), "local_qubits": n - parts.len().trailing_zeros(), "segments": done.segments, @@ -654,12 +1131,21 @@ mod imp { members[0]["detail"] = cpu_detail(job_bytes); for m in ®.members { let mut j = member_json(m); - j["detail"] = json!({ "max_qubits": m.device.max_qubits(), "memory": "two f64 arrays of 2^n within half the device's free memory at discovery, n at most 25", "backend": m.backend, "device": m.detail }); + j["wide"] = wide_json(m); + j["detail"] = json!({ "max_qubits": m.device.max_qubits(), "memory": "two f64 arrays of 2^n within half the device's free memory at discovery (for OpenCL: each within one allocation, both within half its memory), n at most 32", "backend": m.backend, "device": m.detail }); members.push(j); } + let here = super::host(); + let places: Vec = super::super::fabric_measured::rules_for(&here) + .iter() + .filter(|r| reg.members.iter().any(|m| m.id == r.member)) + .map(|r| json!({ "member": r.member, "key": r.key, "fusion": r.fusion, "from_qubits": r.from, "to_qubits": r.to, "min_launches": r.min_launches(r.from) })) + .collect(); json!({ "kernel": KERNEL, "auto": AUTO, + "auto_rule": super::auto_rule(&here, places), + "pauli": spd_list(), "members": members, "refused": reg.refused, "unavailable": reg.unavailable, @@ -867,6 +1353,37 @@ mod tests { assert_eq!(listed["members"][0]["key"], "cpu"); } + /// A `pauli` job runs on the CPU as it always did under auto (no + /// `fabric`), names the CPU when it asks for it with the same answers, + /// and is refused a split, or a member this host does not have. + #[cfg(feature = "quantum_fabric")] + #[test] + fn a_pauli_job_is_placed_as_asked_and_refused_what_the_host_cannot_honour() { + let mut q = "OPENQASM 3.0;\nqubit[40] q;\n".to_string(); + for layer in 0..4 { + for i in 0..40 { + q.push_str(&format!("rx({}) q[{i}];\n", 0.2 + 0.03 * f64::from(i + layer))); + } + for i in (layer % 2..39).step_by(2) { + q.push_str(&format!("rzz({}) q[{i}], q[{}];\n", 0.5 + 0.01 * f64::from(i), i + 1)); + } + } + let job = |f: Value| json!({ "qasm": q, "method": "pauli", "observables": ["Z0", "X3 Z4"], "options": { "threshold": 1e-4 }, "fabric": f }); + let auto = run(&job(json!("auto"))).unwrap(); + assert!(auto.get("fabric").is_none(), "{}", auto["fabric"]); + let cpu = run(&job(json!("cpu"))).unwrap(); + assert_eq!(answers(&cpu), answers(&auto)); + assert_eq!((cpu["fabric"]["kernel"].as_str(), cpu["fabric"]["key"].as_str()), (Some(KERNEL_PAULI), Some("cpu")), "{}", cpu["fabric"]); + assert!(super::super::schema::Registry::engine().check("job-result.v1.json", &cpu).is_empty(), "{cpu}"); + assert!(run(&job(json!(["cpu", "cpu"]))).unwrap_err().ends_with("[J013]")); + if list(CAP)["pauli"]["backends"].as_array().is_none_or(Vec::is_empty) { + let e = run(&job(json!("accelerator"))).unwrap_err(); + assert!(e.ends_with("[J013]") && e.contains("no member certified for the pauli method here"), "{e}"); + assert!(run(&job(json!("gpu-somewhere"))).unwrap_err().ends_with("[J013]")); + } + assert_eq!(list(CAP)["pauli"]["members"][0]["key"], "cpu"); + } + /// A build without the fabric runs every job as it always did, and /// refuses any placement it was asked for. #[cfg(not(feature = "quantum_fabric"))] diff --git a/wai/wai-quantum/src/engine/fabric_measured.rs b/wai/wai-quantum/src/engine/fabric_measured.rs new file mode 100644 index 000000000..6fd0d0488 --- /dev/null +++ b/wai/wai-quantum/src/engine/fabric_measured.rs @@ -0,0 +1,406 @@ +//! The measurements `auto` places a state vector by ([`super::fabric`]). +//! +//! `auto` places a `statevector` job's state on a member of the fabric only +//! where these measurements show that member faster than the CPU beside it, +//! for that job's size. Nothing else moves it: a host, a member or a job the +//! table does not cover stays on the CPU. +//! +//! - **What was timed.** Whole jobs through the engine (plan and run, 1000 +//! shots), the CPU's (`fabric: cpu`) against each member's (`fabric: `), on one family of circuits ([`circuit`]: ten layers of rotations +//! at arbitrary angles on every qubit, entangling pairs in brickwork, and a +//! controlled phase across the register), at each fusion width measured, +//! for each qubit count. The runs of the CPU and of each member alternate, +//! so a change in the host's load falls on both. `cargo run --release +//! --example fabric_timing` with the host's backends measures them again +//! and prints the rows. +//! - **The rule** ([`Rule`]). A row is *faster* when every run on the member +//! took less time than every run on the CPU: the member's slowest beats the +//! CPU's quickest, so the spread of both is inside the verdict. For one +//! host, member and fusion width, the member is placed from the smallest +//! qubit count from which every row up to the largest measured is faster, +//! up to that largest, and only for a job whose plan has at least as many +//! launches as the measured circuit at the nearest measured size below it: +//! a shallower job spends more of its time in the copies, and is not +//! covered. +//! - **Which host.** A row names the host by [`super::fabric::host`] (its +//! processor's model, architecture and thread count, hashed) and the member +//! by its id, which binds the device, its driver and its runtime compiler. +//! A different processor, a different device, or the same device under +//! another driver matches no row. +//! - **Discovery.** A process discovers and certifies its members once, +//! the first time a job needs one; [`Host::discovery_ms`] states what that +//! took. A process that has not discovered them places a job by `auto` +//! only where the measured saving (the CPU's quickest run less the member's +//! slowest) also covers discovery; once it has, the rule alone decides. +//! +//! What the rows say (2026-10-30): on the laptop, the GPU is never faster as +//! a whole job from 22 qubits up, at any fusion width (at 26 qubits and fusion +//! 3, 3.74 s on the CPU against 4.44 s by CUDA and 4.29 s by OpenCL, +//! medians), so `auto` keeps every state on its CPU. On the host with the +//! integrated GPU, every run on the GPU beat every run on the CPU at fusion 3 +//! and 2 from 18 to 28 qubits (at 22 qubits and fusion 3, 205 ms on the CPU +//! against 108 ms by HIP and 102 ms by OpenCL; at 28, 9.81 s against 8.47 s +//! and 8.14 s), and lost from 22 qubits up at fusion 1. Its discovery (1.2 s) +//! is more than the saving below 28 qubits counted that way, so a fresh +//! process places a state on its GPU by `auto` only at 28 qubits (by OpenCL +//! at fusion 3, by either at fusion 2), and one that has discovered its +//! members (a service, a session, or a process whose job asked for one) +//! from 18 qubits. On the aarch64 board (2026-10-31), its GPU by CUDA and by +//! OpenCL alike won some rows below 24 qubits and lost every row from 26 +//! qubits at every fusion width (at 28 qubits and fusion 3, 22.5 s on the CPU +//! against 27.7 s by either), so no size has a faster tail there and `auto` +//! keeps its states on the CPU. +//! +//! The rows' times are what the hosts did on the day, under the load each +//! host's entry states; a test holds the rule to the rows, not the rows to a +//! new measurement. + +/// A measured host. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Host { + /// [`super::fabric::host`] on it. + pub id: &'static str, + /// What it is, by capability. + pub about: &'static str, + /// The day it was measured (UTC). + pub date: &'static str, + /// What else the host was doing, as read before and after. + pub load: &'static str, + /// What discovering and certifying its members took, once per process, + /// in milliseconds. + pub discovery_ms: f64, +} + +/// The least, the median and the most of a row's runs, in milliseconds. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Spread { + pub min: f64, + pub median: f64, + pub max: f64, +} + +/// One measured size: a job of [`circuit`] on `qubits` at `fusion`, on the +/// CPU and on one member of one host. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Row { + /// [`Host::id`]. + pub host: &'static str, + /// The member's id (`wai-quantum fabric` lists it). + pub member: &'static str, + /// The member's key. + pub key: &'static str, + pub fusion: u32, + pub qubits: u32, + /// The launches of the job's plan ([`super::fabric::launches`]). + pub launches: u32, + /// Runs of each, alternating. + pub runs: u32, + pub cpu_ms: Spread, + pub member_ms: Spread, +} + +impl Row { + /// Every run on the member took less time than every run on the CPU. + pub fn faster(&self) -> bool { + self.member_ms.max < self.cpu_ms.min + } +} + +/// Where the rule places a state on one member of one host, at one fusion +/// width: from `from` to `to` qubits, for a plan of at least the launches +/// [`Rule::min_launches`] gives. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Rule { + pub host: &'static str, + pub member: &'static str, + pub key: &'static str, + pub fusion: u32, + pub from: u32, + pub to: u32, +} + +impl Rule { + /// The measured row a job of `n` qubits is judged by: the largest + /// measured size at or below `n`. + pub fn basis(&self, n: u32) -> Option<&'static Row> { + ROWS.iter().filter(|r| r.host == self.host && r.member == self.member && r.fusion == self.fusion && r.qubits <= n).max_by_key(|r| r.qubits) + } + + /// The fewest launches a job of `n` qubits needs for the rule to cover it. + pub fn min_launches(&self, n: u32) -> Option { + self.basis(n).map(|r| r.launches) + } + + /// Whether the rule places a job of `n` qubits whose plan has `launches`. + pub fn covers(&self, n: u32, launches: usize) -> bool { + (self.from..=self.to).contains(&n) && self.min_launches(n).is_some_and(|l| launches >= l as usize) + } +} + +impl Row { + /// What the member saved at the least: the CPU's quickest run less the + /// member's slowest, in milliseconds (negative when it saved nothing). + pub fn saving_ms(&self) -> f64 { + self.cpu_ms.min - self.member_ms.max + } +} + +/// What discovering and certifying a host's members took, once per process. +pub fn discovery_ms(host: &str) -> Option { + HOSTS.iter().find(|h| h.id == host).map(|h| h.discovery_ms) +} + +/// The rules [`ROWS`] give: for each host, member and fusion width with a +/// faster run of rows reaching the largest measured size. +pub fn rules() -> Vec { + let mut out: Vec = Vec::new(); + for r in ROWS { + if out.iter().any(|x| x.host == r.host && x.member == r.member && x.fusion == r.fusion) { + continue; + } + let mut rows: Vec<&Row> = ROWS.iter().filter(|x| x.host == r.host && x.member == r.member && x.fusion == r.fusion).collect(); + rows.sort_by_key(|x| x.qubits); + // The faster rows that reach the largest size without a gap. + let tail = rows.iter().rev().take_while(|x| x.faster()).count(); + if tail == 0 { + continue; + } + let from = rows[rows.len() - tail].qubits; + let to = rows[rows.len() - 1].qubits; + out.push(Rule { host: r.host, member: r.member, key: r.key, fusion: r.fusion, from, to }); + } + out +} + +/// The rules for `host`. +pub fn rules_for(host: &str) -> Vec { + rules().into_iter().filter(|r| r.host == host).collect() +} + +/// The measured family of circuits, as OpenQASM 3: ten layers of `rx` and +/// `rz` on every qubit at arbitrary angles, `cx` and `ry` on alternate pairs +/// in brickwork, and a controlled phase between the first and last qubits; +/// every qubit measured. +pub fn circuit(n: u32) -> String { + let mut q = format!("OPENQASM 3.0;\nqubit[{n}] q;\nbit[{n}] c;\n"); + for layer in 0..10u32 { + for i in 0..n { + q.push_str(&format!("rx({:.6}) q[{i}];\nrz({:.6}) q[{i}];\n", 0.13 + 0.41 * f64::from(i + layer), 0.23 * f64::from(layer + 1) + 0.01 * f64::from(i))); + } + for i in (layer % 2..n.saturating_sub(1)).step_by(2) { + q.push_str(&format!("cx q[{i}], q[{}];\nry({:.6}) q[{}];\n", i + 1, 0.3 + 0.07 * f64::from(i), i + 1)); + } + if n > 1 { + q.push_str(&format!("cp(0.9) q[0], q[{}];\n", n - 1)); + } + } + q.push_str("c = measure q;\n"); + q +} + +/// The job of [`circuit`] the rows time: 1000 shots, seed 1, the state +/// vector, at `fusion`, placed by `fabric`. +pub fn job(n: u32, fusion: u32, fabric: &str) -> serde_json::Value { + serde_json::json!({ "qasm": circuit(n), "shots": 1000, "seed": 1, "method": "statevector", "options": { "fusion": fusion }, "fabric": fabric }) +} + +/// The measured hosts. +pub const HOSTS: &[Host] = &[ + Host { + id: "cpu-x86_64-1f0df95e9732", + about: "a laptop: a 20-thread x86-64 processor with 15 GiB of memory, beside a laptop GPU of compute capability 8.9 with 6 GiB, by CUDA and by OpenCL", + date: "2026-10-30", + load: "load average 1.5 at the start, rising to 7.7 by the last rows (the host is also a CI runner); the GPU idle at the start", + discovery_ms: 3220.0, + }, + Host { + id: "cpu-x86_64-c812bc66ca6b", + about: "a 32-thread x86-64 processor with 128 GiB of memory it shares with its integrated GPU (gfx1151), by HIP and by OpenCL", + date: "2026-10-30", + load: "load average 5.4 at the start, rising to 14 by the last rows (another project's builds and its server on the same host); the GPU idle at the start, 83% in use by the other project when the fusion 1 rows ended", + discovery_ms: 1174.0, + }, + Host { + id: "cpu-aarch64-7d70f63b4684", + about: "a 14-core aarch64 processor with 122 GiB of memory it shares with its integrated GPU of compute capability 11.0, by CUDA and by OpenCL (PoCL)", + date: "2026-10-31", + load: "load average 3.0 at the start (a desktop session on the same host), up to 8.6 through the rows, the runs' own threads included; the GPU otherwise idle", + discovery_ms: 2551.0, + },]; + +/// The measurements: 5 runs of each, alternating, at fusion widths 3 (the +/// default), 2 and 1, from 18 qubits to as many as half the host's free +/// memory held (26 on the laptop, 28 on the others; fusion 1 to 24 on the +/// integrated-GPU x86-64 host). +#[rustfmt::skip] +pub const ROWS: &[Row] = &[ + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 3, qubits: 18, launches: 86, runs: 5, cpu_ms: Spread { min: 17.1, median: 17.6, max: 19.3 }, member_ms: Spread { min: 17.4, median: 20.4, max: 20.9 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 3, qubits: 18, launches: 86, runs: 5, cpu_ms: Spread { min: 17.1, median: 17.6, max: 19.3 }, member_ms: Spread { min: 16.8, median: 20.5, max: 20.5 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 3, qubits: 20, launches: 96, runs: 5, cpu_ms: Spread { min: 47.8, median: 52.0, max: 54.3 }, member_ms: Spread { min: 54.2, median: 55.5, max: 63.1 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 3, qubits: 20, launches: 96, runs: 5, cpu_ms: Spread { min: 47.8, median: 52.0, max: 54.3 }, member_ms: Spread { min: 53.3, median: 53.9, max: 59.1 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 3, qubits: 22, launches: 106, runs: 5, cpu_ms: Spread { min: 205.1, median: 206.9, max: 213.3 }, member_ms: Spread { min: 225.2, median: 227.7, max: 228.5 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 3, qubits: 22, launches: 106, runs: 5, cpu_ms: Spread { min: 205.1, median: 206.9, max: 213.3 }, member_ms: Spread { min: 222.0, median: 222.6, max: 224.4 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 3, qubits: 24, launches: 116, runs: 5, cpu_ms: Spread { min: 838.6, median: 847.9, max: 864.3 }, member_ms: Spread { min: 977.7, median: 978.5, max: 980.7 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 3, qubits: 24, launches: 116, runs: 5, cpu_ms: Spread { min: 838.6, median: 847.9, max: 864.3 }, member_ms: Spread { min: 957.7, median: 958.6, max: 961.2 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 3, qubits: 26, launches: 126, runs: 5, cpu_ms: Spread { min: 3608.0, median: 3738.8, max: 3810.5 }, member_ms: Spread { min: 4426.8, median: 4443.4, max: 4485.1 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 3, qubits: 26, launches: 126, runs: 5, cpu_ms: Spread { min: 3608.0, median: 3738.8, max: 3810.5 }, member_ms: Spread { min: 4242.7, median: 4288.0, max: 4298.5 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 2, qubits: 18, launches: 95, runs: 5, cpu_ms: Spread { min: 13.7, median: 14.9, max: 18.7 }, member_ms: Spread { min: 13.6, median: 13.9, max: 14.8 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 2, qubits: 18, launches: 95, runs: 5, cpu_ms: Spread { min: 13.7, median: 14.9, max: 18.7 }, member_ms: Spread { min: 14.1, median: 14.1, max: 14.4 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 2, qubits: 20, launches: 105, runs: 5, cpu_ms: Spread { min: 56.3, median: 60.3, max: 60.6 }, member_ms: Spread { min: 54.7, median: 55.4, max: 56.0 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 2, qubits: 20, launches: 105, runs: 5, cpu_ms: Spread { min: 56.3, median: 60.3, max: 60.6 }, member_ms: Spread { min: 53.5, median: 53.7, max: 55.7 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 2, qubits: 22, launches: 115, runs: 5, cpu_ms: Spread { min: 231.8, median: 234.4, max: 239.9 }, member_ms: Spread { min: 234.8, median: 236.3, max: 252.3 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 2, qubits: 22, launches: 115, runs: 5, cpu_ms: Spread { min: 231.8, median: 234.4, max: 239.9 }, member_ms: Spread { min: 230.4, median: 231.9, max: 233.3 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 2, qubits: 24, launches: 125, runs: 5, cpu_ms: Spread { min: 947.4, median: 959.4, max: 964.4 }, member_ms: Spread { min: 1006.1, median: 1008.8, max: 1014.0 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 2, qubits: 24, launches: 125, runs: 5, cpu_ms: Spread { min: 947.4, median: 959.4, max: 964.4 }, member_ms: Spread { min: 985.5, median: 987.6, max: 989.1 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 2, qubits: 26, launches: 135, runs: 5, cpu_ms: Spread { min: 4002.2, median: 4015.6, max: 4052.1 }, member_ms: Spread { min: 4510.8, median: 4513.9, max: 4530.0 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 2, qubits: 26, launches: 135, runs: 5, cpu_ms: Spread { min: 4002.2, median: 4015.6, max: 4052.1 }, member_ms: Spread { min: 4318.4, median: 4366.1, max: 4383.9 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 1, qubits: 18, launches: 540, runs: 5, cpu_ms: Spread { min: 38.8, median: 42.1, max: 42.6 }, member_ms: Spread { min: 30.7, median: 31.1, max: 31.5 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 1, qubits: 18, launches: 540, runs: 5, cpu_ms: Spread { min: 38.8, median: 42.1, max: 42.6 }, member_ms: Spread { min: 31.4, median: 31.5, max: 31.8 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 1, qubits: 20, launches: 600, runs: 5, cpu_ms: Spread { min: 147.3, median: 148.1, max: 148.7 }, member_ms: Spread { min: 134.1, median: 134.7, max: 136.0 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 1, qubits: 20, launches: 600, runs: 5, cpu_ms: Spread { min: 147.3, median: 148.1, max: 148.7 }, member_ms: Spread { min: 134.9, median: 136.5, max: 137.4 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 1, qubits: 22, launches: 660, runs: 5, cpu_ms: Spread { min: 663.1, median: 667.6, max: 671.8 }, member_ms: Spread { min: 734.1, median: 737.7, max: 764.1 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 1, qubits: 22, launches: 660, runs: 5, cpu_ms: Spread { min: 663.1, median: 667.6, max: 671.8 }, member_ms: Spread { min: 732.3, median: 738.5, max: 742.9 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 1, qubits: 24, launches: 720, runs: 5, cpu_ms: Spread { min: 2849.4, median: 2858.3, max: 2882.8 }, member_ms: Spread { min: 3269.9, median: 3280.1, max: 3282.4 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 1, qubits: 24, launches: 720, runs: 5, cpu_ms: Spread { min: 2849.4, median: 2858.3, max: 2882.8 }, member_ms: Spread { min: 3208.2, median: 3214.3, max: 3215.2 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/gpu-cuda-847ebbb6bc26", key: "gpu-cuda-sm89", fusion: 1, qubits: 26, launches: 780, runs: 5, cpu_ms: Spread { min: 12254.9, median: 12312.9, max: 12350.7 }, member_ms: Spread { min: 14176.7, median: 14181.4, max: 14197.5 } }, + Row { host: "cpu-x86_64-1f0df95e9732", member: "local/opencl-gpu-5aaf889f602b", key: "opencl-gpu-10de", fusion: 1, qubits: 26, launches: 780, runs: 5, cpu_ms: Spread { min: 12254.9, median: 12312.9, max: 12350.7 }, member_ms: Spread { min: 14096.4, median: 14105.9, max: 14115.1 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 3, qubits: 18, launches: 86, runs: 5, cpu_ms: Spread { min: 16.0, median: 16.5, max: 20.6 }, member_ms: Spread { min: 8.2, median: 8.3, max: 9.2 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 3, qubits: 18, launches: 86, runs: 5, cpu_ms: Spread { min: 16.0, median: 16.5, max: 20.6 }, member_ms: Spread { min: 8.0, median: 8.1, max: 8.3 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 3, qubits: 20, launches: 96, runs: 5, cpu_ms: Spread { min: 59.5, median: 60.8, max: 64.8 }, member_ms: Spread { min: 28.4, median: 28.6, max: 28.7 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 3, qubits: 20, launches: 96, runs: 5, cpu_ms: Spread { min: 59.5, median: 60.8, max: 64.8 }, member_ms: Spread { min: 26.6, median: 27.2, max: 28.9 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 3, qubits: 22, launches: 106, runs: 5, cpu_ms: Spread { min: 202.5, median: 204.7, max: 210.7 }, member_ms: Spread { min: 107.5, median: 108.3, max: 110.8 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 3, qubits: 22, launches: 106, runs: 5, cpu_ms: Spread { min: 202.5, median: 204.7, max: 210.7 }, member_ms: Spread { min: 100.7, median: 102.0, max: 103.9 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 3, qubits: 24, launches: 116, runs: 5, cpu_ms: Spread { min: 618.6, median: 629.0, max: 658.4 }, member_ms: Spread { min: 454.5, median: 460.7, max: 463.3 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 3, qubits: 24, launches: 116, runs: 5, cpu_ms: Spread { min: 618.6, median: 629.0, max: 658.4 }, member_ms: Spread { min: 442.2, median: 445.1, max: 474.5 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 3, qubits: 26, launches: 126, runs: 5, cpu_ms: Spread { min: 2330.1, median: 2358.2, max: 2776.2 }, member_ms: Spread { min: 1980.8, median: 1999.4, max: 2037.8 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 3, qubits: 26, launches: 126, runs: 5, cpu_ms: Spread { min: 2330.1, median: 2358.2, max: 2776.2 }, member_ms: Spread { min: 1926.7, median: 1947.8, max: 2029.0 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 3, qubits: 28, launches: 136, runs: 5, cpu_ms: Spread { min: 9738.3, median: 9814.9, max: 10010.7 }, member_ms: Spread { min: 8315.3, median: 8468.7, max: 9057.3 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 3, qubits: 28, launches: 136, runs: 5, cpu_ms: Spread { min: 9738.3, median: 9814.9, max: 10010.7 }, member_ms: Spread { min: 8063.3, median: 8143.3, max: 8253.0 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 2, qubits: 18, launches: 95, runs: 5, cpu_ms: Spread { min: 17.7, median: 18.4, max: 26.6 }, member_ms: Spread { min: 8.6, median: 9.2, max: 11.6 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 2, qubits: 18, launches: 95, runs: 5, cpu_ms: Spread { min: 17.7, median: 18.4, max: 26.6 }, member_ms: Spread { min: 8.1, median: 8.4, max: 9.0 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 2, qubits: 20, launches: 105, runs: 5, cpu_ms: Spread { min: 61.3, median: 63.3, max: 64.3 }, member_ms: Spread { min: 28.2, median: 28.9, max: 29.3 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 2, qubits: 20, launches: 105, runs: 5, cpu_ms: Spread { min: 61.3, median: 63.3, max: 64.3 }, member_ms: Spread { min: 26.8, median: 27.1, max: 27.7 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 2, qubits: 22, launches: 115, runs: 5, cpu_ms: Spread { min: 210.6, median: 214.2, max: 218.2 }, member_ms: Spread { min: 111.7, median: 114.3, max: 117.8 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 2, qubits: 22, launches: 115, runs: 5, cpu_ms: Spread { min: 210.6, median: 214.2, max: 218.2 }, member_ms: Spread { min: 104.7, median: 106.3, max: 109.4 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 2, qubits: 24, launches: 125, runs: 5, cpu_ms: Spread { min: 628.9, median: 648.0, max: 669.8 }, member_ms: Spread { min: 462.9, median: 469.9, max: 478.2 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 2, qubits: 24, launches: 125, runs: 5, cpu_ms: Spread { min: 628.9, median: 648.0, max: 669.8 }, member_ms: Spread { min: 452.6, median: 457.6, max: 462.5 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 2, qubits: 26, launches: 135, runs: 5, cpu_ms: Spread { min: 2350.7, median: 2405.8, max: 2501.4 }, member_ms: Spread { min: 1992.3, median: 2026.0, max: 2128.3 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 2, qubits: 26, launches: 135, runs: 5, cpu_ms: Spread { min: 2350.7, median: 2405.8, max: 2501.4 }, member_ms: Spread { min: 1955.7, median: 1982.8, max: 2133.2 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 2, qubits: 28, launches: 145, runs: 5, cpu_ms: Spread { min: 9886.3, median: 10132.0, max: 10550.1 }, member_ms: Spread { min: 8408.2, median: 8461.9, max: 8618.6 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 2, qubits: 28, launches: 145, runs: 5, cpu_ms: Spread { min: 9886.3, median: 10132.0, max: 10550.1 }, member_ms: Spread { min: 8259.2, median: 8319.6, max: 8447.8 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 1, qubits: 18, launches: 540, runs: 5, cpu_ms: Spread { min: 37.9, median: 40.0, max: 40.9 }, member_ms: Spread { min: 16.3, median: 16.5, max: 17.5 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 1, qubits: 18, launches: 540, runs: 5, cpu_ms: Spread { min: 37.9, median: 40.0, max: 40.9 }, member_ms: Spread { min: 16.4, median: 16.8, max: 17.1 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 1, qubits: 20, launches: 600, runs: 5, cpu_ms: Spread { min: 106.2, median: 107.6, max: 111.7 }, member_ms: Spread { min: 57.4, median: 58.9, max: 61.2 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 1, qubits: 20, launches: 600, runs: 5, cpu_ms: Spread { min: 106.2, median: 107.6, max: 111.7 }, member_ms: Spread { min: 56.1, median: 57.1, max: 62.1 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 1, qubits: 22, launches: 660, runs: 5, cpu_ms: Spread { min: 394.7, median: 400.8, max: 421.3 }, member_ms: Spread { min: 454.7, median: 458.8, max: 465.7 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 1, qubits: 22, launches: 660, runs: 5, cpu_ms: Spread { min: 394.7, median: 400.8, max: 421.3 }, member_ms: Spread { min: 450.7, median: 458.2, max: 467.6 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/gpu-hip-61a15b9fb9ac", key: "gpu-hip-gfx1151", fusion: 1, qubits: 24, launches: 720, runs: 5, cpu_ms: Spread { min: 1407.9, median: 1506.3, max: 1642.9 }, member_ms: Spread { min: 2115.4, median: 2301.3, max: 2557.8 } }, + Row { host: "cpu-x86_64-c812bc66ca6b", member: "local/opencl-gpu-bd32c87ff874", key: "opencl-gpu-1002", fusion: 1, qubits: 24, launches: 720, runs: 5, cpu_ms: Spread { min: 1407.9, median: 1506.3, max: 1642.9 }, member_ms: Spread { min: 2122.7, median: 2388.3, max: 2977.1 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 3, qubits: 18, launches: 86, runs: 5, cpu_ms: Spread { min: 35.3, median: 38.1, max: 40.8 }, member_ms: Spread { min: 29.3, median: 29.8, max: 32.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 3, qubits: 18, launches: 86, runs: 5, cpu_ms: Spread { min: 35.3, median: 38.1, max: 40.8 }, member_ms: Spread { min: 30.9, median: 31.4, max: 33.9 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 3, qubits: 20, launches: 96, runs: 5, cpu_ms: Spread { min: 137.2, median: 138.8, max: 148.7 }, member_ms: Spread { min: 152.2, median: 158.3, max: 166.8 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 3, qubits: 20, launches: 96, runs: 5, cpu_ms: Spread { min: 137.2, median: 138.8, max: 148.7 }, member_ms: Spread { min: 98.0, median: 103.2, max: 105.7 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 3, qubits: 22, launches: 106, runs: 5, cpu_ms: Spread { min: 571.3, median: 575.1, max: 586.7 }, member_ms: Spread { min: 363.3, median: 365.5, max: 368.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 3, qubits: 22, launches: 106, runs: 5, cpu_ms: Spread { min: 571.3, median: 575.1, max: 586.7 }, member_ms: Spread { min: 357.7, median: 363.3, max: 367.7 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 3, qubits: 24, launches: 116, runs: 5, cpu_ms: Spread { min: 1533.7, median: 1545.6, max: 1565.9 }, member_ms: Spread { min: 1527.8, median: 1529.9, max: 1536.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 3, qubits: 24, launches: 116, runs: 5, cpu_ms: Spread { min: 1533.7, median: 1545.6, max: 1565.9 }, member_ms: Spread { min: 1530.9, median: 1544.7, max: 1545.7 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 3, qubits: 26, launches: 126, runs: 5, cpu_ms: Spread { min: 5258.9, median: 5288.2, max: 5328.5 }, member_ms: Spread { min: 6495.9, median: 6504.2, max: 6507.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 3, qubits: 26, launches: 126, runs: 5, cpu_ms: Spread { min: 5258.9, median: 5288.2, max: 5328.5 }, member_ms: Spread { min: 6477.1, median: 6486.6, max: 6490.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 3, qubits: 28, launches: 136, runs: 5, cpu_ms: Spread { min: 22284.0, median: 22505.8, max: 22598.3 }, member_ms: Spread { min: 27732.5, median: 27738.2, max: 27793.9 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 3, qubits: 28, launches: 136, runs: 5, cpu_ms: Spread { min: 22284.0, median: 22505.8, max: 22598.3 }, member_ms: Spread { min: 27636.8, median: 27662.2, max: 27670.7 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 2, qubits: 18, launches: 95, runs: 5, cpu_ms: Spread { min: 32.0, median: 36.8, max: 41.8 }, member_ms: Spread { min: 27.4, median: 29.9, max: 31.3 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 2, qubits: 18, launches: 95, runs: 5, cpu_ms: Spread { min: 32.0, median: 36.8, max: 41.8 }, member_ms: Spread { min: 29.8, median: 33.8, max: 34.1 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 2, qubits: 20, launches: 105, runs: 5, cpu_ms: Spread { min: 136.1, median: 138.2, max: 144.0 }, member_ms: Spread { min: 160.8, median: 167.3, max: 171.3 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 2, qubits: 20, launches: 105, runs: 5, cpu_ms: Spread { min: 136.1, median: 138.2, max: 144.0 }, member_ms: Spread { min: 106.5, median: 106.9, max: 108.2 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 2, qubits: 22, launches: 115, runs: 5, cpu_ms: Spread { min: 547.0, median: 570.6, max: 579.7 }, member_ms: Spread { min: 363.4, median: 365.1, max: 377.5 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 2, qubits: 22, launches: 115, runs: 5, cpu_ms: Spread { min: 547.0, median: 570.6, max: 579.7 }, member_ms: Spread { min: 363.4, median: 363.7, max: 368.1 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 2, qubits: 24, launches: 125, runs: 5, cpu_ms: Spread { min: 1486.5, median: 1498.3, max: 1505.9 }, member_ms: Spread { min: 1524.6, median: 1528.2, max: 1532.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 2, qubits: 24, launches: 125, runs: 5, cpu_ms: Spread { min: 1486.5, median: 1498.3, max: 1505.9 }, member_ms: Spread { min: 1542.1, median: 1546.6, max: 1550.6 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 2, qubits: 26, launches: 135, runs: 5, cpu_ms: Spread { min: 5062.4, median: 5086.4, max: 5109.2 }, member_ms: Spread { min: 6494.2, median: 6505.9, max: 6519.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 2, qubits: 26, launches: 135, runs: 5, cpu_ms: Spread { min: 5062.4, median: 5086.4, max: 5109.2 }, member_ms: Spread { min: 6475.2, median: 6484.4, max: 6486.9 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 2, qubits: 28, launches: 145, runs: 5, cpu_ms: Spread { min: 21509.5, median: 21680.3, max: 21736.2 }, member_ms: Spread { min: 27729.1, median: 27738.5, max: 27800.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 2, qubits: 28, launches: 145, runs: 5, cpu_ms: Spread { min: 21509.5, median: 21680.3, max: 21736.2 }, member_ms: Spread { min: 27648.3, median: 27651.3, max: 27658.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 1, qubits: 18, launches: 540, runs: 5, cpu_ms: Spread { min: 82.2, median: 87.0, max: 89.1 }, member_ms: Spread { min: 58.4, median: 58.7, max: 65.9 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 1, qubits: 18, launches: 540, runs: 5, cpu_ms: Spread { min: 82.2, median: 87.0, max: 89.1 }, member_ms: Spread { min: 60.7, median: 61.9, max: 63.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 1, qubits: 20, launches: 600, runs: 5, cpu_ms: Spread { min: 331.8, median: 336.5, max: 342.2 }, member_ms: Spread { min: 288.1, median: 291.6, max: 295.2 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 1, qubits: 20, launches: 600, runs: 5, cpu_ms: Spread { min: 331.8, median: 336.5, max: 342.2 }, member_ms: Spread { min: 229.5, median: 230.1, max: 234.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 1, qubits: 22, launches: 660, runs: 5, cpu_ms: Spread { min: 1253.8, median: 1278.4, max: 1282.7 }, member_ms: Spread { min: 924.2, median: 924.8, max: 936.6 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 1, qubits: 22, launches: 660, runs: 5, cpu_ms: Spread { min: 1253.8, median: 1278.4, max: 1282.7 }, member_ms: Spread { min: 915.7, median: 920.2, max: 923.1 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 1, qubits: 24, launches: 720, runs: 5, cpu_ms: Spread { min: 3717.2, median: 3787.7, max: 3853.3 }, member_ms: Spread { min: 3945.4, median: 3956.7, max: 3961.4 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 1, qubits: 24, launches: 720, runs: 5, cpu_ms: Spread { min: 3717.2, median: 3787.7, max: 3853.3 }, member_ms: Spread { min: 3957.0, median: 3957.6, max: 3960.1 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 1, qubits: 26, launches: 780, runs: 5, cpu_ms: Spread { min: 14503.8, median: 14526.0, max: 14557.9 }, member_ms: Spread { min: 16968.6, median: 16971.5, max: 16978.0 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 1, qubits: 26, launches: 780, runs: 5, cpu_ms: Spread { min: 14503.8, median: 14526.0, max: 14557.9 }, member_ms: Spread { min: 16947.2, median: 16957.5, max: 16963.8 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/gpu-cuda-d3c99f0065ab", key: "gpu-cuda-sm110", fusion: 1, qubits: 28, launches: 840, runs: 5, cpu_ms: Spread { min: 62334.3, median: 62528.7, max: 62591.8 }, member_ms: Spread { min: 72951.8, median: 72956.5, max: 72979.2 } }, + Row { host: "cpu-aarch64-7d70f63b4684", member: "local/opencl-gpu-279107deba65", key: "opencl-gpu-10de", fusion: 1, qubits: 28, launches: 840, runs: 5, cpu_ms: Spread { min: 62334.3, median: 62528.7, max: 62591.8 }, member_ms: Spread { min: 72884.9, median: 72896.4, max: 72920.2 } }, +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_row_names_a_measured_host_and_its_spread_is_ordered() { + for r in ROWS { + assert!(HOSTS.iter().any(|h| h.id == r.host), "{r:?}"); + for s in [r.cpu_ms, r.member_ms] { + assert!(s.min <= s.median && s.median <= s.max && s.min > 0.0, "{r:?}"); + } + assert!(r.runs >= 3 && r.launches > 0 && (1..=5).contains(&r.fusion), "{r:?}"); + } + for h in HOSTS { + assert!(h.id.starts_with("cpu-") && h.date.len() == 10 && h.discovery_ms > 0.0, "{h:?}"); + } + } + + /// The rule is the rows': a rule's every row from `from` up is faster, + /// the row below it (if any) is not, and no faster tail is left without a + /// rule. + #[test] + fn the_rules_are_the_rows_faster_tails() { + let rules = rules(); + for rule in &rules { + let rows: Vec<&Row> = ROWS.iter().filter(|r| r.host == rule.host && r.member == rule.member && r.fusion == rule.fusion).collect(); + assert!(rows.iter().filter(|r| r.qubits >= rule.from).all(|r| r.faster()), "{rule:?}"); + if let Some(below) = rows.iter().filter(|r| r.qubits < rule.from).max_by_key(|r| r.qubits) { + assert!(!below.faster(), "{rule:?}"); + } + assert_eq!(rule.to, rows.iter().map(|r| r.qubits).max().unwrap()); + assert!(rule.covers(rule.from, rule.min_launches(rule.from).unwrap() as usize)); + assert!(!rule.covers(rule.from, rule.min_launches(rule.from).unwrap() as usize - 1)); + assert!(!rule.covers(rule.to + 1, usize::MAX) && !rule.covers(rule.from - 1, usize::MAX)); + } + for r in ROWS { + let top = ROWS.iter().filter(|x| x.host == r.host && x.member == r.member && x.fusion == r.fusion).max_by_key(|x| x.qubits).unwrap(); + assert_eq!(top.faster(), rules.iter().any(|x| x.host == r.host && x.member == r.member && x.fusion == r.fusion), "{r:?}"); + } + } + + /// The rule on rows made up for the test: a gap below the top leaves only + /// the tail above it, a slower top leaves none, and launches bound it. + #[test] + fn a_faster_tail_is_found_on_rows_of_any_shape() { + let s = |median: f64| Spread { min: median * 0.9, median, max: median * 1.1 }; + let row = |qubits, cpu, member, launches| Row { host: "cpu-test", member: "m", key: "k", fusion: 3, qubits, launches, runs: 5, cpu_ms: s(cpu), member_ms: s(member) }; + let tail = |rows: &[Row]| { + let mut v: Vec<&Row> = rows.iter().collect(); + v.sort_by_key(|x| x.qubits); + let t = v.iter().rev().take_while(|x| x.faster()).count(); + (t > 0).then(|| (v[v.len() - t].qubits, v[v.len() - 1].qubits)) + }; + assert_eq!(tail(&[row(18, 10.0, 20.0, 50), row(20, 40.0, 30.0, 55), row(22, 160.0, 100.0, 60)]), Some((20, 22))); + assert_eq!(tail(&[row(18, 10.0, 5.0, 50), row(20, 40.0, 39.0, 55), row(22, 160.0, 100.0, 60)]), Some((22, 22)), "a spread that overlaps is not faster"); + assert_eq!(tail(&[row(18, 10.0, 5.0, 50), row(20, 40.0, 30.0, 55), row(22, 160.0, 170.0, 60)]), None); + } + + #[test] + fn the_measured_circuit_reads_and_its_job_plans() { + for n in [2u32, 9, 18] { + let v = job(n, 3, "cpu"); + let r = super::super::catalog::read(&v).unwrap(); + assert_eq!(r.program.n_qubits, n); + assert!(super::super::catalog::plan(&v, 8 << 30).is_ok()); + } + assert_eq!(circuit(5), circuit(5)); + } +} diff --git a/wai/wai-quantum/src/engine/guide.rs b/wai/wai-quantum/src/engine/guide.rs index eddda93aa..4a35b2719 100644 --- a/wai/wai-quantum/src/engine/guide.rs +++ b/wai/wai-quantum/src/engine/guide.rs @@ -97,7 +97,7 @@ pub const VERDICTS: &str = "wai-quantum://docs/verdicts"; pub const AUTO: &str = "auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, stabilizer_rank, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, stabilizer_rank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen; stabilizer_rank is chosen by its T count, its exact sum taken when its branches are within one job's work."; /// Where the statevector method's state runs, in a paragraph. -pub const FABRIC: &str = "The statevector method's state runs where the job's `fabric` says: auto (the default) places it on the CPU, on every host, until a member of the fabric is measured faster than the CPU; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere."; +pub const FABRIC: &str = "The statevector method's state runs where the job's `fabric` says: auto (the default) places it on a member only where the committed measurements show that member faster than this host's CPU for the job's size and fusion width, and otherwise on the CPU (the result's `fabric` then names nothing), a placement by measurement saying in its receipt which rule and which measured row it rests on; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere. A pauli job (sparse Pauli dynamics) takes accelerator or a member's id or key too: its sum of terms then runs on that member, certified bit-exact against the CPU for the propagation (result and final table), with the CPU's answers byte for byte; auto keeps it on the CPU, no member being measured faster for it."; /// The language in a paragraph, for `AGENTS.md`. pub const SUMMARY: &str = "Programs are OpenQASM 3.1, or the OpenQASM 2 forms in common use, read by one reader on every surface: registers, the standard gates, `gate` and `def` definitions, the `ctrl @`, `negctrl @`, `inv @` and `pow @` modifiers, classical types and expressions, measurement and `reset` anywhere, `if`, `switch` and bounded `while` on measured bits, timing carried, and calibrations kept for export. Generators (integer `input`s, `const`s and `for` loops) describe a family of circuits. Every refusal and warning names its line and ends with a stable code (`[Q103]`), which the diagnostics resource explains."; diff --git a/wai/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs index 09f94f8ed..68298fcc1 100644 --- a/wai/wai-quantum/src/engine/mod.rs +++ b/wai/wai-quantum/src/engine/mod.rs @@ -203,6 +203,7 @@ pub mod equiv; pub mod estimate; pub mod formats; pub mod fabric; +pub mod fabric_measured; pub mod guide; pub mod latex; #[cfg(feature = "quantum_noise")] diff --git a/wai/wai-quantum/src/engine/run.rs b/wai/wai-quantum/src/engine/run.rs index c352b0cef..1acbe02af 100644 --- a/wai/wai-quantum/src/engine/run.rs +++ b/wai/wai-quantum/src/engine/run.rs @@ -428,7 +428,7 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result { - use crate::quantum_spd::{Pauli, SpdConfig, expectation_capped}; + use crate::quantum_spd::{Pauli, SpdConfig}; let rc = r.program.rotations(); // As many terms as the job's memory holds: a term's string (two // words of bits a 64 qubits, padded to a power of two), its @@ -445,32 +445,41 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result 0.0; let rungs: Vec = if ladder { [1000.0, 100.0, 10.0].iter().map(|f| threshold * f).filter(|&t| t < 1.0).chain([threshold]).collect() } else { vec![threshold] }; out["accuracy"] = json!({ "class": "bounded", "threshold": threshold, "max_weight": max_weight }); - let mut entries = Vec::new(); - for (text, coeff, string) in &r.observables { - let obs: Vec<(u32, Pauli)> = string.iter().map(|&(q, c)| (q, match c { 'X' => Pauli::X, 'Y' => Pauli::Y, _ => Pauli::Z })).collect(); - let mut steps = Vec::new(); - let mut last = None; - for &t in &rungs { - let (res, used) = expectation_capped(&rc, &[(*coeff, obs.clone())], &[], &SpdConfig { threshold: t, max_weight }, threads, max_terms).map_err(|e| e.to_string())?; - let mut step = json!({ "threshold": t, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms }); - if used > t { - step["threshold_reached"] = json!(used); + // On the CPU, or on the fabric's member the job's placement + // names: the CPU's bits either way. + let mut each = |propagate: &mut crate::engine::fabric::Propagator<'_>| -> Result, String> { + let mut entries = Vec::new(); + for (text, coeff, string) in &r.observables { + let obs: Vec<(u32, Pauli)> = string.iter().map(|&(q, c)| (q, match c { 'X' => Pauli::X, 'Y' => Pauli::Y, _ => Pauli::Z })).collect(); + let mut steps = Vec::new(); + let mut last = None; + for &t in &rungs { + let (res, used) = propagate(&rc, &[(*coeff, obs.clone())], &SpdConfig { threshold: t, max_weight }, max_terms)?; + let mut step = json!({ "threshold": t, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms }); + if used > t { + step["threshold_reached"] = json!(used); + } + steps.push(step); + last = Some((res, used)); } - steps.push(step); - last = Some((res, used)); + let (res, used) = last.expect("one rung at least"); + let mut entry = json!({ "observable": text, "coeff": coeff, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms }); + if used > threshold { + // The sum outgrew the job's memory: the threshold rose to + // fit, and the bound covers what that dropped. + entry["threshold_reached"] = json!(used); + entry["max_terms"] = json!(max_terms); + } + if steps.len() > 1 { + entry["ladder"] = Value::Array(steps); + } + entries.push(entry); } - let (res, used) = last.expect("one rung at least"); - let mut entry = json!({ "observable": text, "coeff": coeff, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms }); - if used > threshold { - // The sum outgrew the job's memory: the threshold rose to - // fit, and the bound covers what that dropped. - entry["threshold_reached"] = json!(used); - entry["max_terms"] = json!(max_terms); - } - if steps.len() > 1 { - entry["ladder"] = Value::Array(steps); - } - entries.push(entry); + Ok(entries) + }; + let (entries, placed) = crate::engine::fabric::pauli(request, threads, &mut each)?; + if let Some(f) = placed { + out["fabric"] = f; } out["expectations"] = Value::Array(entries); } diff --git a/wai/wai-quantum/src/quantum_fabric.rs b/wai/wai-quantum/src/quantum_fabric.rs index eea7dc12f..40129abdf 100644 --- a/wai/wai-quantum/src/quantum_fabric.rs +++ b/wai/wai-quantum/src/quantum_fabric.rs @@ -2066,6 +2066,8 @@ pub mod stablehlo; pub mod tensor_engine; #[cfg(feature = "quantum_sv")] pub mod sv64; +#[cfg(feature = "quantum_spd")] +pub mod spd; pub mod vqpu; pub mod wgsl; diff --git a/wai/wai-quantum/src/quantum_fabric/spd.cu b/wai/wai-quantum/src/quantum_fabric/spd.cu new file mode 100644 index 000000000..b42c890b3 --- /dev/null +++ b/wai/wai-quantum/src/quantum_fabric/spd.cu @@ -0,0 +1,358 @@ +// wai-quantum fabric: sparse Pauli dynamics' steps on a GPU, built without +// fused multiply-add, so each coefficient's arithmetic is one IEEE binary64 +// operation at a time, in the order the CPU performs it +// (`quantum_spd::device`). +// +// A term is its string, `words` words of x bits then `words` of z bits, and +// its coefficient. The table keeps the CPU's insertion order. Nothing here +// decides an order by a race: +// +// - a rotation's scan marks each live term that anticommutes with the axis, +// scales it by cos and writes its product with the axis (times +-sin); +// - an index of every slot (dead ones too) is built by compare-and-swap; the +// slot a key lands in depends on the threads, but a key is only ever found, +// never ordered, by it; +// - a new string found in the table is added to that one coefficient (no two +// new strings are the same, so no two threads add to one coefficient); the +// rest are appended at the positions an exclusive prefix sum of their marks +// gives, so in the order of the terms they came from; +// - truncation, compaction and the value's contributions are marks and +// prefix sums too: every list a step hands back is in table order. +// +// Counters are added with integer atomics, whose sum does not depend on the +// order. The host sums the doubles. + +typedef unsigned long long u64; + +#define SPD_EMPTY 0xffffffffu +#define SPD_BLOCK 256 + +__device__ inline u64 spd_mix(u64 z) { + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebull; + return z ^ (z >> 31); +} + +__device__ inline u64 spd_hash(const u64* k, unsigned int words) { + u64 h = 0x9e3779b97f4a7c15ull; + for (unsigned int w = 0; w < 2 * words; w++) { + h = spd_mix(h ^ k[w]); + } + return h; +} + +__device__ inline bool spd_equal(const u64* a, const u64* b, unsigned int words) { + for (unsigned int w = 0; w < 2 * words; w++) { + if (a[w] != b[w]) { + return false; + } + } + return true; +} + +__device__ inline bool spd_commutes(const u64* a, const u64* b, unsigned int words) { + unsigned int p = 0; + for (unsigned int w = 0; w < words; w++) { + p += __popcll(a[w] & b[words + w]) + __popcll(a[words + w] & b[w]); + } + return (p & 1) == 0; +} + +// out = a xor b; returns the exponent e (mod 4) of a.b = i^e (a xor b). +__device__ inline unsigned int spd_mul(const u64* a, const u64* b, u64* out, unsigned int words) { + unsigned int e = 0; + for (unsigned int w = 0; w < words; w++) { + u64 x = a[w] ^ b[w]; + u64 z = a[words + w] ^ b[words + w]; + out[w] = x; + out[words + w] = z; + e += __popcll(a[w] & a[words + w]); + e += __popcll(b[w] & b[words + w]); + e += 2 * __popcll(a[words + w] & b[w]); + e -= __popcll(x & z); + } + return e & 3; +} + +#define SPD_FOR(i, n) for (u64 i = (u64)blockIdx.x * (u64)blockDim.x + (u64)threadIdx.x; i < (n); i += (u64)gridDim.x * (u64)blockDim.x) + +// The rotation's scan. mark[i] = 1 for a live anticommuting term, with its +// new string and value; its coefficient scaled by cos. +extern "C" __global__ void spd_scan(const u64* __restrict__ keys, double* __restrict__ coefs, u64 len, unsigned int words, const u64* __restrict__ axis, double s, double c, unsigned int* __restrict__ mark, u64* __restrict__ fresh_keys, double* __restrict__ fresh_vals) { + SPD_FOR(i, len) { + double v = coefs[i]; + const u64* q = keys + 2 * words * i; + if (v == 0.0 || spd_commutes(q, axis, words)) { + mark[i] = 0; + continue; + } + unsigned int e = spd_mul(q, axis, fresh_keys + 2 * words * i, words); + double nv = v * s; + fresh_vals[i] = ((e + 3) & 3) == 2 ? -nv : nv; + coefs[i] = v * c; + mark[i] = 1; + } +} + +// The index emptied. +extern "C" __global__ void spd_fill(unsigned int* __restrict__ index, u64 cap) { + SPD_FOR(i, cap) { + index[i] = SPD_EMPTY; + } +} + +// Every slot into the index (cap a power of two, at least twice len). +extern "C" __global__ void spd_index(const u64* __restrict__ keys, u64 len, unsigned int words, unsigned int* __restrict__ index, u64 cap) { + SPD_FOR(i, len) { + u64 slot = spd_hash(keys + 2 * words * i, words) & (cap - 1); + while (atomicCAS(index + slot, SPD_EMPTY, (unsigned int)i) != SPD_EMPTY) { + slot = (slot + 1) & (cap - 1); + } + } +} + +// Each marked term's new string looked up: added where found (counting +// revived and killed coefficients), marked absent where not. +extern "C" __global__ void spd_merge(const u64* __restrict__ keys, double* __restrict__ coefs, u64 len, unsigned int words, const unsigned int* __restrict__ index, u64 cap, const unsigned int* __restrict__ mark, const u64* __restrict__ fresh_keys, const double* __restrict__ fresh_vals, unsigned int* __restrict__ absent, unsigned int* __restrict__ counters) { + SPD_FOR(i, len) { + if (!mark[i]) { + absent[i] = 0; + continue; + } + const u64* r = fresh_keys + 2 * words * i; + u64 slot = spd_hash(r, words) & (cap - 1); + unsigned int at = SPD_EMPTY; + for (;;) { + unsigned int j = index[slot]; + if (j == SPD_EMPTY) { + break; + } + if (spd_equal(keys + 2 * words * (u64)j, r, words)) { + at = j; + break; + } + slot = (slot + 1) & (cap - 1); + } + if (at == SPD_EMPTY) { + absent[i] = 1; + continue; + } + absent[i] = 0; + double before = coefs[at]; + double after = before + fresh_vals[i]; + coefs[at] = after; + if (before == 0.0 && after != 0.0) { + atomicAdd(counters + 0, 1u); + } else if (before != 0.0 && after == 0.0) { + atomicAdd(counters + 1, 1u); + } + } +} + +// The absent new terms appended at len + their prefix, in table order. +extern "C" __global__ void spd_append(u64* __restrict__ keys, double* __restrict__ coefs, u64 len, unsigned int words, const unsigned int* __restrict__ absent, const unsigned int* __restrict__ prefix, const u64* __restrict__ fresh_keys, const double* __restrict__ fresh_vals, unsigned int* __restrict__ counters) { + SPD_FOR(i, len) { + if (!absent[i]) { + continue; + } + u64 at = len + prefix[i]; + for (unsigned int w = 0; w < 2 * words; w++) { + keys[2 * words * at + w] = fresh_keys[2 * words * i + w]; + } + double v = fresh_vals[i]; + coefs[at] = v; + if (v == 0.0) { + atomicAdd(counters + 2, 1u); + } + } +} + +// Truncation: a live term below the threshold, or heavier than max_weight, +// zeroed and marked, its magnitude kept for the host. +extern "C" __global__ void spd_truncate(const u64* __restrict__ keys, double* __restrict__ coefs, u64 len, unsigned int words, double threshold, unsigned int max_weight, unsigned int* __restrict__ mark, double* __restrict__ vals) { + SPD_FOR(i, len) { + double v = coefs[i]; + mark[i] = 0; + if (v == 0.0) { + continue; + } + bool heavy = false; + if (max_weight != SPD_EMPTY) { + const u64* k = keys + 2 * words * i; + unsigned int wt = 0; + for (unsigned int w = 0; w < words; w++) { + wt += __popcll(k[w] | k[words + w]); + } + heavy = wt > max_weight; + } + if (fabs(v) < threshold || heavy) { + vals[i] = fabs(v); + coefs[i] = 0.0; + mark[i] = 1; + } + } +} + +// The live terms marked, for a compaction. +extern "C" __global__ void spd_live(const double* __restrict__ coefs, u64 len, unsigned int* __restrict__ mark) { + SPD_FOR(i, len) { + mark[i] = coefs[i] != 0.0 ? 1u : 0u; + } +} + +// The marked terms moved to their prefix, in order, into the other buffers. +extern "C" __global__ void spd_compact(const u64* __restrict__ keys, const double* __restrict__ coefs, u64 len, unsigned int words, const unsigned int* __restrict__ mark, const unsigned int* __restrict__ prefix, u64* __restrict__ out_keys, double* __restrict__ out_coefs) { + SPD_FOR(i, len) { + if (!mark[i]) { + continue; + } + u64 at = prefix[i]; + for (unsigned int w = 0; w < 2 * words; w++) { + out_keys[2 * words * at + w] = keys[2 * words * i + w]; + } + out_coefs[at] = coefs[i]; + } +} + +// The marked values gathered at their prefix, in order. +extern "C" __global__ void spd_gather(const double* __restrict__ vals, u64 len, const unsigned int* __restrict__ mark, const unsigned int* __restrict__ prefix, double* __restrict__ out) { + SPD_FOR(i, len) { + if (mark[i]) { + out[prefix[i]] = vals[i]; + } + } +} + +// The value's contributions: a live term with no x bit, its coefficient +// negated where its z bits meet `ones` an odd number of times. +extern "C" __global__ void spd_contrib(const u64* __restrict__ keys, const double* __restrict__ coefs, u64 len, unsigned int words, const u64* __restrict__ ones, unsigned int* __restrict__ mark, double* __restrict__ vals) { + SPD_FOR(i, len) { + const u64* k = keys + 2 * words * i; + double v = coefs[i]; + bool zx = v != 0.0; + unsigned int flips = 0; + for (unsigned int w = 0; w < words; w++) { + zx = zx && k[w] == 0; + flips += __popcll(k[words + w] & ones[words + w]); + } + mark[i] = zx ? 1u : 0u; + if (zx) { + vals[i] = (flips & 1) ? -v : v; + } + } +} + +// One factor's conjugation, from the host's table of the CPU's: for gate +// code g (0..7: H, S, S^dagger, sqrt X, its inverse, X, Y, Z) and factor +// (x, z), conj[4 g + 2 x + z] holds the new x (bit 2), z (bit 1) and sign +// (bit 0). +__device__ inline bool spd_one(u64* k, unsigned int words, unsigned int q, unsigned int g, const unsigned char* conj) { + unsigned int w = q / 64; + u64 b = 1ull << (q % 64); + unsigned int x = (k[w] & b) ? 1 : 0; + unsigned int z = (k[words + w] & b) ? 1 : 0; + unsigned char r = conj[4 * g + 2 * x + z]; + k[w] = (k[w] & ~b) | ((r & 4) ? b : 0); + k[words + w] = (k[words + w] & ~b) | ((r & 2) ? b : 0); + return (r & 1) != 0; +} + +__device__ inline void spd_get(const u64* k, unsigned int words, unsigned int q, bool* x, bool* z) { + unsigned int w = q / 64; + u64 b = 1ull << (q % 64); + *x = (k[w] & b) != 0; + *z = (k[words + w] & b) != 0; +} + +__device__ inline void spd_set(u64* k, unsigned int words, unsigned int q, bool x, bool z) { + unsigned int w = q / 64; + u64 b = 1ull << (q % 64); + k[w] = (k[w] & ~b) | (x ? b : 0); + k[words + w] = (k[words + w] & ~b) | (z ? b : 0); +} + +// Every term through each gate in turn. ops: four words a gate (code, a, b, +// c): codes 0..7 one factor on qubit a; 8 CX(a, b); 9 CZ(a, b); 10 swap(a, +// b); 11 a quarter turn k = a about the string at axes + 2 words b. +extern "C" __global__ void spd_cliffords(u64* __restrict__ keys, double* __restrict__ coefs, u64 len, unsigned int words, const unsigned int* __restrict__ ops, unsigned int count, const u64* __restrict__ axes, const unsigned char* __restrict__ conj) { + u64 r[32]; + SPD_FOR(i, len) { + u64* k = keys + 2 * words * i; + double v = coefs[i]; + for (unsigned int g = 0; g < count; g++) { + unsigned int code = ops[4 * g], a = ops[4 * g + 1], b = ops[4 * g + 2]; + bool neg = false; + if (code < 8) { + neg = spd_one(k, words, a, code, conj); + } else if (code == 8) { + bool xc, zc, xt, zt; + spd_get(k, words, a, &xc, &zc); + spd_get(k, words, b, &xt, &zt); + spd_set(k, words, b, xt ^ xc, zt); + spd_set(k, words, a, xc, zc ^ zt); + neg = xc && zt && !(xt ^ zc); + } else if (code == 9) { + bool xa, za, xb, zb; + spd_get(k, words, a, &xa, &za); + spd_get(k, words, b, &xb, &zb); + spd_set(k, words, a, xa, za ^ xb); + spd_set(k, words, b, xb, zb ^ xa); + neg = xa && xb && (za ^ zb); + } else if (code == 10) { + bool xa, za, xb, zb; + spd_get(k, words, a, &xa, &za); + spd_get(k, words, b, &xb, &zb); + spd_set(k, words, a, xb, zb); + spd_set(k, words, b, xa, za); + } else { + const u64* p = axes + 2 * words * (u64)b; + if (!spd_commutes(k, p, words)) { + if (a == 2) { + neg = true; + } else { + unsigned int e = spd_mul(k, p, r, words); + for (unsigned int w = 0; w < 2 * words; w++) { + k[w] = r[w]; + } + neg = ((e + 3) & 3) == 2; + if (a == 3) { + neg = !neg; + } + } + } + } + if (neg) { + v = -v; + } + } + coefs[i] = v; + } +} + +// An exclusive prefix sum within each block of SPD_BLOCK marks, and each +// block's total; the host scans the totals and spd_offsets adds them back. +extern "C" __global__ void spd_prefix(const unsigned int* __restrict__ mark, u64 len, unsigned int* __restrict__ prefix, unsigned int* __restrict__ totals) { + __shared__ unsigned int s[SPD_BLOCK]; + u64 i = (u64)blockIdx.x * SPD_BLOCK + threadIdx.x; + unsigned int v = i < len ? mark[i] : 0; + s[threadIdx.x] = v; + __syncthreads(); + for (unsigned int d = 1; d < SPD_BLOCK; d <<= 1) { + unsigned int t = threadIdx.x >= d ? s[threadIdx.x - d] : 0; + __syncthreads(); + s[threadIdx.x] += t; + __syncthreads(); + } + if (i < len) { + prefix[i] = s[threadIdx.x] - v; + } + if (threadIdx.x == SPD_BLOCK - 1) { + totals[blockIdx.x] = s[SPD_BLOCK - 1]; + } +} + +extern "C" __global__ void spd_offsets(unsigned int* __restrict__ prefix, u64 len, const unsigned int* __restrict__ offsets) { + SPD_FOR(i, len) { + prefix[i] += offsets[i / SPD_BLOCK]; + } +} diff --git a/wai/wai-quantum/src/quantum_fabric/spd.rs b/wai/wai-quantum/src/quantum_fabric/spd.rs new file mode 100644 index 000000000..643302c11 --- /dev/null +++ b/wai/wai-quantum/src/quantum_fabric/spd.rs @@ -0,0 +1,107 @@ +//! Sparse Pauli dynamics on a GPU, bit for bit: the kernels a device runs +//! for [`crate::quantum_spd::device`]'s steps, their implementation id, and +//! the mutants a conforming device must be caught running. +//! +//! The kernels (`spd.cu`, [`SOURCE`]) are compiled without fused +//! multiply-add, so a coefficient's every operation is one IEEE binary64 +//! operation: the multiply by `sin`, the multiply by `cos`, the addition +//! where a new term meets an old one, and sign changes. The order of the +//! terms is kept by exclusive prefix sums of integer marks, never by which +//! thread ran first, and every list a step hands the host is in table order; +//! the host adds the doubles. A member's certificate is earned on +//! [`SPD_DEFAULT`](crate::quantum_spd::device::SPD_DEFAULT) against the CPU: +//! every value, bound, threshold reached, peak, final count and term-update +//! count the same, bit for bit. + +use crate::quantum_fabric::wgsl::{Detector, Mutant}; + +/// The CUDA kernels, `spd.cu`, exactly as they are compiled; the HIP +/// member's runtime compiler builds the same text. +pub const SOURCE: &str = include_str!("spd.cu"); + +/// The implementation name the CUDA id starts with. +pub const IMPLEMENTATION: &str = "cuda-spd"; + +/// Version of the host-side layout: a term's string as `2·words` words (x +/// then z), the coefficients apart, the kernels in [`KERNELS`], the +/// four-word gate encoding and the conjugation table +/// ([`crate::quantum_spd::device::conj_table`]), the index at least twice the +/// table, marks and prefix sums in blocks of [`BLOCK`]. Part of the +/// implementation id. +pub const HOST_LAYOUT: u32 = 1; + +/// The marks one prefix-sum block sums (`SPD_BLOCK`). +pub const BLOCK: u32 = 256; + +/// The kernels a member loads, by name. +pub const KERNELS: [&str; 13] = ["spd_scan", "spd_fill", "spd_index", "spd_merge", "spd_append", "spd_truncate", "spd_live", "spd_compact", "spd_gather", "spd_contrib", "spd_cliffords", "spd_prefix", "spd_offsets"]; + +/// The compile options: no fused multiply-add. +pub const COMPILE_OPTIONS: [&str; 2] = ["--std=c++17", "--fmad=false"]; + +const DOMAIN_IMPLEMENTATION: &[u8] = b"wai:quantum-fabric-spd\x01"; + +fn put(o: &mut Vec, bytes: &[u8]) { + o.extend_from_slice(&(bytes.len() as u64).to_be_bytes()); + o.extend_from_slice(bytes); +} + +/// `"cuda-spd/<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 mutants a conforming device must be caught running: each changes one +/// step's arithmetic or one decision of the term set. +pub const MUTANTS: [Mutant; 6] = [ + // The new term's sign is lost. + Mutant { name: "P1", find: "fresh_vals[i] = ((e + 3) & 3) == 2 ? -nv : nv;", replace: "fresh_vals[i] = nv;", detector: Detector::Certificate }, + // Truncation reads the coefficient, not its magnitude: every negative + // term is dropped. + Mutant { name: "P2", find: "if (fabs(v) < threshold || heavy) {", replace: "if (v < threshold || heavy) {", detector: Detector::Certificate }, + // The appended terms go in the reverse of the CPU's order. + Mutant { name: "P3", find: "u64 at = len + prefix[i];", replace: "u64 at = len + (prefix[len - 1] + absent[len - 1] - 1 - prefix[i]);", detector: Detector::Certificate }, + // A dead slot is not found: a reappearing string is appended again + // rather than revived in place. + Mutant { name: "P4", find: " if (spd_equal(keys + 2 * words * (u64)j, r, words)) {", replace: " if (coefs[j] != 0.0 && spd_equal(keys + 2 * words * (u64)j, r, words)) {", detector: Detector::Certificate }, + // The old coefficient is scaled by sin, not cos. + Mutant { name: "P5", find: "coefs[i] = v * c;", replace: "coefs[i] = v * s;", detector: Detector::Certificate }, + // A string at the weight cap is dropped, which the CPU keeps. + Mutant { name: "P6", find: "heavy = wt > max_weight;", replace: "heavy = wt >= max_weight;", detector: Detector::Certificate }, +]; + +/// [`SOURCE`] with a mutant applied. Panics unless its `find` occurs exactly +/// once. +pub fn mutate(m: &Mutant) -> String { + assert_eq!(SOURCE.matches(m.find).count(), 1, "mutant {}", m.name); + SOURCE.replacen(m.find, m.replace, 1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_implementation_id_follows_the_kernel_and_every_mutant_edits_once() { + let id = implementation_id(); + assert!(id.starts_with("cuda-spd/") && id.len() == "cuda-spd/".len() + 16); + for m in &MUTANTS { + assert_ne!(implementation_id_of(&mutate(m)), id, "{}", m.name); + } + for k in KERNELS { + assert!(SOURCE.contains(&format!("void {k}(")), "{k}"); + } + assert!(SOURCE.contains(&format!("#define SPD_BLOCK {BLOCK}"))); + } +} diff --git a/wai/wai-quantum/src/quantum_fabric/sv64.cl b/wai/wai-quantum/src/quantum_fabric/sv64.cl index 2603fe472..f2bf75ba5 100644 --- a/wai/wai-quantum/src/quantum_fabric/sv64.cl +++ b/wai/wai-quantum/src/quantum_fabric/sv64.cl @@ -17,6 +17,18 @@ // // Each thread owns one amplitude, pair or group, and a launch is one gate, so // no thread reads what another writes within a launch. +// +// The state stays on the device for a whole run: the host uploads it (or +// zeroes it there) once, writes every launch's parameters once as a table of +// three arrays (bit positions, offsets, doubles), enqueues each gate in order +// on one in-order queue with no wait between them, and reads the state back +// once. A gate's parameters are read from the table at the indices the host +// gives. +// +// A gate on k = 2 to 5 qubits takes the kernel built for its group size, +// whose group the compiler may hold in registers; a wider one takes +// `sv_many`, which reads k at run time. Both run the same expressions in the +// same order. #pragma OPENCL EXTENSION cl_khr_fp64 : enable #pragma OPENCL FP_CONTRACT OFF @@ -30,10 +42,14 @@ __kernel void sv_diag( __global double* restrict re, __global double* restrict im, u64 len, - __global const uint* restrict bitpos, + __global const uint* restrict u32s, + u64 bits_at, uint k, - __global const double* restrict diag) + __global const double* restrict f64s, + u64 diag_at) { + __global const uint* bitpos = u32s + bits_at; + __global const double* diag = f64s + diag_at; u64 x = (u64)get_global_id(0); u64 stride = (u64)get_global_size(0); for (; x < len; x += stride) { @@ -75,24 +91,28 @@ __kernel void sv_one( } } -__kernel void sv_many( +// A gate on K qubits (K = 0: k, read at run time), each thread a group of +// 2^K amplitudes. +inline __attribute__((always_inline)) void 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) + __global const double* restrict m, + const uint K) { u64 t = (u64)get_global_id(0); u64 stride = (u64)get_global_size(0); - uint d = 1u << k; + const uint kk = K ? K : k; + const uint d = 1u << kk; 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++) { + for (uint b = 0; b < kk; b++) { u64 pos = sorted[b]; u64 low = base & (((u64)1 << pos) - 1); base = ((base >> pos) << (pos + 1)) | low; @@ -114,3 +134,25 @@ __kernel void sv_many( } } } + +#define SV_MANY(name, K) \ + __kernel void name( \ + __global double* restrict re, \ + __global double* restrict im, \ + u64 groups, \ + __global const uint* restrict u32s, \ + u64 bits_at, \ + uint k, \ + __global const u64* restrict u64s, \ + u64 offsets_at, \ + __global const double* restrict f64s, \ + u64 m_at) \ + { \ + many(re, im, groups, u32s + bits_at, k, u64s + offsets_at, f64s + m_at, K); \ + } + +SV_MANY(sv_many4, 2) +SV_MANY(sv_many8, 3) +SV_MANY(sv_many16, 4) +SV_MANY(sv_many32, 5) +SV_MANY(sv_many, 0) diff --git a/wai/wai-quantum/src/quantum_fabric/sv64.cu b/wai/wai-quantum/src/quantum_fabric/sv64.cu index d62771d36..5de1877b0 100644 --- a/wai/wai-quantum/src/quantum_fabric/sv64.cu +++ b/wai/wai-quantum/src/quantum_fabric/sv64.cu @@ -15,6 +15,16 @@ // // Each thread owns one amplitude, pair or group, and a launch is one gate, so // no thread reads what another writes within a launch. +// +// The state stays on the device for a whole run: the host uploads it (or +// zeroes it there) once, uploads every launch's parameters once as a table, +// launches each gate in order on one stream with no wait between them, and +// reads the state back once. A gate's bit positions, diagonal, offsets and +// matrix are read from that table where the host points. +// +// A gate on k = 2 to 5 qubits takes the kernel compiled for its group size, +// whose group is held in registers; a wider one takes `sv_many`, which reads +// k at run time. Both run the same expressions in the same order. typedef unsigned long long u64; @@ -70,7 +80,10 @@ extern "C" __global__ void sv_one( } } -extern "C" __global__ void sv_many( +// A gate on K qubits (K = 0: k, read at run time), each thread a group of +// 2^K amplitudes. +template +__device__ inline void many( double* __restrict__ re, double* __restrict__ im, u64 groups, @@ -81,13 +94,14 @@ extern "C" __global__ void sv_many( { 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]; + const unsigned int kk = K ? K : k; + const unsigned int d = 1u << kk; + double vr[K ? (1u << K) : SV_MAX_D]; + double vi[K ? (1u << K) : 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++) { + for (unsigned int b = 0; b < kk; b++) { u64 pos = sorted[b]; u64 low = base & ((1ull << pos) - 1); base = ((base >> pos) << (pos + 1)) | low; @@ -109,3 +123,22 @@ extern "C" __global__ void sv_many( } } } + +#define SV_MANY(name, K) \ + extern "C" __global__ void name( \ + double* __restrict__ re, \ + double* __restrict__ im, \ + u64 groups, \ + const unsigned int* __restrict__ sorted, \ + unsigned int k, \ + const u64* __restrict__ offsets, \ + const double* __restrict__ m) \ + { \ + many(re, im, groups, sorted, k, offsets, m); \ + } + +SV_MANY(sv_many4, 2) +SV_MANY(sv_many8, 3) +SV_MANY(sv_many16, 4) +SV_MANY(sv_many32, 5) +SV_MANY(sv_many, 0) diff --git a/wai/wai-quantum/src/quantum_fabric/sv64.rs b/wai/wai-quantum/src/quantum_fabric/sv64.rs index 5d6c244fa..4f18579ca 100644 --- a/wai/wai-quantum/src/quantum_fabric/sv64.rs +++ b/wai/wai-quantum/src/quantum_fabric/sv64.rs @@ -19,6 +19,17 @@ //! IEEE binary64 operations in the same order, with no fused multiply-add, //! returns the same bits; its certificate says whether it did. //! +//! **Resident.** A member runs a whole plan with the state on the device: the +//! state is uploaded once (or zeroed there, for a run from `|0…0⟩`), every +//! launch's parameters are uploaded once as a [`LaunchTable`], each launch is +//! enqueued in order on one stream or in-order queue with no wait between +//! them, and the state is read back once. That is the host layout +//! [`HOST_LAYOUT`] names, part of every implementation id, so a certificate +//! earned on it is a certificate of the resident path and of nothing else. +//! A wider gate (k = 2 to 5) takes the kernel compiled for its group size +//! ([`many_kernel`]), its group held in registers; each runs the same +//! expressions in the same order as the kernel that reads k at run time. +//! //! The kernel for CUDA devices is [`SOURCE`] (`sv64.cu`). use crate::gates::Gate; @@ -29,6 +40,16 @@ 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 same run with gates on four to six qubits at once (fusion widths 4 to +/// 6, or wider gates): the kernels [`KERNEL_SV_RUN`]'s corpus never reaches +/// (`sv_many16`, `sv_many32`, `sv_many`), certified on a corpus of their own +/// ([`SV_WIDE`]). A member placed for a run whose widest launch acts on more +/// than [`NARROW`] qubits holds this certificate too. +pub const KERNEL_SV_RUN_WIDE: &str = "wai.quantum.sv/run-wide"; + +/// The widest launch [`KERNEL_SV_RUN`]'s corpus certifies: three qubits. +pub const NARROW: u32 = 3; + /// The CUDA kernels, `sv64.cu`, exactly as they are compiled. pub const SOURCE: &str = include_str!("sv64.cu"); @@ -36,9 +57,28 @@ pub const SOURCE: &str = include_str!("sv64.cu"); 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 +/// launch per planned gate, the launch kinds and their parameters, and the +/// run resident (version 2): the state uploaded or zeroed once, the +/// parameters uploaded once as a [`LaunchTable`], every launch enqueued with +/// no wait between them, the state read back once. Part of the /// implementation id. -pub const HOST_LAYOUT: u32 = 1; +pub const HOST_LAYOUT: u32 = 2; + +/// The kernels a member loads, by name: the diagonal, the one-qubit gate, the +/// wider gates by group size, and the wider gate that reads k at run time. +pub const KERNELS: [&str; 7] = ["sv_diag", "sv_one", "sv_many4", "sv_many8", "sv_many16", "sv_many32", "sv_many"]; + +/// The kernel a gate on `k ≥ 2` qubits takes ([`KERNELS`]): the one compiled +/// for its group size, for k = 2 to 5, or `sv_many`. +pub fn many_kernel(k: u32) -> &'static str { + match k { + 2 => "sv_many4", + 3 => "sv_many8", + 4 => "sv_many16", + 5 => "sv_many32", + _ => "sv_many", + } +} /// The largest gate a launch takes: `2^k ≤ MAX_D` (the kernel's `SV_MAX_D`). pub const MAX_D: usize = 64; @@ -246,6 +286,92 @@ pub fn replay(re: &mut [f64], im: &mut [f64], launches: &[Launch]) { } } +/// The most qubits whose state, two f64 arrays of `2^n`, fits in `bytes`, at +/// most [`crate::quantum_sv::MAX_QUBITS`]; 0 when not even one qubit's does. +pub fn qubits_in(bytes: u64) -> u32 { + let amplitudes = bytes / 16; + if amplitudes < 2 { 0 } else { amplitudes.ilog2().min(crate::quantum_sv::MAX_QUBITS) } +} + +/// One launch as a member runs it from a [`LaunchTable`]: its kernel and +/// where its parameters start in the table's arrays. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Entry { + /// `sv_diag`: the `k` bit positions at `u32s[bits..]`, the diagonal's + /// `2·2^k` doubles (real, imaginary) at `f64s[diag..]`. + Diag { k: u32, bits: usize, diag: usize }, + /// `sv_one`: the stride and the matrix's eight doubles, passed by value. + One { s: u64, m: [f64; 8] }, + /// `sv_many…` ([`many_kernel`]): the `k` target bits ascending at + /// `u32s[bits..]`, the `2^k` offsets at `u64s[offsets..]`, the matrix's + /// `2·4^k` doubles, row-major, at `f64s[m..]`. + Many { k: u32, bits: usize, offsets: usize, m: usize }, +} + +/// A plan's launches as a member uploads them, once, before the first: +/// every parameter in three arrays, and each launch as an [`Entry`] into +/// them. Each array holds at least one element, so no upload is empty. +#[derive(Clone, Debug, PartialEq)] +pub struct LaunchTable { + pub f64s: Vec, + pub u32s: Vec, + pub u64s: Vec, + pub entries: Vec, +} + +impl LaunchTable { + pub fn new(launches: &[Launch]) -> LaunchTable { + let (mut f64s, mut u32s, mut u64s) = (Vec::new(), Vec::new(), Vec::new()); + let entries = launches + .iter() + .map(|l| match l { + Launch::Diag { bitpos, diag } => { + let e = Entry::Diag { k: bitpos.len() as u32, bits: u32s.len(), diag: f64s.len() }; + u32s.extend_from_slice(bitpos); + f64s.extend(diag.iter().flat_map(|c| [c.re, c.im])); + e + } + Launch::One { s, m } => Entry::One { s: *s, m: [m[0].re, m[0].im, m[1].re, m[1].im, m[2].re, m[2].im, m[3].re, m[3].im] }, + Launch::Many { sorted, offsets, m } => { + let e = Entry::Many { k: sorted.len() as u32, bits: u32s.len(), offsets: u64s.len(), m: f64s.len() }; + u32s.extend_from_slice(sorted); + u64s.extend_from_slice(offsets); + f64s.extend(m.iter().flat_map(|c| [c.re, c.im])); + e + } + }) + .collect(); + if f64s.is_empty() { + f64s.push(0.0); + } + if u32s.is_empty() { + u32s.push(0); + } + if u64s.is_empty() { + u64s.push(0); + } + LaunchTable { f64s, u32s, u64s, entries } + } + + /// The launches as a member reads them back from the table: equal to + /// those it was made from, which the tests hold it to. + pub fn launches(&self) -> Vec { + let c = |v: &[f64]| -> Vec { v.chunks(2).map(|p| C { re: p[0], im: p[1] }).collect() }; + self.entries + .iter() + .map(|e| match *e { + Entry::Diag { k, bits, diag } => Launch::Diag { bitpos: self.u32s[bits..bits + k as usize].to_vec(), diag: c(&self.f64s[diag..diag + (2usize << k)]) }, + Entry::One { s, m } => Launch::One { s, m: [C { re: m[0], im: m[1] }, C { re: m[2], im: m[3] }, C { re: m[4], im: m[5] }, C { re: m[6], im: m[7] }] }, + Entry::Many { k, bits, offsets, m } => Launch::Many { + sorted: self.u32s[bits..bits + k as usize].to_vec(), + offsets: self.u64s[offsets..offsets + (1usize << k)].to_vec(), + m: c(&self.f64s[m..m + (2usize << (2 * k))]), + }, + }) + .collect() + } +} + /// The index bits `l` reads and writes, as its parameters name them: a /// diagonal's in its gate order, a one-qubit gate's one, a wider gate's /// ascending. @@ -464,6 +590,17 @@ pub const SV_DEFAULT: SvCorpus = SvCorpus { seed: 7, cases: 48, max_qubits: 10, /// every case: what a bit-exact certificate reproduces. pub const SV_DEFAULT_PIN: &str = "b7d129d0593adf70dc164477664970c5c0b8c09f79f1a0573036bd60888dbcd1"; +/// The corpus of the wider kernels: [`SvCorpus`]'s draws at fusion widths 4, +/// 5 and 6 in turn ([`SvCorpus::wide_cases`]), 120 cases of 2 to 12 qubits +/// and 40 gates. Larger than [`SV_DEFAULT`]: a device's compiler was seen +/// to reorder the wider rows' sums on about one input in six, which a corpus +/// a third this size let through. +pub const SV_WIDE: SvCorpus = SvCorpus { seed: 8, cases: 120, max_qubits: 12, depth: 40 }; + +/// The corpus digest of [`SV_WIDE`]'s wide cases when the device's state is +/// the CPU's on every case. +pub const SV_WIDE_PIN: &str = "093614f2386ec5120ab6ccde5187a544518432751591535a6f58c7a44061ebaf"; + /// One case: the width, the fusion width and the gates. #[derive(Clone, Debug)] pub struct SvCase { @@ -486,6 +623,24 @@ impl SvCorpus { format!("sv64-v1 seed={} cases={} qubits={} depth={}", self.seed, self.cases, self.max_qubits, self.depth) } + /// `"sv64-wide-v1 seed=… cases=… qubits=… depth=…"`. + pub fn wide_generator(&self) -> String { + format!("sv64-wide-v1 seed={} cases={} qubits={} depth={}", self.seed, self.cases, self.max_qubits, self.depth) + } + + /// The same draws as [`cases`](Self::cases), at fusion widths 4, 5 and 6 + /// in turn: what [`KERNEL_SV_RUN_WIDE`] is certified on. + pub fn wide_cases(&self) -> Vec { + self.cases() + .into_iter() + .enumerate() + .map(|(i, mut c)| { + c.width = 4 + i % 3; + c + }) + .collect() + } + pub fn cases(&self) -> Vec { let mut s = self.seed; (0..self.cases) @@ -518,6 +673,22 @@ impl SvCorpus { } } +/// Random cases beyond the pinned corpus, for ground-truth tests: `count` +/// circuits from `seed`, as [`SvCorpus`] draws them, of 2 to `max_qubits` +/// qubits and `depth` gates, at fusion widths 0 to 6 in turn, so every +/// kernel of [`KERNELS`] runs. +pub fn random_cases(seed: u64, count: usize, max_qubits: u32, depth: usize) -> Vec { + SvCorpus { seed, cases: count, max_qubits, depth } + .cases() + .into_iter() + .enumerate() + .map(|(i, mut c)| { + c.width = i % 7; + c + }) + .collect() +} + impl SvCase { /// The case as bytes: its width, fusion width, and each gate's qubits and /// matrix bits. @@ -547,6 +718,19 @@ impl SvCase { } } +/// The certificate of `kernel` over `cases`, each run by `run` (the state +/// from `|0…0⟩`, real and imaginary parts) against the CPU's state vector; +/// a case `run` refuses ends the certification with its error. +pub fn certify_cases(kernel: &str, implementation: &str, member: &str, generator: &str, cases: &[SvCase], mut run: impl FnMut(&SvCase) -> Result<(Vec, Vec), E>, refused: impl Fn() -> E) -> Result { + let mut cert = crate::quantum_fabric::Certification::new(); + for case in cases { + let reference = case.reference().map_err(|_| refused())?; + let (re, im) = run(case)?; + cert.add(&case.bytes(), &reference, &state_output(&re, &im)); + } + Ok(cert.finish(kernel, implementation, member, generator)) +} + /// 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()) @@ -622,6 +806,72 @@ mod tests { 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); + let mut cert = crate::quantum_fabric::Certification::new(); + for case in SV_WIDE.wide_cases() { + let r = case.reference().unwrap(); + cert.add(&case.bytes(), &r, &r); + } + let c = cert.finish(KERNEL_SV_RUN_WIDE, "cpu", "cpu", &SV_WIDE.wide_generator()); + let hex: String = c.corpus.iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!(hex, SV_WIDE_PIN); + } + + /// The pinned corpus reaches the narrow kernels and no other; the wide + /// corpus reaches the three wider ones, each of them. + #[test] + fn each_corpus_certifies_the_kernels_it_names() { + let reached = |cases: Vec| -> Vec<&'static str> { + let mut used = std::collections::BTreeSet::new(); + for case in cases { + for e in &LaunchTable::new(&plan(case.n, &case.gates, case.width).unwrap()).entries { + used.insert(match *e { + Entry::Diag { .. } => "sv_diag", + Entry::One { .. } => "sv_one", + Entry::Many { k, .. } => many_kernel(k), + }); + } + } + used.into_iter().collect() + }; + assert_eq!(reached(SV_DEFAULT.cases()), ["sv_diag", "sv_many4", "sv_many8", "sv_one"]); + let wide = reached(SV_WIDE.wide_cases()); + for k in ["sv_many16", "sv_many32", "sv_many"] { + assert!(wide.contains(&k), "{k}: {wide:?}"); + } + assert!(SV_DEFAULT.cases().iter().all(|c| plan(c.n, &c.gates, c.width).unwrap().iter().all(|l| launch_bits(l).len() as u32 <= NARROW))); + } + + /// The table a member uploads holds every launch as it was planned, and + /// the cases the device tests run reach every kernel. + #[test] + fn a_launch_table_holds_every_launch_as_it_was_planned() { + let mut used = std::collections::BTreeSet::new(); + let cases: Vec = SV_DEFAULT.cases().into_iter().chain(random_cases(11, 42, 12, 40)).collect(); + for case in &cases { + let launches = plan(case.n, &case.gates, case.width).unwrap(); + let table = LaunchTable::new(&launches); + assert_eq!(table.launches(), launches, "n={} width={}", case.n, case.width); + assert!(!table.f64s.is_empty() && !table.u32s.is_empty() && !table.u64s.is_empty()); + for e in &table.entries { + used.insert(match *e { + Entry::Diag { .. } => "sv_diag", + Entry::One { .. } => "sv_one", + Entry::Many { k, .. } => many_kernel(k), + }); + } + // Replayed from the table, the CPU's state. + 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, &table.launches()); + let mut sv = StateVector::zero(case.n).unwrap(); + sv.run(&case.gates, case.width).unwrap(); + assert!(re.iter().zip(&sv.re).chain(im.iter().zip(&sv.im)).all(|(a, b)| a.to_bits() == b.to_bits()), "n={} width={}", case.n, case.width); + } + assert_eq!(used.into_iter().collect::>(), { let mut k = KERNELS.to_vec(); k.sort_unstable(); k }, "every kernel is reached"); + assert_eq!((qubits_in(0), qubits_in(31), qubits_in(32), qubits_in(1 << 31), qubits_in(3 << 30), qubits_in(u64::MAX)), (0, 0, 1, 27, 27, crate::quantum_sv::MAX_QUBITS)); + let empty = LaunchTable::new(&[]); + assert_eq!((empty.f64s.len(), empty.u32s.len(), empty.u64s.len(), empty.entries.len()), (1, 1, 1, 0)); } #[test] @@ -793,6 +1043,11 @@ mod tests { 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")); + // Every kernel a member loads is in both texts, by name. + for k in KERNELS { + let named = |src: &str| src.contains(&format!("void {k}(")) || src.contains(&format!("SV_MANY({k},")); + assert!(named(SOURCE) && named(CL_SOURCE), "{k}"); + } let hip = hip_implementation_id(); assert!(hip.starts_with("hip-sv64/") && hip != id && hip != cl); for m in &MUTANTS { diff --git a/wai/wai-quantum/src/quantum_fabric_cuda.rs b/wai/wai-quantum/src/quantum_fabric_cuda.rs index 91ef71941..1326bb663 100644 --- a/wai/wai-quantum/src/quantum_fabric_cuda.rs +++ b/wai/wai-quantum/src/quantum_fabric_cuda.rs @@ -27,6 +27,10 @@ //! own implementation id, so its certificates are certificates of that //! silicon path. A device below [`cuda::TENSOR_MIN_COMPUTE`] is //! [`CudaError::Unsupported`]. +//! - **Sparse Pauli dynamics** (`CudaMember::spd`, with `quantum_spd`): the +//! propagation's sum of terms on the device (`quantum_spd::device`), the +//! kernels of `quantum_fabric::spd`, certified against the CPU on its +//! pinned corpus, result and final table alike. //! - **Placement, receipts and metering** are as for the WebGPU member: //! [`place`](crate::quantum_fabric::place), //! [`FabricReceipt`](crate::quantum_fabric::FabricReceipt) and a caller's @@ -276,6 +280,7 @@ fn open(ordinal: usize) -> Result { discovered: detail.clone(), detail, max_qubits, + free: free as u64, identity, }; member.probed().map_err(refuse) @@ -331,6 +336,8 @@ pub struct CudaMember { discovered: String, detail: String, max_qubits: u8, + /// The device's free memory at discovery, in bytes. + free: u64, } impl std::fmt::Debug for CudaMember { @@ -633,17 +640,28 @@ impl CudaMember { /// ([`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. +/// certificate says it did not. A run is resident: the state is uploaded (or +/// zeroed on the device) once, the launch table once, every launch goes on +/// the member's stream with no wait between them, and the state is read back +/// once. #[cfg(feature = "quantum_sv")] #[derive(Clone)] pub struct CudaSv { member: CudaMember, - diag: CudaFunction, - one: CudaFunction, - many: CudaFunction, + /// The kernels of [`sv64::KERNELS`](crate::quantum_fabric::sv64::KERNELS), in order. + kernels: Vec, implementation: String, + /// Two page-locked buffers a large state is read back through, in turn, + /// each with the event its last copy recorded; allocated with the first + /// such readback. + staging: Arc, cudarc::driver::CudaEvent)>>>, } +/// Doubles in each page-locked readback buffer (16 MiB); a state this size or +/// smaller is read back directly. +#[cfg(feature = "quantum_sv")] +const STAGE: usize = 1 << 21; + #[cfg(feature = "quantum_sv")] impl std::fmt::Debug for CudaSv { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -669,13 +687,8 @@ impl CudaMember { }; 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), - }) + let kernels = sv64::KERNELS.iter().map(|k| module.load_function(k).map_err(driver)).collect::, _>>()?; + Ok(CudaSv { member: self.clone(), kernels, implementation: sv64::implementation_id_of(source), staging: Arc::new(Mutex::new(Vec::new())) }) } } @@ -689,7 +702,16 @@ impl CudaSv { /// The most qubits a state of this member holds: two f64 arrays of /// `2^n` in half its free memory at discovery. pub fn max_qubits(&self) -> u32 { - u32::from(self.member.max_qubits.saturating_sub(1)) + crate::quantum_fabric::sv64::qubits_in(self.member.free / 2) + } + + fn too_wide(&self, n: u32) -> CudaError { + CudaError::TooWide { n_qubits: n.min(255) as u8, max: self.max_qubits().min(255) as u8 } + } + + fn function(&self, name: &str) -> &CudaFunction { + let i = crate::quantum_fabric::sv64::KERNELS.iter().position(|k| *k == name).expect("a kernel of KERNELS"); + &self.kernels[i] } /// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses @@ -697,14 +719,13 @@ impl CudaSv { pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec, Vec), CudaError> { use crate::quantum_fabric::sv64::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 > self.max_qubits() { - return Err(CudaError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) }); - } - let len = 1usize << n; - let mut re = vec![0.0f64; len]; - re[0] = 1.0; - self.run_launches(&re, &vec![0.0f64; len], &launches) + self.run_from_zero(n, &launches) + } + + /// `launches` on `n` qubits from `|0…0⟩`, the state zeroed on the device + /// rather than uploaded. + pub fn run_from_zero(&self, n: u32, launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), CudaError> { + self.resident(n, None, launches) } /// `launches` ([`sv64::plan`](crate::quantum_fabric::sv64::plan)'s, or a @@ -712,72 +733,136 @@ impl CudaSv { /// unless the state is `2^n` amplitudes this member holds and every launch /// acts within it. pub fn run_launches(&self, re: &[f64], im: &[f64], launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), CudaError> { - use crate::quantum_fabric::sv64::{Launch, fits}; let len = re.len(); if !len.is_power_of_two() || len < 2 || im.len() != len { return Err(CudaError::Internal("a state that is not 2^n amplitudes, n at least 1")); } - let n = len.trailing_zeros(); - if n > self.max_qubits() { - return Err(CudaError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) }); + self.resident(len.trailing_zeros(), Some((re, im)), launches) + } + + /// The resident run: the state from `start` (or `|0…0⟩`), every launch + /// on the device, the state back. + fn resident(&self, n: u32, start: Option<(&[f64], &[f64])>, launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), CudaError> { + use crate::quantum_fabric::sv64::{Entry, LaunchTable, fits, many_kernel}; + if n == 0 || n > self.max_qubits() { + return Err(self.too_wide(n)); } if !fits(launches, n) { return Err(CudaError::Internal("a launch outside the state")); } + let len = 1usize << n; let stream = &self.member.stream; - let mut re = stream.clone_htod(re).map_err(driver)?; - let mut im = stream.clone_htod(im).map_err(driver)?; - let flat = |m: &[crate::linalg::C]| -> Vec { 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); + let table = LaunchTable::new(launches); + let f64s = stream.clone_htod(&table.f64s).map_err(driver)?; + let u32s = stream.clone_htod(&table.u32s).map_err(driver)?; + let u64s = stream.clone_htod(&table.u64s).map_err(driver)?; + let (mut re, mut im) = match start { + Some((r, i)) => (stream.clone_htod(r).map_err(driver)?, stream.clone_htod(i).map_err(driver)?), + None => { + let mut re = stream.alloc_zeros::(len).map_err(driver)?; + let im = stream.alloc_zeros::(len).map_err(driver)?; + let one = [1.0f64]; + stream.memcpy_htod(&one[..], &mut re.slice_mut(0..1)).map_err(driver)?; + (re, im) + } + }; + for e in &table.entries { + match *e { + Entry::Diag { k, bits, diag } => { + let bp = u32s.slice(bits..bits + k as usize); + let dg = f64s.slice(diag..diag + (2usize << k)); + let count = len as u64; + let mut a = stream.launch_builder(self.function("sv_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 } => { + Entry::One { s, m } => { let pairs = (len / 2) as u64; - let v: Vec = 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 { + let mut a = stream.launch_builder(self.function("sv_one")); + a.arg(&mut re).arg(&mut im).arg(&pairs).arg(&s); + for x in &m { 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); + Entry::Many { k, bits, offsets, m } => { + let so = u32s.slice(bits..bits + k as usize); + let of = u64s.slice(offsets..offsets + (1usize << k)); + let mm = f64s.slice(m..m + (2usize << (2 * k))); + let groups = (len >> k) as u64; + let mut a = stream.launch_builder(self.function(many_kernel(k))); 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. + // which `plan` holds, and the kernel compiled for a group + // size is given that size. unsafe { a.launch(launch(groups)) }.map_err(driver)?; } } } - Ok((stream.clone_dtoh(&re).map_err(driver)?, stream.clone_dtoh(&im).map_err(driver)?)) + Ok((self.readback(&re)?, self.readback(&im)?)) + } + + /// The contents of `dev`. A large state comes back through two + /// page-locked buffers in turn, each chunk copied out while the next + /// crosses, rather than through the driver's staging of pageable memory; + /// the bytes are the same either way. + fn readback(&self, dev: &CudaSlice) -> Result, CudaError> { + let stream = &self.member.stream; + let len = dev.len(); + if len <= STAGE { + return stream.clone_dtoh(dev).map_err(driver); + } + let mut stages = self.staging.lock().unwrap_or_else(|p| p.into_inner()); + while stages.len() < 2 { + // SAFETY: the buffer is written by a copy before it is read. + let buffer = unsafe { self.member.ctx.alloc_pinned_with_flags::(STAGE, 0) }; + match (buffer, self.member.ctx.new_event(None)) { + (Ok(b), Ok(e)) => stages.push((b, e)), + // No page-locked memory to be had: the driver's own staging. + _ => return stream.clone_dtoh(dev).map_err(driver), + } + } + let mut out: Vec = Vec::new(); + out.try_reserve_exact(len).map_err(|e| CudaError::Memory(e.to_string()))?; + let chunks: Vec<(usize, usize)> = (0..len).step_by(STAGE).map(|s| (s, (s + STAGE).min(len))).collect(); + // Each copy is followed by its buffer's event, which the host waits + // on before it reads that buffer: the stream itself records none + // when it is the context's only stream. + let copy = |(buf, done): &mut (cudarc::driver::PinnedHostSlice, cudarc::driver::CudaEvent), s: usize, e: usize| -> Result<(), CudaError> { + stream.memcpy_dtoh(&dev.slice(s..e), buf).map_err(driver)?; + done.record(stream).map_err(driver) + }; + for (i, &(s, e)) in chunks.iter().enumerate().take(2) { + copy(&mut stages[i], s, e)?; + } + for (i, &(s, e)) in chunks.iter().enumerate() { + let stage = &mut stages[i % 2]; + stage.1.synchronize().map_err(driver)?; + out.extend_from_slice(&stage.0.as_slice().map_err(driver)?[..e - s]); + if let Some(&(s2, e2)) = chunks.get(i + 2) { + copy(stage, s2, e2)?; + } + } + Ok(out) } /// 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 { - 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())) + use crate::quantum_fabric::sv64::{KERNEL_SV_RUN, certify_cases}; + certify_cases(KERNEL_SV_RUN, &self.implementation, &self.member.id, &corpus.generator(), &corpus.cases(), |c| self.run(c.n, &c.gates, c.width), || CudaError::Internal("the corpus holds a gate the state vector refuses")) + } + + /// The certificate of [`KERNEL_SV_RUN_WIDE`](crate::quantum_fabric::sv64::KERNEL_SV_RUN_WIDE) + /// on this device over `corpus`'s wide cases + /// ([`SvCorpus::wide_cases`](crate::quantum_fabric::sv64::SvCorpus::wide_cases)): + /// the kernels of gates on four to six qubits at once. + pub fn certify_wide(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result { + use crate::quantum_fabric::sv64::{KERNEL_SV_RUN_WIDE, certify_cases}; + certify_cases(KERNEL_SV_RUN_WIDE, &self.implementation, &self.member.id, &corpus.wide_generator(), &corpus.wide_cases(), |c| self.run(c.n, &c.gates, c.width), || CudaError::Internal("the corpus holds a gate the state vector refuses")) } /// The member's entry for the state vector, certified in this call. @@ -786,6 +871,386 @@ impl CudaSv { } } +/// Sparse Pauli dynamics on this device ([`crate::quantum_spd::device`]): +/// the kernels of [`quantum_fabric::spd`](crate::quantum_fabric::spd) +/// compiled for it, each run a [`CudaSpdRun`] whose table lives on the +/// device. The term set, its order and the sums are the CPU's, so the result +/// is the CPU's bit for bit, or the certificate says it is not. +#[cfg(feature = "quantum_spd")] +#[derive(Clone)] +pub struct CudaSpd { + member: CudaMember, + /// The kernels of [`spd::KERNELS`](crate::quantum_fabric::spd::KERNELS), in order. + kernels: Vec, + implementation: String, +} + +#[cfg(feature = "quantum_spd")] +impl std::fmt::Debug for CudaSpd { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CudaSpd").field("member", &self.member.id).field("implementation", &self.implementation).finish() + } +} + +#[cfg(feature = "quantum_spd")] +impl CudaMember { + /// The propagation's kernels compiled for this device. + pub fn spd(&self) -> Result { + self.spd_with_source(crate::quantum_fabric::spd::SOURCE) + } + + /// The same, from another kernel text: for the mutation tests. + #[doc(hidden)] + pub fn spd_with_source(&self, source: &str) -> Result { + use crate::quantum_fabric::spd; + let opts = CompileOptions { arch: Some(arch_option(self.identity.arch())), options: spd::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)?; + let kernels = spd::KERNELS.iter().map(|k| module.load_function(k).map_err(driver)).collect::, _>>()?; + Ok(CudaSpd { member: self.clone(), kernels, implementation: spd::implementation_id_of(source) }) + } +} + +#[cfg(feature = "quantum_spd")] +impl CudaSpd { + pub fn implementation(&self) -> &str { + &self.implementation + } + + /// A run: a table of its own on the device, empty until loaded. + pub fn run(&self) -> CudaSpdRun<'_> { + CudaSpdRun { spd: self, t: None } + } + + /// The certificate of [`KERNEL_SPD`](crate::quantum_spd::device::KERNEL_SPD) + /// on this device over `corpus`, against the CPU's propagation. + pub fn certify(&self, corpus: crate::quantum_spd::device::SpdCorpus) -> Result { + use crate::quantum_spd::device::{KERNEL_SPD, OnDevice}; + let mut cert = Certification::new(); + for case in corpus.cases() { + let reference = case.reference().map_err(|_| CudaError::Internal("the corpus holds a case the propagation refuses"))?; + let got = case.run_on(&mut self.run()).map_err(|e| match e { + OnDevice::Device(e) => CudaError::Device(e), + OnDevice::Refused(_) => CudaError::Internal("the corpus holds a case the propagation refuses"), + })?; + cert.add(&case.bytes(), &reference, &got); + } + Ok(cert.finish(KERNEL_SPD, &self.implementation, &self.member.id, &corpus.generator())) + } + + fn function(&self, name: &str) -> &CudaFunction { + let i = crate::quantum_fabric::spd::KERNELS.iter().position(|k| *k == name).expect("a kernel of KERNELS"); + &self.kernels[i] + } +} + +/// A run's table on the device, and the scratch its steps use. +#[cfg(feature = "quantum_spd")] +struct SpdTable { + words: usize, + len: usize, + cap: usize, + keys: CudaSlice, + coefs: CudaSlice, + fresh_keys: CudaSlice, + fresh_vals: CudaSlice, + mark: CudaSlice, + prefix: CudaSlice, + absent: CudaSlice, + vals: CudaSlice, + index: CudaSlice, + counters: CudaSlice, + conj: CudaSlice, +} + +/// One propagation on a [`CudaSpd`]: the protocol's steps +/// ([`SpdDevice`](crate::quantum_spd::device::SpdDevice)) on a table held by +/// the device. +#[cfg(feature = "quantum_spd")] +pub struct CudaSpdRun<'a> { + spd: &'a CudaSpd, + t: Option, +} + +#[cfg(feature = "quantum_spd")] +impl CudaSpdRun<'_> { + fn loaded(&mut self) -> Result<&mut SpdTable, String> { + self.t.as_mut().ok_or_else(|| "no table is loaded".to_string()) + } + + /// Room for `need` terms, the table kept. + fn reserve(&mut self, need: usize) -> Result<(), CudaError> { + let stream = self.spd.member.stream.clone(); + let t = self.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + if need <= t.cap { + return Ok(()); + } + let cap = need.next_power_of_two(); + let w = 2 * t.words; + let mut keys = stream.alloc_zeros::(cap * w).map_err(driver)?; + let mut coefs = stream.alloc_zeros::(cap).map_err(driver)?; + if t.len > 0 { + stream.memcpy_dtod(&t.keys.slice(0..t.len * w), &mut keys.slice_mut(0..t.len * w)).map_err(driver)?; + stream.memcpy_dtod(&t.coefs.slice(0..t.len), &mut coefs.slice_mut(0..t.len)).map_err(driver)?; + } + t.keys = keys; + t.coefs = coefs; + t.fresh_keys = stream.alloc_zeros::(cap * w).map_err(driver)?; + t.fresh_vals = stream.alloc_zeros::(cap).map_err(driver)?; + t.mark = stream.alloc_zeros::(cap).map_err(driver)?; + t.prefix = stream.alloc_zeros::(cap).map_err(driver)?; + t.absent = stream.alloc_zeros::(cap).map_err(driver)?; + t.vals = stream.alloc_zeros::(cap).map_err(driver)?; + t.cap = cap; + Ok(()) + } + + /// The exclusive prefix sum of `marks` (the table's `mark` or `absent`) + /// into `prefix`, and their total: block sums on the device, the blocks' + /// totals added on the host. + fn prefix(&mut self, absent: bool) -> Result { + use crate::quantum_fabric::spd::BLOCK; + let spd = self.spd; + let stream = spd.member.stream.clone(); + let t = self.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let len = t.len as u64; + if len == 0 { + return Ok(0); + } + let blocks = len.div_ceil(u64::from(BLOCK)); + let mut totals = stream.alloc_zeros::(blocks as usize).map_err(driver)?; + { + let marks = if absent { &t.absent } else { &t.mark }; + let mut a = stream.launch_builder(spd.function("spd_prefix")); + a.arg(marks).arg(&len).arg(&mut t.prefix).arg(&mut totals); + let cfg = LaunchConfig { grid_dim: (blocks as u32, 1, 1), block_dim: (BLOCK, 1, 1), shared_mem_bytes: 0 }; + // SAFETY: `marks` and `prefix` hold `len` words, `totals` one a block. + unsafe { a.launch(cfg) }.map_err(driver)?; + } + let sums = stream.clone_dtoh(&totals).map_err(driver)?; + let mut offsets = Vec::with_capacity(sums.len()); + let mut total = 0usize; + for s in &sums { + offsets.push(total as u32); + total += *s as usize; + } + let offsets = stream.clone_htod(&offsets).map_err(driver)?; + let mut a = stream.launch_builder(spd.function("spd_offsets")); + a.arg(&mut t.prefix).arg(&len).arg(&offsets); + // SAFETY: `prefix` holds `len` words, `offsets` one a block. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + Ok(total) + } + + /// The marked values of `vals` (or `coefs`, for none), gathered in table + /// order and brought back. + fn gather(&mut self, n: usize) -> Result, CudaError> { + if n == 0 { + return Ok(Vec::new()); + } + let spd = self.spd; + let stream = spd.member.stream.clone(); + let t = self.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let len = t.len as u64; + let mut out = stream.alloc_zeros::(n).map_err(driver)?; + let mut a = stream.launch_builder(spd.function("spd_gather")); + a.arg(&t.vals).arg(&len).arg(&t.mark).arg(&t.prefix).arg(&mut out); + // SAFETY: each array holds `len` items; `out` holds the `n` marked. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + stream.clone_dtoh(&out).map_err(driver) + } + + fn step(r: Result) -> Result { + r.map_err(|e| e.to_string()) + } +} + +#[cfg(feature = "quantum_spd")] +impl crate::quantum_spd::device::SpdDevice for CudaSpdRun<'_> { + fn load(&mut self, words: usize, keys: &[u64], coefs: &[f64]) -> Result<(), String> { + let stream = self.spd.member.stream.clone(); + let go = || -> Result { + let len = coefs.len(); + let cap = (2 * len).max(1024).next_power_of_two(); + let w = 2 * words; + let mut k = stream.alloc_zeros::(cap * w).map_err(driver)?; + let mut c = stream.alloc_zeros::(cap).map_err(driver)?; + if len > 0 { + stream.memcpy_htod(keys, &mut k.slice_mut(0..len * w)).map_err(driver)?; + stream.memcpy_htod(coefs, &mut c.slice_mut(0..len)).map_err(driver)?; + } + Ok(SpdTable { + words, + len, + cap, + keys: k, + coefs: c, + fresh_keys: stream.alloc_zeros::(cap * w).map_err(driver)?, + fresh_vals: stream.alloc_zeros::(cap).map_err(driver)?, + mark: stream.alloc_zeros::(cap).map_err(driver)?, + prefix: stream.alloc_zeros::(cap).map_err(driver)?, + absent: stream.alloc_zeros::(cap).map_err(driver)?, + vals: stream.alloc_zeros::(cap).map_err(driver)?, + index: stream.alloc_zeros::(2 * cap).map_err(driver)?, + counters: stream.alloc_zeros::(4).map_err(driver)?, + conj: stream.clone_htod(&crate::quantum_spd::device::conj_table()).map_err(driver)?, + }) + }; + self.t = Some(Self::step(go())?); + Ok(()) + } + + fn rotate(&mut self, axis: &[u64], sin: f64, cos: f64) -> Result { + let need = 2 * self.loaded()?.len; + Self::step(self.reserve(need))?; + let spd = self.spd; + let stream = spd.member.stream.clone(); + let go = |run: &mut Self| -> Result { + let t = run.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let (len, words) = (t.len as u64, t.words as u32); + let p = stream.clone_htod(axis).map_err(driver)?; + if len > 0 { + let mut a = stream.launch_builder(spd.function("spd_scan")); + a.arg(&t.keys).arg(&mut t.coefs).arg(&len).arg(&words).arg(&p).arg(&sin).arg(&cos).arg(&mut t.mark).arg(&mut t.fresh_keys).arg(&mut t.fresh_vals); + // SAFETY: the table holds `len` terms of `2·words` words and + // the scratch as many; `p` one string. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + // The index: a power of two at least twice the table. + let icap = (2 * t.len + 2).next_power_of_two().max(16) as u64; + if (t.index.len() as u64) < icap { + t.index = stream.alloc_zeros::(icap as usize).map_err(driver)?; + } + let mut a = stream.launch_builder(spd.function("spd_fill")); + a.arg(&mut t.index).arg(&icap); + // SAFETY: `index` holds at least `icap` words. + unsafe { a.launch(launch(icap)) }.map_err(driver)?; + let mut a = stream.launch_builder(spd.function("spd_index")); + a.arg(&t.keys).arg(&len).arg(&words).arg(&mut t.index).arg(&icap); + // SAFETY: as above; the index has room for every slot twice over. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + stream.memset_zeros(&mut t.counters).map_err(driver)?; + let mut a = stream.launch_builder(spd.function("spd_merge")); + a.arg(&t.keys).arg(&mut t.coefs).arg(&len).arg(&words).arg(&t.index).arg(&icap).arg(&t.mark).arg(&t.fresh_keys).arg(&t.fresh_vals).arg(&mut t.absent).arg(&mut t.counters); + // SAFETY: as above. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + } + let fresh = run.prefix(true)?; + let t = run.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + if fresh > 0 { + let mut a = stream.launch_builder(spd.function("spd_append")); + a.arg(&mut t.keys).arg(&mut t.coefs).arg(&len).arg(&words).arg(&t.absent).arg(&t.prefix).arg(&t.fresh_keys).arg(&t.fresh_vals).arg(&mut t.counters); + // SAFETY: the table has room for `len + fresh` terms, `fresh` + // at most `len`. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + } + let c = if len > 0 { stream.clone_dtoh(&t.counters).map_err(driver)? } else { vec![0; 4] }; + t.len += fresh; + Ok(crate::quantum_spd::device::Rotated { fresh, revived: c[0] as usize, killed: c[1] as usize, fresh_zero: c[2] as usize }) + }; + Self::step(go(self)) + } + + fn truncate(&mut self, threshold: f64, max_weight: Option) -> Result, String> { + let spd = self.spd; + let stream = spd.member.stream.clone(); + let go = |run: &mut Self| -> Result, CudaError> { + let t = run.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let (len, words, cap) = (t.len as u64, t.words as u32, max_weight.unwrap_or(u32::MAX)); + if len == 0 { + return Ok(Vec::new()); + } + let mut a = stream.launch_builder(spd.function("spd_truncate")); + a.arg(&t.keys).arg(&mut t.coefs).arg(&len).arg(&words).arg(&threshold).arg(&cap).arg(&mut t.mark).arg(&mut t.vals); + // SAFETY: the table and the scratch hold `len` terms. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + let n = run.prefix(false)?; + run.gather(n) + }; + Self::step(go(self)) + } + + fn compact(&mut self) -> Result<(), String> { + let spd = self.spd; + let stream = spd.member.stream.clone(); + let go = |run: &mut Self| -> Result<(), CudaError> { + let t = run.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let len = t.len as u64; + if len == 0 { + return Ok(()); + } + let mut a = stream.launch_builder(spd.function("spd_live")); + a.arg(&t.coefs).arg(&len).arg(&mut t.mark); + // SAFETY: as above. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + let n = run.prefix(false)?; + let t = run.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let words = t.words as u32; + let mut a = stream.launch_builder(spd.function("spd_compact")); + a.arg(&t.keys).arg(&t.coefs).arg(&len).arg(&words).arg(&t.mark).arg(&t.prefix).arg(&mut t.fresh_keys).arg(&mut t.fresh_vals); + // SAFETY: the live terms go to the other buffers, which hold as many. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + std::mem::swap(&mut t.keys, &mut t.fresh_keys); + std::mem::swap(&mut t.coefs, &mut t.fresh_vals); + t.len = n; + Ok(()) + }; + Self::step(go(self)) + } + + fn cliffords(&mut self, gates: &[crate::quantum_spd::device::Gate]) -> Result<(), String> { + let spd = self.spd; + let stream = spd.member.stream.clone(); + let go = |run: &mut Self| -> Result<(), CudaError> { + let t = run.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let (len, words) = (t.len as u64, t.words as u32); + if len == 0 || gates.is_empty() { + return Ok(()); + } + let (ops, axes) = crate::quantum_spd::device::encode(gates, t.words); + let (ops, axes, count) = (stream.clone_htod(&ops).map_err(driver)?, stream.clone_htod(&axes).map_err(driver)?, gates.len() as u32); + let mut a = stream.launch_builder(spd.function("spd_cliffords")); + a.arg(&mut t.keys).arg(&mut t.coefs).arg(&len).arg(&words).arg(&ops).arg(&count).arg(&axes).arg(&t.conj); + // SAFETY: four words a gate, each quarter turn's string among the + // axes, the conjugation table's 32 entries. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + Ok(()) + }; + Self::step(go(self)) + } + + fn table(&mut self) -> Result<(Vec, Vec), String> { + let stream = self.spd.member.stream.clone(); + let t = self.loaded()?; + let (n, w) = (t.len, 2 * t.words); + if n == 0 { + return Ok((Vec::new(), Vec::new())); + } + let keys = stream.clone_dtoh(&t.keys.slice(0..n * w)).map_err(|e| driver(e).to_string())?; + let coefs = stream.clone_dtoh(&t.coefs.slice(0..n)).map_err(|e| driver(e).to_string())?; + Ok((keys, coefs)) + } + + fn contributions(&mut self, ones: &[u64]) -> Result, String> { + let spd = self.spd; + let stream = spd.member.stream.clone(); + let go = |run: &mut Self| -> Result, CudaError> { + let t = run.t.as_mut().ok_or(CudaError::Internal("no table is loaded"))?; + let (len, words) = (t.len as u64, t.words as u32); + if len == 0 { + return Ok(Vec::new()); + } + let b = stream.clone_htod(ones).map_err(driver)?; + let mut a = stream.launch_builder(spd.function("spd_contrib")); + a.arg(&t.keys).arg(&t.coefs).arg(&len).arg(&words).arg(&b).arg(&mut t.mark).arg(&mut t.vals); + // SAFETY: as above; `b` one string. + unsafe { a.launch(launch(len)) }.map_err(driver)?; + let n = run.prefix(false)?; + run.gather(n) + }; + Self::step(go(self)) + } +} + /// 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 diff --git a/wai/wai-quantum/src/quantum_fabric_hip.rs b/wai/wai-quantum/src/quantum_fabric_hip.rs index 7e22074e7..55f3beb6f 100644 --- a/wai/wai-quantum/src/quantum_fabric_hip.rs +++ b/wai/wai-quantum/src/quantum_fabric_hip.rs @@ -166,6 +166,7 @@ struct Api { malloc: unsafe extern "C" fn(*mut *mut c_void, usize) -> c_int, free: unsafe extern "C" fn(*mut c_void) -> c_int, memcpy: unsafe extern "C" fn(*mut c_void, *const c_void, usize, c_int) -> c_int, + memset: unsafe extern "C" fn(*mut c_void, c_int, usize) -> c_int, module_load_data: unsafe extern "C" fn(*mut *mut c_void, *const c_void) -> c_int, module_unload: unsafe extern "C" fn(*mut c_void) -> c_int, module_get_function: unsafe extern "C" fn(*mut *mut c_void, *mut c_void, *const c_char) -> c_int, @@ -240,6 +241,7 @@ fn open_api() -> Result { malloc: as_fn(sym(rt, "hipMalloc")?), free: as_fn(sym(rt, "hipFree")?), memcpy: as_fn(sym(rt, "hipMemcpy")?), + memset: as_fn(sym(rt, "hipMemset")?), module_load_data: as_fn(sym(rt, "hipModuleLoadData")?), module_unload: as_fn(sym(rt, "hipModuleUnload")?), module_get_function: as_fn(sym(rt, "hipModuleGetFunction")?), @@ -424,12 +426,19 @@ fn arg(v: &T) -> *mut c_void { /// at most [`hip::GRID_MAX`] blocks, the kernel striding over the rest — and /// waits for it. fn launch(a: &'static Api, function: *mut c_void, work: u64, args: &mut [*mut c_void]) -> Result<(), HipError> { + enqueue(a, function, work, args)?; + // SAFETY: waits for the device's work. + status(a, unsafe { (a.synchronize)() }, "running") +} + +/// [`launch`] without the wait: the launch joins the device's default +/// stream after every launch before it, which run in order. +fn enqueue(a: &'static Api, function: *mut c_void, work: u64, args: &mut [*mut c_void]) -> Result<(), HipError> { let blocks = blocks_for(work); // SAFETY: `args` holds a pointer to each of the kernel's parameters, in // order, each of the type the kernel declares; the caller checks them. - status(a, unsafe { (a.launch)(function, blocks, 1, 1, hip::BLOCK, 1, 1, 0, std::ptr::null_mut(), args.as_mut_ptr(), std::ptr::null_mut()) }, "launching")?; - // SAFETY: waits for the device's work. - status(a, unsafe { (a.synchronize)() }, "running") + // The runtime copies the values before it returns. + status(a, unsafe { (a.launch)(function, blocks, 1, 1, hip::BLOCK, 1, 1, 0, std::ptr::null_mut(), args.as_mut_ptr(), std::ptr::null_mut()) }, "launching") } /// The blocks a launch over `work` items takes. @@ -571,6 +580,7 @@ fn open(a: &'static Api, ordinal: c_int) -> Result { discovered: detail.clone(), detail, max_qubits, + free: free as u64, identity: identity.clone(), }; member.probed().map_err(refuse) @@ -595,6 +605,8 @@ pub struct HipMember { discovered: String, detail: String, max_qubits: u8, + /// The device's free memory at discovery, in bytes. + free: u64, } impl std::fmt::Debug for HipMember { @@ -910,11 +922,15 @@ impl HipMember { /// ([`quantum_fabric::sv64`](crate::quantum_fabric::sv64)): the CUDA kernels' /// text, compiled with contraction off, so each formula is one IEEE binary64 /// operation at a time and the state comes back with the CPU's bits, or the -/// certificate says it did not. +/// certificate says it did not. A run is resident: the state is uploaded (or +/// zeroed on the device) once, the launch table once, every launch goes on +/// the device's default stream with no wait between them, and the state is +/// read back once. #[cfg(feature = "quantum_sv")] #[derive(Clone)] pub struct HipSv { member: HipMember, + /// The kernels of [`sv64::KERNELS`](crate::quantum_fabric::sv64::KERNELS), in order. kernels: Arc, implementation: String, } @@ -937,7 +953,7 @@ impl HipMember { #[doc(hidden)] pub fn sv_with_source(&self, source: &str) -> Result { use crate::quantum_fabric::sv64; - let kernels = compile(self.api, self.ordinal, &self.identity, source, &sv64::HIP_COMPILE_OPTIONS, &["sv_diag", "sv_one", "sv_many"])?; + let kernels = compile(self.api, self.ordinal, &self.identity, source, &sv64::HIP_COMPILE_OPTIONS, &sv64::KERNELS)?; Ok(HipSv { member: self.clone(), kernels: Arc::new(kernels), implementation: sv64::hip_implementation_id_of(source) }) } } @@ -952,7 +968,16 @@ impl HipSv { /// The most qubits a state of this member holds: two f64 arrays of /// `2^n` in half its free memory at discovery. pub fn max_qubits(&self) -> u32 { - u32::from(self.member.max_qubits.saturating_sub(1)) + crate::quantum_fabric::sv64::qubits_in(self.member.free / 2) + } + + fn too_wide(&self, n: u32) -> HipError { + HipError::TooWide { n_qubits: n.min(255) as u8, max: self.max_qubits().min(255) as u8 } + } + + fn function(&self, name: &str) -> *mut c_void { + let i = crate::quantum_fabric::sv64::KERNELS.iter().position(|k| *k == name).expect("a kernel of KERNELS"); + self.kernels.functions[i] } /// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses @@ -960,14 +985,13 @@ impl HipSv { pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec, Vec), HipError> { use crate::quantum_fabric::sv64::plan; let launches = plan(n, gates, width).map_err(|_| HipError::Internal("a gate the state vector refuses"))?; - // Two f64 arrays of 2^n. - if n == 0 || n > self.max_qubits() { - return Err(HipError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) }); - } - let len = 1usize << n; - let mut re = vec![0.0f64; len]; - re[0] = 1.0; - self.run_launches(&re, &vec![0.0f64; len], &launches) + self.run_from_zero(n, &launches) + } + + /// `launches` on `n` qubits from `|0…0⟩`, the state zeroed on the device + /// rather than uploaded. + pub fn run_from_zero(&self, n: u32, launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), HipError> { + self.resident(n, None, launches) } /// `launches` ([`sv64::plan`](crate::quantum_fabric::sv64::plan)'s, or a @@ -975,67 +999,91 @@ impl HipSv { /// unless the state is `2^n` amplitudes this member holds and every launch /// acts within it. pub fn run_launches(&self, re: &[f64], im: &[f64], launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), HipError> { - use crate::quantum_fabric::sv64::{Launch, fits}; let len = re.len(); if !len.is_power_of_two() || len < 2 || im.len() != len { return Err(HipError::Internal("a state that is not 2^n amplitudes, n at least 1")); } - let n = len.trailing_zeros(); - if n > self.max_qubits() { - return Err(HipError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) }); + self.resident(len.trailing_zeros(), Some((re, im)), launches) + } + + /// The resident run: the state from `start` (or `|0…0⟩`), every launch + /// on the device, the state back. + fn resident(&self, n: u32, start: Option<(&[f64], &[f64])>, launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), HipError> { + use crate::quantum_fabric::sv64::{Entry, LaunchTable, fits, many_kernel}; + if n == 0 || n > self.max_qubits() { + return Err(self.too_wide(n)); } if !fits(launches, n) { return Err(HipError::Internal("a launch outside the state")); } + let len = 1usize << n; let a = self.member.api; self.member.select()?; - let re = DeviceBuffer::upload(a, re)?; - let im = DeviceBuffer::upload(a, im)?; - let flat = |m: &[crate::linalg::C]| -> Vec { m.iter().flat_map(|c| [c.re, c.im]).collect() }; - let [diag, one, many] = [0, 1, 2].map(|k| self.kernels.functions[k]); - for l in launches { - match l { - Launch::Diag { bitpos, diag: d } => { - let bp = DeviceBuffer::upload(a, bitpos)?; - let dg = DeviceBuffer::upload(a, &flat(d))?; - let (count, k) = (len as u64, bitpos.len() as u32); + let table = LaunchTable::new(launches); + let f64s = DeviceBuffer::upload(a, &table.f64s)?; + let u32s = DeviceBuffer::upload(a, &table.u32s)?; + let u64s = DeviceBuffer::upload(a, &table.u64s)?; + let (re, im) = match start { + Some((r, i)) => (DeviceBuffer::upload(a, r)?, DeviceBuffer::upload(a, i)?), + None => { + let re = DeviceBuffer::alloc(a, len * 8)?; + let im = DeviceBuffer::alloc(a, len * 8)?; + // SAFETY: each buffer holds `len` doubles; all-zero bytes are 0.0. + unsafe { + status(a, (a.memset)(re.ptr, 0, len * 8), "zeroing the state")?; + status(a, (a.memset)(im.ptr, 0, len * 8), "zeroing the state")?; + } + let one = 1.0f64; + // SAFETY: one double into the first of `len`. + status(a, unsafe { (a.memcpy)(re.ptr, (&one as *const f64).cast(), 8, HIP_MEMCPY_HOST_TO_DEVICE) }, "uploading")?; + (re, im) + } + }; + // A pointer `at` items into a table. + let at = |b: &DeviceBuffer, size: usize, at: usize| -> *mut c_void { b.ptr.cast::().wrapping_add(size * at).cast() }; + for e in &table.entries { + match *e { + Entry::Diag { k, bits, diag } => { + let (bp, dg, count) = (at(&u32s, 4, bits), at(&f64s, 8, diag), len as u64); // `re` and `im` hold `len` doubles, `bp` k positions below // n and `dg` 2·2^k doubles: the kernel's arguments. - launch(a, diag, count, &mut [arg(&re.ptr), arg(&im.ptr), arg(&count), arg(&bp.ptr), arg(&k), arg(&dg.ptr)])?; + enqueue(a, self.function("sv_diag"), count, &mut [arg(&re.ptr), arg(&im.ptr), arg(&count), arg(&bp), arg(&k), arg(&dg)])?; } - Launch::One { s, m } => { + Entry::One { s, m } => { let pairs = (len / 2) as u64; - let v: Vec = flat(m); - let mut args = vec![arg(&re.ptr), arg(&im.ptr), arg(&pairs), arg(s)]; - args.extend(v.iter().map(arg)); + let mut args = vec![arg(&re.ptr), arg(&im.ptr), arg(&pairs), arg(&s)]; + args.extend(m.iter().map(arg)); // As above; `s` is a power of two below 2^n. - launch(a, one, pairs, &mut args)?; + enqueue(a, self.function("sv_one"), pairs, &mut args)?; } - Launch::Many { sorted, offsets, m } => { - let so = DeviceBuffer::upload(a, sorted)?; - let of = DeviceBuffer::upload(a, offsets)?; - let mm = DeviceBuffer::upload(a, &flat(m))?; - let (groups, k) = ((len >> sorted.len()) as u64, sorted.len() as u32); + Entry::Many { k, bits, offsets, m } => { + let (so, of, mm, groups) = (at(&u32s, 4, bits), at(&u64s, 8, offsets), at(&f64s, 8, m), (len >> k) as u64); // As above; 2^k is at most the kernel's SV_MAX_D, which - // `plan` holds. - launch(a, many, groups, &mut [arg(&re.ptr), arg(&im.ptr), arg(&groups), arg(&so.ptr), arg(&k), arg(&of.ptr), arg(&mm.ptr)])?; + // `plan` holds, and the kernel compiled for a group size + // is given that size. + enqueue(a, self.function(many_kernel(k)), groups, &mut [arg(&re.ptr), arg(&im.ptr), arg(&groups), arg(&so), arg(&k), arg(&of), arg(&mm)])?; } } } + // SAFETY: waits for the device's work. + status(a, unsafe { (a.synchronize)() }, "running")?; Ok((re.download(a)?, im.download(a)?)) } /// 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 { - 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(|_| HipError::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())) + use crate::quantum_fabric::sv64::{KERNEL_SV_RUN, certify_cases}; + certify_cases(KERNEL_SV_RUN, &self.implementation, &self.member.id, &corpus.generator(), &corpus.cases(), |c| self.run(c.n, &c.gates, c.width), || HipError::Internal("the corpus holds a gate the state vector refuses")) + } + + /// The certificate of [`KERNEL_SV_RUN_WIDE`](crate::quantum_fabric::sv64::KERNEL_SV_RUN_WIDE) + /// on this device over `corpus`'s wide cases + /// ([`SvCorpus::wide_cases`](crate::quantum_fabric::sv64::SvCorpus::wide_cases)): + /// the kernels of gates on four to six qubits at once. + pub fn certify_wide(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result { + use crate::quantum_fabric::sv64::{KERNEL_SV_RUN_WIDE, certify_cases}; + certify_cases(KERNEL_SV_RUN_WIDE, &self.implementation, &self.member.id, &corpus.wide_generator(), &corpus.wide_cases(), |c| self.run(c.n, &c.gates, c.width), || HipError::Internal("the corpus holds a gate the state vector refuses")) } /// The member's entry for the state vector, certified in this call. diff --git a/wai/wai-quantum/src/quantum_fabric_opencl.rs b/wai/wai-quantum/src/quantum_fabric_opencl.rs index d393947f6..912aa6f46 100644 --- a/wai/wai-quantum/src/quantum_fabric_opencl.rs +++ b/wai/wai-quantum/src/quantum_fabric_opencl.rs @@ -138,6 +138,23 @@ fn cl(e: ClError) -> OpenClError { if OUT_OF_MEMORY.contains(&e.0) { OpenClError::Memory(e.to_string()) } else { OpenClError::Device(e.to_string()) } } +/// `len` doubles of `buf` set to zero by a fill of a 4-byte zero pattern: the +/// same bits as a fill of `0.0`, and one every runtime takes (PoCL's CUDA +/// device takes no pattern of 8 bytes or more, and ends the process on one). +fn zero(q: &CommandQueue, buf: &Buffer, len: usize) -> Result<(), OpenClError> { + use opencl3::memory::ClMem; + let pattern = 0u32; + // SAFETY: the fill covers exactly the buffer's `len` doubles; OpenCL + // copies the pattern before the call returns, and the event it gives back + // is released when its `Event` drops. + let event = unsafe { + opencl3::command_queue::enqueue_fill_buffer(q.get(), buf.get(), (&pattern as *const u32).cast(), size_of::(), 0, len * 8, 0, std::ptr::null()) + } + .map_err(|e| cl(ClError(e)))?; + drop(opencl3::event::Event::new(event)); + Ok(()) +} + /// A device that is not a member, and why. #[derive(Debug)] pub struct Refusal { @@ -289,6 +306,7 @@ fn open(platform: &PlatformInfo, position: usize, id: cl_device_id) -> Result usize { work.clamp(1, opencl::GLOBAL_MAX) as usize } +/// `launch` over `work` items: the global size [`global`] gives, and where a +/// runtime is held to a work-group size (`local`), that size, the global size +/// rounded up to a multiple of it (the kernels stride, so the items past the +/// work do nothing). +#[cfg(feature = "quantum_sv")] +fn shape<'a, 'b>(launch: &'a mut ExecuteKernel<'b>, work: u64, local: Option) -> &'a mut ExecuteKernel<'b> { + let g = global(work); + match local { + Some(l) => launch.set_global_work_size(g.div_ceil(l) * l).set_local_work_size(l), + None => launch.set_global_work_size(g), + } +} + /// 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. @@ -347,6 +378,8 @@ pub struct OpenClMember { implementation: String, detail: String, max_qubits: u8, + /// The largest single allocation the device takes, in bytes. + max_alloc: u64, /// Whether the device has binary64 (`cl_khr_fp64`). binary64: bool, } @@ -671,9 +704,11 @@ pub struct OpenClSv { #[cfg(feature = "quantum_sv")] struct SvKernels { - diag: ClKernel, - one: ClKernel, - many: ClKernel, + /// The kernels of [`sv64::KERNELS`](crate::quantum_fabric::sv64::KERNELS), in order. + kernels: Vec, + /// The work-group size each is launched with, where the runtime is not left + /// to choose it. + local: Vec>, } // SAFETY: as for [`OpenClMember`]: the kernels' arguments are set and enqueued @@ -707,11 +742,19 @@ impl OpenClMember { 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)?, + let kernels = sv64::KERNELS.iter().map(|k| ClKernel::create(&program, k).map_err(cl)).collect::, _>>()?; + // PoCL's CUDA device chooses a work-group size its CUDA driver cannot + // launch the wider kernels at (CUDA_ERROR_LAUNCH_OUT_OF_RESOURCES, and + // the runtime ends the process), so there each launch names one: the + // largest power of two the kernel takes on the device, at most 128. + // Every other runtime chooses its own. + let local = if self.identity.platform.contains("Portable Computing Language") && self.identity.kind == DeviceKind::Gpu { + let device = *self.context.devices().first().ok_or(OpenClError::Internal("the context holds no device"))?; + kernels.iter().map(|k| k.get_work_group_size(device).map(|w| Some(1usize << w.clamp(1, 128).ilog2())).map_err(cl)).collect::, _>>()? + } else { + vec![None; kernels.len()] }; + let kernels = SvKernels { kernels, local }; Ok(OpenClSv { member: self.clone(), kernels: Arc::new(Mutex::new(kernels)), implementation: sv64::cl_implementation_id_of(source) }) } } @@ -725,7 +768,13 @@ impl OpenClSv { /// The most qubits a state of this member holds: two f64 arrays of /// `2^n`, each within one allocation and together within half its memory. pub fn max_qubits(&self) -> u32 { - u32::from(self.member.max_qubits.saturating_sub(1)) + let one = self.member.max_alloc / 8; + let one = if one < 2 { 0 } else { one.ilog2() }; + crate::quantum_fabric::sv64::qubits_in(self.member.identity.memory / 2).min(one) + } + + fn too_wide(&self, n: u32) -> OpenClError { + OpenClError::TooWide { n_qubits: n.min(255) as u8, max: self.max_qubits().min(255) as u8 } } /// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses @@ -733,14 +782,13 @@ impl OpenClSv { pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec, Vec), OpenClError> { use crate::quantum_fabric::sv64::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 re = vec![0.0f64; len]; - re[0] = 1.0; - self.run_launches(&re, &vec![0.0f64; len], &launches) + self.run_from_zero(n, &launches) + } + + /// `launches` on `n` qubits from `|0…0⟩`, the state filled with zeros on + /// the device rather than uploaded. + pub fn run_from_zero(&self, n: u32, launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), OpenClError> { + self.resident(n, None, launches) } /// `launches` ([`sv64::plan`](crate::quantum_fabric::sv64::plan)'s, or a @@ -748,57 +796,80 @@ impl OpenClSv { /// unless the state is `2^n` amplitudes this member holds and every launch /// acts within it. pub fn run_launches(&self, re: &[f64], im: &[f64], launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), OpenClError> { - use crate::quantum_fabric::sv64::{Launch, fits}; let len = re.len(); if !len.is_power_of_two() || len < 2 || im.len() != len { return Err(OpenClError::Internal("a state that is not 2^n amplitudes, n at least 1")); } - let n = len.trailing_zeros(); - let max = self.member.max_qubits.saturating_sub(1); - if n > u32::from(max) { - return Err(OpenClError::TooWide { n_qubits: n.min(255) as u8, max }); + self.resident(len.trailing_zeros(), Some((re, im)), launches) + } + + /// The resident run: the state from `start` (or `|0…0⟩`), every launch + /// enqueued on the member's in-order queue, the state back. + fn resident(&self, n: u32, start: Option<(&[f64], &[f64])>, launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec, Vec), OpenClError> { + use crate::quantum_fabric::sv64::{Entry, KERNELS, LaunchTable, fits, many_kernel}; + if n == 0 || n > self.max_qubits() { + return Err(self.too_wide(n)); } if !fits(launches, n) { return Err(OpenClError::Internal("a launch outside the state")); } - let re = self.member.upload(re)?; - let im = self.member.upload(im)?; - let flat = |m: &[crate::linalg::C]| -> Vec { m.iter().flat_map(|c| [c.re, c.im]).collect() }; + let len = 1usize << n; + let table = LaunchTable::new(launches); + let f64s = self.member.upload(&table.f64s)?; + let u32s = self.member.upload(&table.u32s)?; + let u64s = self.member.upload(&table.u64s)?; + let (re, im) = match start { + Some((r, i)) => (self.member.upload(r)?, self.member.upload(i)?), + None => { + let q = &self.member.queue; + // SAFETY: no host pointer is given; each buffer is `len` items. + let mut re = unsafe { Buffer::::create(&self.member.context, CL_MEM_READ_WRITE, len, std::ptr::null_mut()) }.map_err(cl)?; + // SAFETY: as above. + let im = unsafe { Buffer::::create(&self.member.context, CL_MEM_READ_WRITE, len, std::ptr::null_mut()) }.map_err(cl)?; + zero(q, &re, len)?; + zero(q, &im, len)?; + // SAFETY: the queue is in order, so the write of the first + // amplitude comes after the fills, and it is blocking. + unsafe { q.enqueue_write_buffer(&mut re, CL_BLOCKING, 0, &[1.0f64], &[]) }.map_err(cl)?; + (re, im) + } + }; { 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)?; + let at = |name: &str| KERNELS.iter().position(|x| *x == name).expect("a kernel of KERNELS"); + for e in &table.entries { + match *e { + Entry::Diag { k: nk, bits, diag } => { + let (count, bits, diag): (cl_ulong, cl_ulong, cl_ulong) = (len as u64, bits as u64, diag as u64); + // SAFETY: `re`, `im` hold `len` doubles; the table holds + // the k bit positions below n at `bits` and the 2·2^k + // doubles at `diag`: the kernel's arguments, in order. + let i = at("sv_diag"); + // SAFETY: as above. + unsafe { shape(ExecuteKernel::new(&k.kernels[i]).set_arg(&re).set_arg(&im).set_arg(&count).set_arg(&u32s).set_arg(&bits).set_arg(&nk).set_arg(&f64s).set_arg(&diag), count, k.local[i]).enqueue_nd_range(&self.member.queue) }.map_err(cl)?; } - Launch::One { s, m } => { + Entry::One { s, m } => { let pairs = (len / 2) as cl_ulong; - let v = flat(m); - let mut launch = ExecuteKernel::new(&k.one); + let i = at("sv_one"); + let mut launch = ExecuteKernel::new(&k.kernels[i]); // 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(&re).set_arg(&im).set_arg(&pairs).set_arg(&s); + for x in &m { launch.set_arg(x); } - launch.set_global_work_size(global(pairs)).enqueue_nd_range(&self.member.queue) + shape(&mut launch, pairs, k.local[i]).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)?; + Entry::Many { k: nk, bits, offsets, m } => { + let (groups, bits, offsets, m): (cl_ulong, cl_ulong, cl_ulong, cl_ulong) = ((len >> nk) as u64, bits as u64, offsets as u64, m as u64); + // SAFETY: as above; 2^k is at most the kernel's + // SV_MAX_D, which `plan` holds, and the kernel built + // for a group size is given that size. + let i = at(many_kernel(nk)); + // SAFETY: as above. + unsafe { shape(ExecuteKernel::new(&k.kernels[i]).set_arg(&re).set_arg(&im).set_arg(&groups).set_arg(&u32s).set_arg(&bits).set_arg(&nk).set_arg(&u64s).set_arg(&offsets).set_arg(&f64s).set_arg(&m), groups, k.local[i]).enqueue_nd_range(&self.member.queue) }.map_err(cl)?; } } } @@ -809,14 +880,17 @@ impl OpenClSv { /// 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 { - 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())) + use crate::quantum_fabric::sv64::{KERNEL_SV_RUN, certify_cases}; + certify_cases(KERNEL_SV_RUN, &self.implementation, &self.member.id, &corpus.generator(), &corpus.cases(), |c| self.run(c.n, &c.gates, c.width), || OpenClError::Internal("the corpus holds a gate the state vector refuses")) + } + + /// The certificate of [`KERNEL_SV_RUN_WIDE`](crate::quantum_fabric::sv64::KERNEL_SV_RUN_WIDE) + /// on this device over `corpus`'s wide cases + /// ([`SvCorpus::wide_cases`](crate::quantum_fabric::sv64::SvCorpus::wide_cases)): + /// the kernels of gates on four to six qubits at once. + pub fn certify_wide(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result { + use crate::quantum_fabric::sv64::{KERNEL_SV_RUN_WIDE, certify_cases}; + certify_cases(KERNEL_SV_RUN_WIDE, &self.implementation, &self.member.id, &corpus.wide_generator(), &corpus.wide_cases(), |c| self.run(c.n, &c.gates, c.width), || OpenClError::Internal("the corpus holds a gate the state vector refuses")) } /// The member's entry for the state vector, certified in this call. diff --git a/wai/wai-quantum/src/quantum_spd.rs b/wai/wai-quantum/src/quantum_spd.rs index 14b3165e0..9475b06ca 100644 --- a/wai/wai-quantum/src/quantum_spd.rs +++ b/wai/wai-quantum/src/quantum_spd.rs @@ -44,6 +44,10 @@ use crate::pool::Pool; pub use crate::repro::sin_cos; +/// The propagation with its terms on a device, with the CPU's bits +/// ([`device::expectation_on`]). +pub mod device; + // --------------------------------------------------------------------------- // Circuits // --------------------------------------------------------------------------- @@ -869,10 +873,41 @@ fn run( threads: usize, max_terms: usize, ) -> (SpdResult, f64) { - crate::pool::scoped(threads, |pool| propagate::(circuit, observable, ones, cfg, pool, max_terms)) + crate::pool::scoped(threads, |pool| { + let (r, t, _) = propagate::(circuit, observable, ones, cfg, pool, max_terms); + (r, t) + }) } -fn propagate(circuit: &RotCircuit, observable: &[(f64, PauliString)], ones: &[u32], cfg: &SpdConfig, pool: &Pool, max_terms: usize) -> (SpdResult, f64) { +/// [`expectation_capped`] on one thread, with the table it ended with: every +/// slot's string (in [`device::key_of`]'s layout) and coefficient, in order, +/// dead slots included. What a device's certificate is held to. +#[cfg(any(test, feature = "quantum_fabric"))] +pub(crate) fn expectation_table(circuit: &RotCircuit, observable: &[(f64, PauliString)], ones: &[u32], cfg: &SpdConfig, max_terms: usize) -> Result<(SpdResult, f64, Vec, Vec), SpdError> { + circuit.validate()?; + for (_, s) in observable { + check_string(s, circuit.n)?; + } + if let Some(&q) = ones.iter().find(|&&q| q >= circuit.n) { + return Err(SpdError::QubitOutOfRange { qubit: q, n: circuit.n }); + } + fn table(circuit: &RotCircuit, observable: &[(f64, PauliString)], ones: &[u32], cfg: &SpdConfig, max_terms: usize) -> (SpdResult, f64, Vec, Vec) { + crate::pool::scoped(1, |pool| { + let (r, threshold, t) = propagate::(circuit, observable, ones, cfg, pool, max_terms); + let keys = t.keys.iter().flat_map(|k| k.x.iter().chain(k.z.iter()).copied()).collect(); + (r, threshold, keys, t.coef) + }) + } + Ok(match circuit.n.div_ceil(64) { + 1 => table::<1>(circuit, observable, ones, cfg, max_terms), + 2 => table::<2>(circuit, observable, ones, cfg, max_terms), + 3 | 4 => table::<4>(circuit, observable, ones, cfg, max_terms), + 5..=8 => table::<8>(circuit, observable, ones, cfg, max_terms), + _ => table::<16>(circuit, observable, ones, cfg, max_terms), + }) +} + +fn propagate(circuit: &RotCircuit, observable: &[(f64, PauliString)], ones: &[u32], cfg: &SpdConfig, pool: &Pool, max_terms: usize) -> (SpdResult, f64, Terms) { let mut t = Terms::::new(); for (c, s) in observable { t.add(Key::from_string(s), *c, pool); @@ -920,7 +955,8 @@ fn propagate(circuit: &RotCircuit, observable: &[(f64, PauliStri value += if flips & 1 == 1 { -t.coef[i] } else { t.coef[i] }; } } - (SpdResult { value, truncation_bound: bound, peak_terms: peak, final_terms: t.live(), term_updates: work }, threshold) + let live = t.live(); + (SpdResult { value, truncation_bound: bound, peak_terms: peak, final_terms: live, term_updates: work }, threshold, t) } // --------------------------------------------------------------------------- diff --git a/wai/wai-quantum/src/quantum_spd/device.rs b/wai/wai-quantum/src/quantum_spd/device.rs new file mode 100644 index 000000000..b04f88acf --- /dev/null +++ b/wai/wai-quantum/src/quantum_spd/device.rs @@ -0,0 +1,893 @@ +//! Sparse Pauli dynamics on a device, with the CPU's bits. +//! +//! [`expectation_on`] is [`super::expectation_capped`] with the sum of terms +//! held by an [`SpdDevice`]: the device runs each step's arithmetic on every +//! term at once, and the host keeps everything that decides which terms there +//! are, in the order the CPU decides it. +//! +//! - **The term set and its order.** Terms live in insertion order on the +//! device too. A rotation's new terms are found in the table (every slot, +//! dead ones included, as the CPU's index finds them), added where they are +//! found, and the rest appended in the order of the terms they came from: +//! an order-preserving compaction, so the appended terms are the CPU's, in +//! the CPU's order. A truncation zeroes the terms the CPU drops; a +//! compaction keeps the live terms in order. +//! - **The CPU's schedule.** The host counts dead and live terms from what +//! each step reports and compacts exactly when the CPU does (dead slots +//! outnumbering live ones after a truncation, and before a run of Clifford +//! gates), so the table, index for index, is the CPU's after every step. +//! - **Sums on the host, in table order.** A truncation hands back the +//! magnitudes it dropped in table order, and the host adds them as the CPU +//! does; the value is added up on the host from each contributing term's +//! signed coefficient, in table order. +//! - **The arithmetic per term** is one multiply by `sin`, one by `cos`, one +//! addition where a new term meets an old one, and sign changes: each one +//! IEEE binary64 operation, which a device reproduces when it does not fuse +//! a multiply into an add. +//! +//! [`HostDevice`] runs the protocol on the CPU, one term after another; the +//! tests hold it to [`super::expectation_capped`] bit for bit, so the +//! protocol itself is the CPU's computation. A device member is certified on +//! [`SpdCorpus`] against the CPU, and is placed only bit-exact. + +use super::{Op, Pauli, RotCircuit, SpdConfig, SpdError, SpdResult, check_string, conj1, sin_cos}; +use std::collections::HashMap; + +/// The kernel this module certifies: an observable propagated through a +/// rotation circuit, truncated. +pub const KERNEL_SPD: &str = "wai.quantum.spd/propagate"; + +/// Words of a string's x part (and of its z part) for `n` qubits: the CPU's +/// own word count, 1, 2, 4, 8 or 16. +pub fn words_for(n: u32) -> usize { + match n.div_ceil(64) { + 0 | 1 => 1, + 2 => 2, + 3 | 4 => 4, + 5..=8 => 8, + _ => 16, + } +} + +/// A string in a device's layout: `words` words of its x bits, then `words` +/// of its z bits, qubit `q` at bit `q % 64` of word `q / 64`. +pub fn key_of(s: &[(u32, Pauli)], words: usize) -> Vec { + let mut k = vec![0u64; 2 * words]; + for &(q, p) in s { + let (w, b) = ((q / 64) as usize, 1u64 << (q % 64)); + match p { + Pauli::X => k[w] |= b, + Pauli::Z => k[words + w] |= b, + Pauli::Y => { + k[w] |= b; + k[words + w] |= b; + } + } + } + k +} + +/// Whether two strings commute. +pub fn commutes(a: &[u64], b: &[u64], words: usize) -> bool { + let mut p = 0u32; + for w in 0..words { + p += (a[w] & b[words + w]).count_ones() + (a[words + w] & b[w]).count_ones(); + } + p & 1 == 0 +} + +/// `a · b = i^e · (a ⊕ b)`: the product string and `e` mod 4. +pub fn mul(a: &[u64], b: &[u64], words: usize) -> (Vec, u32) { + let mut out = vec![0u64; 2 * words]; + let mut e = 0u32; + for w in 0..words { + let (x, z) = (a[w] ^ b[w], a[words + w] ^ b[words + w]); + out[w] = x; + out[words + w] = z; + e = e.wrapping_add((a[w] & a[words + w]).count_ones()); + e = e.wrapping_add((b[w] & b[words + w]).count_ones()); + e = e.wrapping_add(2 * (a[words + w] & b[w]).count_ones()); + e = e.wrapping_sub((x & z).count_ones()); + } + (out, e & 3) +} + +/// The qubits a string acts on. +pub fn weight(k: &[u64], words: usize) -> u32 { + (0..words).map(|w| (k[w] | k[words + w]).count_ones()).sum() +} + +fn get(k: &[u64], words: usize, q: u32) -> (bool, bool) { + let (w, b) = ((q / 64) as usize, q % 64); + ((k[w] >> b) & 1 == 1, (k[words + w] >> b) & 1 == 1) +} + +fn set(k: &mut [u64], words: usize, q: u32, x: bool, z: bool) { + let (w, b) = ((q / 64) as usize, 1u64 << (q % 64)); + k[w] = (k[w] & !b) | if x { b } else { 0 }; + k[words + w] = (k[words + w] & !b) | if z { b } else { 0 }; +} + +/// A gate that maps each string to one signed string, as a device applies +/// it: a fixed Clifford gate, or a quarter turn about a string (in the +/// device's layout). +#[derive(Clone, Debug, PartialEq)] +pub enum Gate { + Fixed(Op), + Quarter(Vec, u8), +} + +/// One term through one gate, exactly as the CPU conjugates it. +pub fn apply(g: &Gate, k: &mut [u64], c: &mut f64, words: usize) { + let neg = match g { + Gate::Fixed(op) => match *op { + Op::H(q) | Op::S(q) | Op::Sdg(q) | Op::SX(q) | Op::SXdg(q) | Op::X(q) | Op::Y(q) | Op::Z(q) => { + let (x, z) = get(k, words, q); + let (nx, nz, neg) = conj1(op, x, z); + set(k, words, q, nx, nz); + neg + } + Op::CX(cq, tq) => { + let ((xc, zc), (xt, zt)) = (get(k, words, cq), get(k, words, tq)); + set(k, words, tq, xt ^ xc, zt); + set(k, words, cq, xc, zc ^ zt); + xc && zt && !(xt ^ zc) + } + Op::CZ(a, b) => { + let ((xa, za), (xb, zb)) = (get(k, words, a), get(k, words, b)); + set(k, words, a, xa, za ^ xb); + set(k, words, b, xb, zb ^ xa); + xa && xb && (za ^ zb) + } + Op::Swap(a, b) => { + let (fa, fb) = (get(k, words, a), get(k, words, b)); + set(k, words, a, fb.0, fb.1); + set(k, words, b, fa.0, fa.1); + false + } + Op::Rot { .. } | Op::QuarterRot { .. } => unreachable!("not a fixed Clifford"), + }, + Gate::Quarter(p, turns) => { + if commutes(k, p, words) { + return; + } + if *turns == 2 { + true + } else { + let (r, e) = mul(k, p, words); + k.copy_from_slice(&r); + let neg = (e + 3) & 3 == 2; + if *turns == 3 { !neg } else { neg } + } + } + }; + if neg { + *c = -*c; + } +} + +/// The conjugation of one factor by each single-qubit Clifford gate, as the +/// CPU does it, for a device: entry `4·g + 2·x + z`, for gate `g` (0 to 7: +/// H, S, S†, √X, √X†, X, Y, Z) and factor `X^x Z^z`, holds the new `x` in bit +/// 2, the new `z` in bit 1 and whether the sign flips in bit 0. +pub fn conj_table() -> [u8; 32] { + let gates = [Op::H(0), Op::S(0), Op::Sdg(0), Op::SX(0), Op::SXdg(0), Op::X(0), Op::Y(0), Op::Z(0)]; + let mut t = [0u8; 32]; + for (g, op) in gates.iter().enumerate() { + for (x, z) in [(false, false), (false, true), (true, false), (true, true)] { + let (nx, nz, neg) = conj1(op, x, z); + t[4 * g + 2 * usize::from(x) + usize::from(z)] = (u8::from(nx) << 2) | (u8::from(nz) << 1) | u8::from(neg); + } + } + t +} + +/// The gate code of a fixed Clifford gate in a device's encoding +/// ([`encode`]). +fn code(op: &Op) -> (u32, u32, u32) { + match *op { + Op::H(q) => (0, q, 0), + Op::S(q) => (1, q, 0), + Op::Sdg(q) => (2, q, 0), + Op::SX(q) => (3, q, 0), + Op::SXdg(q) => (4, q, 0), + Op::X(q) => (5, q, 0), + Op::Y(q) => (6, q, 0), + Op::Z(q) => (7, q, 0), + Op::CX(a, b) => (8, a, b), + Op::CZ(a, b) => (9, a, b), + Op::Swap(a, b) => (10, a, b), + Op::Rot { .. } | Op::QuarterRot { .. } => unreachable!("not a fixed Clifford"), + } +} + +/// Gates as a device takes them: four words a gate (code, a, b, 0), codes 0 +/// to 7 one factor on qubit `a` ([`conj_table`]), 8 CX(a, b), 9 CZ(a, b), 10 +/// swap(a, b), 11 a quarter turn of `a` quarters about the `b`-th string of +/// the axes returned with them (each `2·words` words). Each array holds at +/// least one element. +pub fn encode(gates: &[Gate], words: usize) -> (Vec, Vec) { + let (mut ops, mut axes) = (Vec::with_capacity(4 * gates.len()), Vec::new()); + for g in gates { + match g { + Gate::Fixed(op) => { + let (c, a, b) = code(op); + ops.extend_from_slice(&[c, a, b, 0]); + } + Gate::Quarter(p, k) => { + ops.extend_from_slice(&[11, u32::from(*k), (axes.len() / (2 * words)) as u32, 0]); + axes.extend_from_slice(p); + } + } + } + if ops.is_empty() { + ops.push(0); + } + if axes.is_empty() { + axes.push(0); + } + (ops, axes) +} + +/// What a rotation did to the table: terms appended, dead terms revived and +/// live ones killed by an addition, and appended terms whose coefficient is +/// zero. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Rotated { + pub fresh: usize, + pub revived: usize, + pub killed: usize, + pub fresh_zero: usize, +} + +/// A sum of Pauli terms held by a device, and the steps propagation takes on +/// it ([`expectation_on`]). Each step acts on every term the table holds, +/// dead ones included, as the CPU's does. +pub trait SpdDevice { + /// The table: `keys` (`2·words` words a term, [`key_of`]'s layout) and + /// `coefs`, in order. + fn load(&mut self, words: usize, keys: &[u64], coefs: &[f64]) -> Result<(), String>; + /// Conjugation by `exp(−iθP/2)`, `P` = `axis`: every live term that + /// anticommutes with `P` scaled by `cos`, its product with `P` (times + /// `±sin`) added where that string is in the table and appended, in the + /// order of the terms they came from, where it is not. + fn rotate(&mut self, axis: &[u64], sin: f64, cos: f64) -> Result; + /// Every live term with `|c| < threshold`, or heavier than `max_weight`, + /// zeroed; their magnitudes, in table order. + fn truncate(&mut self, threshold: f64, max_weight: Option) -> Result, String>; + /// The dead terms removed, the rest kept in order. + fn compact(&mut self) -> Result<(), String>; + /// Every term through each gate in turn. + fn cliffords(&mut self, gates: &[Gate]) -> Result<(), String>; + /// The signed coefficient of each live term with no X or Y factor, + /// negated where it has an odd number of Z factors on `ones` (a string of + /// Z on the qubits set), in table order. + fn contributions(&mut self, ones: &[u64]) -> Result, String>; + /// The table as it stands: every slot's string and coefficient, in + /// order, dead slots included (for a certificate). + fn table(&mut self) -> Result<(Vec, Vec), String>; +} + +/// Why a run on a device has no result. +#[derive(Clone, Debug, PartialEq)] +pub enum OnDevice { + /// What [`super::expectation_capped`] refuses. + Refused(SpdError), + /// The device failed, and how. + Device(String), +} + +impl std::fmt::Display for OnDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OnDevice::Refused(e) => write!(f, "{e}"), + OnDevice::Device(e) => write!(f, "device: {e}"), + } + } +} + +impl From for OnDevice { + fn from(e: String) -> OnDevice { + OnDevice::Device(e) + } +} + +/// The host's count of a device's table, kept as the CPU keeps its own. +struct Count { + len: usize, + dead: usize, +} + +impl Count { + fn live(&self) -> usize { + self.len - self.dead + } + + /// The CPU's truncation: the dropped magnitudes added in table order, + /// and a compaction when dead slots outnumber live ones. + fn truncate(&mut self, dev: &mut dyn SpdDevice, threshold: f64, max_weight: Option) -> Result { + let dropped = dev.truncate(threshold, max_weight)?; + let mut sum = 0.0; + for v in &dropped { + sum += v; + } + self.dead += dropped.len(); + if 2 * self.dead > self.len { + self.compact(dev)?; + } + Ok(sum) + } + + fn compact(&mut self, dev: &mut dyn SpdDevice) -> Result<(), String> { + dev.compact()?; + self.len -= self.dead; + self.dead = 0; + Ok(()) + } + + /// A run of Clifford gates: compacted first when any slot is dead, as + /// the CPU does before it rewrites every key. + fn cliffords(&mut self, dev: &mut dyn SpdDevice, gates: &[Gate]) -> Result<(), String> { + if self.dead > 0 { + self.compact(dev)?; + } + dev.cliffords(gates) + } +} + +/// The table the CPU starts from: each string added in order, the +/// coefficients of a repeated string summed in order, and zeros dead. +fn initial(observable: &[(f64, Vec<(u32, Pauli)>)], words: usize) -> (Vec, Vec, usize) { + let (mut keys, mut coefs, mut dead) = (Vec::new(), Vec::new(), 0usize); + let mut index: HashMap, usize> = HashMap::new(); + for (c, s) in observable { + let k = key_of(s, words); + match index.get(&k) { + Some(&i) => { + let before = coefs[i]; + let after = before + *c; + coefs[i] = after; + match (before == 0.0, after == 0.0) { + (true, false) => dead -= 1, + (false, true) => dead += 1, + _ => {} + } + } + None => { + index.insert(k.clone(), coefs.len()); + keys.extend_from_slice(&k); + coefs.push(*c); + if *c == 0.0 { + dead += 1; + } + } + } + } + (keys, coefs, dead) +} + +/// [`super::expectation_capped`] with the terms on `dev`: the same result, +/// bit for bit, when every step the device runs is the CPU's step (which +/// [`HostDevice`] is, and a certified member's certificate says). Returns the +/// result and the threshold the run ended at. +pub fn expectation_on(dev: &mut dyn SpdDevice, circuit: &RotCircuit, observable: &[(f64, Vec<(u32, Pauli)>)], ones: &[u32], cfg: &SpdConfig, max_terms: usize) -> Result<(SpdResult, f64), OnDevice> { + circuit.validate().map_err(OnDevice::Refused)?; + for (_, s) in observable { + check_string(s, circuit.n).map_err(OnDevice::Refused)?; + } + if let Some(&q) = ones.iter().find(|&&q| q >= circuit.n) { + return Err(OnDevice::Refused(SpdError::QubitOutOfRange { qubit: q, n: circuit.n })); + } + let words = words_for(circuit.n); + let (keys, coefs, dead) = initial(observable, words); + dev.load(words, &keys, &coefs)?; + let mut t = Count { len: coefs.len(), dead }; + let mut bound = t.truncate(dev, 0.0, None)?; + let mut peak = t.live(); + let mut work = 0u64; + let mut batch: Vec = Vec::new(); + let mut threshold = cfg.threshold; + for op in circuit.ops.iter().rev() { + work += t.live() as u64; + match op { + Op::Rot { axis, theta } => { + if !batch.is_empty() { + t.cliffords(dev, &batch)?; + batch.clear(); + } + let (s, c) = sin_cos(*theta); + let r = dev.rotate(&key_of(axis, words), s, c)?; + t.dead = t.dead + r.killed - r.revived + r.fresh_zero; + t.len += r.fresh; + bound += t.truncate(dev, threshold, cfg.max_weight)?; + while t.live() > max_terms { + threshold = if threshold > 0.0 { threshold * 2.0 } else { f64::MIN_POSITIVE }; + bound += t.truncate(dev, threshold, cfg.max_weight)?; + } + } + Op::QuarterRot { axis, k } => batch.push(Gate::Quarter(key_of(axis, words), *k)), + other => batch.push(Gate::Fixed(other.clone())), + } + peak = peak.max(t.live()); + } + if !batch.is_empty() { + t.cliffords(dev, &batch)?; + } + let b = key_of(&ones.iter().map(|&q| (q, Pauli::Z)).collect::>(), words); + let mut value = 0.0; + for v in dev.contributions(&b)? { + value += v; + } + Ok((SpdResult { value, truncation_bound: bound, peak_terms: peak, final_terms: t.live(), term_updates: work }, threshold)) +} + +/// The protocol on the CPU, one term after another: what a device's steps +/// must equal. +#[derive(Clone, Debug, Default)] +pub struct HostDevice { + words: usize, + keys: Vec, + coefs: Vec, +} + +impl HostDevice { + fn key(&self, i: usize) -> &[u64] { + &self.keys[2 * self.words * i..2 * self.words * (i + 1)] + } +} + +impl SpdDevice for HostDevice { + fn load(&mut self, words: usize, keys: &[u64], coefs: &[f64]) -> Result<(), String> { + *self = HostDevice { words, keys: keys.to_vec(), coefs: coefs.to_vec() }; + Ok(()) + } + + fn rotate(&mut self, axis: &[u64], sin: f64, cos: f64) -> Result { + let w = self.words; + let len = self.coefs.len(); + let mut fresh: Vec<(Vec, f64)> = Vec::new(); + for i in 0..len { + let c = self.coefs[i]; + if c == 0.0 || commutes(self.key(i), axis, w) { + continue; + } + let (r, e) = mul(self.key(i), axis, w); + let v = c * sin; + fresh.push((r, if (e + 3) & 3 == 2 { -v } else { v })); + self.coefs[i] = c * cos; + } + let index: HashMap<&[u64], usize> = (0..len).map(|i| (&self.keys[2 * w * i..2 * w * (i + 1)], i)).collect(); + let (mut out, mut appended) = (Rotated::default(), Vec::new()); + let found: Vec> = fresh.iter().map(|(r, _)| index.get(r.as_slice()).copied()).collect(); + for ((r, v), at) in fresh.into_iter().zip(found) { + match at { + Some(j) => { + let before = self.coefs[j]; + let after = before + v; + self.coefs[j] = after; + match (before == 0.0, after == 0.0) { + (true, false) => out.revived += 1, + (false, true) => out.killed += 1, + _ => {} + } + } + None => appended.push((r, v)), + } + } + for (r, v) in appended { + self.keys.extend_from_slice(&r); + self.coefs.push(v); + out.fresh += 1; + if v == 0.0 { + out.fresh_zero += 1; + } + } + Ok(out) + } + + fn truncate(&mut self, threshold: f64, max_weight: Option) -> Result, String> { + let w = self.words; + let mut out = Vec::new(); + for i in 0..self.coefs.len() { + let c = self.coefs[i]; + if c == 0.0 { + continue; + } + if c.abs() < threshold || max_weight.is_some_and(|m| weight(self.key(i), w) > m) { + out.push(c.abs()); + self.coefs[i] = 0.0; + } + } + Ok(out) + } + + fn compact(&mut self) -> Result<(), String> { + let w = 2 * self.words; + let mut keep = 0; + for i in 0..self.coefs.len() { + if self.coefs[i] != 0.0 { + self.keys.copy_within(w * i..w * (i + 1), w * keep); + self.coefs[keep] = self.coefs[i]; + keep += 1; + } + } + self.keys.truncate(w * keep); + self.coefs.truncate(keep); + Ok(()) + } + + fn cliffords(&mut self, gates: &[Gate]) -> Result<(), String> { + let w = self.words; + for (k, c) in self.keys.chunks_mut(2 * w).zip(self.coefs.iter_mut()) { + for g in gates { + apply(g, k, c, w); + } + } + Ok(()) + } + + fn table(&mut self) -> Result<(Vec, Vec), String> { + Ok((self.keys.clone(), self.coefs.clone())) + } + + fn contributions(&mut self, ones: &[u64]) -> Result, String> { + let w = self.words; + let mut out = Vec::new(); + for i in 0..self.coefs.len() { + let (k, c) = (self.key(i), self.coefs[i]); + if c != 0.0 && k[..w].iter().all(|&x| x == 0) { + let flips: u32 = (0..w).map(|j| (k[w + j] & ones[w + j]).count_ones()).sum(); + out.push(if flips & 1 == 1 { -c } else { c }); + } + } + Ok(out) + } +} + +/// One certification case: a circuit, an observable, the qubits set in the +/// initial basis state, and how hard to truncate. +#[derive(Clone, Debug, PartialEq)] +pub struct SpdCase { + pub circuit: RotCircuit, + pub observable: Vec<(f64, Vec<(u32, Pauli)>)>, + pub ones: Vec, + pub cfg: SpdConfig, + pub max_terms: usize, +} + +/// A corpus of cases, drawn from a seed: random circuits of every gate the +/// propagation takes (fixed Cliffords, quarter turns, rotations at arbitrary +/// angles about strings of one to three factors), at thresholds from zero +/// up, some with a weight cap and some with a term cap, on registers that +/// take one to three words. Every third case repeats one block of gates +/// three times, as a layered circuit does, so strings a truncation dropped +/// come back and are revived in place; and the second ends in a rotation +/// whose new term cancels an old one exactly (`s·c − c·s`), so an addition +/// kills a live term. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SpdCorpus { + pub seed: u64, + pub cases: usize, + pub max_qubits: u32, + pub depth: usize, +} + +/// The default: 40 cases of up to 12 qubits (and some on 70 and 150, two and +/// four words), 60 gates each. +pub const SPD_DEFAULT: SpdCorpus = SpdCorpus { seed: 5, cases: 40, max_qubits: 12, depth: 60 }; + +/// The corpus digest of [`SPD_DEFAULT`] when the device's result is the +/// CPU's on every case: what a bit-exact certificate reproduces. +pub const SPD_DEFAULT_PIN: &str = "25737ef1c61f056c0830404c64dcb06d5355ea784bd15bebbc201500ae85bb8b"; + +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 SpdCorpus { + /// `"spd-v1 seed=… cases=… qubits=… depth=…"`. + pub fn generator(&self) -> String { + format!("spd-v1 seed={} cases={} qubits={} depth={}", self.seed, self.cases, self.max_qubits, self.depth) + } + + pub fn cases(&self) -> Vec { + let mut s = self.seed; + let below = |s: &mut u64, n: u64| next(s) % n.max(1); + (0..self.cases) + .map(|i| { + // Every fifth case on a register of two or four words. + let n = match i % 5 { + 3 => 70, + 4 if i % 10 == 9 => 150, + _ => 3 + below(&mut s, u64::from(self.max_qubits.max(4) - 2)) as u32, + }; + // Strings near the observable's support, so the sum grows; + // on a wide register, spread across it, so every word is used. + let span = n.min(self.max_qubits.max(4)); + let at = move |q: u32| if n > 64 { q * (n - 1) / (span - 1) } else { q }; + let string = |s: &mut u64, max: u64| -> Vec<(u32, Pauli)> { + let len = 1 + below(s, max) as usize; + let mut qs: Vec = Vec::new(); + while qs.len() < len { + let q = at(below(s, u64::from(span)) as u32); + if !qs.contains(&q) { + qs.push(q); + } + } + qs.into_iter().map(|q| (q, [Pauli::X, Pauli::Y, Pauli::Z][below(s, 3) as usize])).collect() + }; + let mut c = RotCircuit::new(n); + let layered = i % 3 == 2; + for _ in 0..if layered { self.depth.div_ceil(3) } else { self.depth } { + let q = below(&mut s, u64::from(span)) as u32; + let mut r = below(&mut s, u64::from(span - 1)) as u32; + if r >= q { + r += 1; + } + let (q, r) = (at(q), at(r)); + let op = match below(&mut s, 16) { + 0 => Op::H(q), + 1 => Op::S(q), + 2 => Op::Sdg(q), + 3 => Op::SX(q), + 4 => Op::SXdg(q), + 5 => Op::X(q), + 6 => Op::Y(q), + 7 => Op::Z(q), + 8 => Op::CX(q, r), + 9 => Op::CZ(q, r), + 10 => Op::Swap(q, r), + 11 => Op::QuarterRot { axis: string(&mut s, 3), k: 1 + below(&mut s, 3) as u8 }, + _ => Op::Rot { axis: string(&mut s, 3), theta: (next(&mut s) >> 11) as f64 / (1u64 << 53) as f64 * 6.5 - 0.1 }, + }; + c.push(op); + } + if layered { + let block = c.ops.clone(); + c.ops.extend(block.iter().cloned().chain(block.iter().cloned())); + } + let terms = 1 + below(&mut s, 3) as usize; + let mut observable: Vec<(f64, Vec<(u32, Pauli)>)> = (0..terms).map(|_| (0.25 + (next(&mut s) >> 11) as f64 / (1u64 << 53) as f64, string(&mut s, 2))).collect(); + if i == 1 { + // Z(0.7) first on cos·X + sin·Y: Y's coefficient becomes + // sin·cos − cos·sin, exactly zero. + let (sn, cs) = sin_cos(0.7); + observable = vec![(cs, vec![(0, Pauli::X)]), (sn, vec![(0, Pauli::Y)])]; + c.ops.push(Op::Rot { axis: vec![(0, Pauli::Z)], theta: 0.7 }); + } + let ones: Vec = (0..span).filter(|_| below(&mut s, 3) == 0).map(at).collect(); + let threshold = [0.0, 1e-6, 1e-4, 1e-3, 1e-2][i % 5]; + let max_weight = (i % 7 == 6).then_some(4); + let max_terms = if i % 6 == 5 { 64 } else { usize::MAX }; + SpdCase { circuit: c, observable, ones, cfg: SpdConfig { threshold, max_weight }, max_terms } + }) + .collect() + } +} + +impl SpdCase { + /// The case as bytes: the register, every op, the observable, the basis + /// state and the truncation. + pub fn bytes(&self) -> Vec { + let mut o = Vec::new(); + o.extend_from_slice(&self.circuit.n.to_le_bytes()); + let string = |o: &mut Vec, s: &[(u32, Pauli)]| { + o.extend_from_slice(&(s.len() as u64).to_le_bytes()); + for &(q, p) in s { + o.extend_from_slice(&q.to_le_bytes()); + o.push(p as u8); + } + }; + for op in &self.circuit.ops { + let (code, a, b): (u8, u32, u32) = match *op { + Op::H(q) => (0, q, 0), + Op::S(q) => (1, q, 0), + Op::Sdg(q) => (2, q, 0), + Op::SX(q) => (3, q, 0), + Op::SXdg(q) => (4, q, 0), + Op::X(q) => (5, q, 0), + Op::Y(q) => (6, q, 0), + Op::Z(q) => (7, q, 0), + Op::CX(a, b) => (8, a, b), + Op::CZ(a, b) => (9, a, b), + Op::Swap(a, b) => (10, a, b), + Op::QuarterRot { k, .. } => (11, u32::from(k), 0), + Op::Rot { .. } => (12, 0, 0), + }; + o.push(code); + o.extend_from_slice(&a.to_le_bytes()); + o.extend_from_slice(&b.to_le_bytes()); + match op { + Op::QuarterRot { axis, .. } => string(&mut o, axis), + Op::Rot { axis, theta } => { + string(&mut o, axis); + o.extend_from_slice(&theta.to_bits().to_le_bytes()); + } + _ => {} + } + } + for (c, s) in &self.observable { + o.extend_from_slice(&c.to_bits().to_le_bytes()); + string(&mut o, s); + } + for q in &self.ones { + o.extend_from_slice(&q.to_le_bytes()); + } + o.extend_from_slice(&self.cfg.threshold.to_bits().to_le_bytes()); + o.extend_from_slice(&self.cfg.max_weight.map_or(u32::MAX, |w| w).to_le_bytes()); + o.extend_from_slice(&(self.max_terms as u64).to_le_bytes()); + o + } + + /// The CPU's answer ([`output`]): its result and the table it ended + /// with. + #[cfg(feature = "quantum_fabric")] + pub fn reference(&self) -> Result { + let (r, t, keys, coefs) = super::expectation_table(&self.circuit, &self.observable, &self.ones, &self.cfg, self.max_terms)?; + Ok(output(&r, t, &keys, &coefs)) + } + + /// The case run on `dev` ([`expectation_on`]), as a certificate output. + #[cfg(feature = "quantum_fabric")] + pub fn run_on(&self, dev: &mut dyn SpdDevice) -> Result { + let (r, t) = expectation_on(dev, &self.circuit, &self.observable, &self.ones, &self.cfg, self.max_terms)?; + let (keys, coefs) = dev.table()?; + Ok(output(&r, t, &keys, &coefs)) + } +} + +/// A result and the table it ended with as a certificate output, every bit +/// compared: the value's, the bound's and the threshold's bits, the peak, +/// final terms and term updates, then every slot's string and every +/// coefficient's bits, in table order. A device that keeps the CPU's numbers +/// but not its term set or its order is caught. +#[cfg(feature = "quantum_fabric")] +pub fn output(r: &SpdResult, threshold: f64, keys: &[u64], coefs: &[f64]) -> crate::quantum_fabric::Output { + let mut v = vec![r.value.to_bits(), r.truncation_bound.to_bits(), threshold.to_bits(), r.peak_terms as u64, r.final_terms as u64, r.term_updates]; + v.extend_from_slice(keys); + v.extend(coefs.iter().map(|c| c.to_bits())); + crate::quantum_fabric::Output::Bits(v) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn same(a: &SpdResult, b: &SpdResult) -> bool { + (a.value.to_bits(), a.truncation_bound.to_bits(), a.peak_terms, a.final_terms, a.term_updates) == (b.value.to_bits(), b.truncation_bound.to_bits(), b.peak_terms, b.final_terms, b.term_updates) + } + + /// The host's protocol, counting what its rotations did. + #[derive(Default)] + struct Counting { + dev: HostDevice, + seen: Rotated, + } + + impl SpdDevice for Counting { + fn load(&mut self, words: usize, keys: &[u64], coefs: &[f64]) -> Result<(), String> { + self.dev.load(words, keys, coefs) + } + fn rotate(&mut self, axis: &[u64], sin: f64, cos: f64) -> Result { + let r = self.dev.rotate(axis, sin, cos)?; + self.seen.fresh += r.fresh; + self.seen.revived += r.revived; + self.seen.killed += r.killed; + self.seen.fresh_zero += r.fresh_zero; + Ok(r) + } + fn truncate(&mut self, threshold: f64, max_weight: Option) -> Result, String> { + self.dev.truncate(threshold, max_weight) + } + fn compact(&mut self) -> Result<(), String> { + self.dev.compact() + } + fn cliffords(&mut self, gates: &[Gate]) -> Result<(), String> { + self.dev.cliffords(gates) + } + fn contributions(&mut self, ones: &[u64]) -> Result, String> { + self.dev.contributions(ones) + } + fn table(&mut self) -> Result<(Vec, Vec), String> { + self.dev.table() + } + } + + /// The pinned corpus reaches every decision a device makes: new terms + /// appended, dead slots revived in place, live ones killed by an + /// addition, and growth past what the term cap allows. + #[test] + fn the_corpus_reaches_every_decision_of_the_term_set() { + let mut seen = Rotated::default(); + let (mut capped, mut weighed) = (0, 0); + for case in SPD_DEFAULT.cases() { + let mut dev = Counting::default(); + let (_, t) = expectation_on(&mut dev, &case.circuit, &case.observable, &case.ones, &case.cfg, case.max_terms).unwrap(); + seen.fresh += dev.seen.fresh; + seen.revived += dev.seen.revived; + seen.killed += dev.seen.killed; + capped += usize::from(t > case.cfg.threshold); + weighed += usize::from(case.cfg.max_weight.is_some()); + } + assert!(seen.fresh > 1000 && seen.revived > 10 && seen.killed > 0 && capped > 0 && weighed > 0, "{seen:?} capped {capped} weighed {weighed}"); + } + + /// Ground truth first: the protocol, run on the host one term after + /// another, is the CPU's propagation bit for bit, on every case of the + /// corpus and on wider random ones, at one thread and many. + #[test] + fn the_protocol_on_the_host_is_the_cpu_bit_for_bit() { + let mut grew = 0; + let mut capped = 0; + let cases: Vec = SPD_DEFAULT.cases().into_iter().chain(SpdCorpus { seed: 99, cases: 60, max_qubits: 16, depth: 90 }.cases()).collect(); + for (i, case) in cases.iter().enumerate() { + let (want, t) = super::super::expectation_capped(&case.circuit, &case.observable, &case.ones, &case.cfg, 1 + i % 4, case.max_terms).unwrap(); + let mut dev = HostDevice::default(); + let (got, u) = expectation_on(&mut dev, &case.circuit, &case.observable, &case.ones, &case.cfg, case.max_terms).unwrap(); + assert!(same(&want, &got) && t.to_bits() == u.to_bits(), "case {i}: {want:?} {got:?}"); + // And the table, slot for slot: the term set and its order. + let (_, _, keys, coefs) = super::super::expectation_table(&case.circuit, &case.observable, &case.ones, &case.cfg, case.max_terms).unwrap(); + let (dk, dc) = dev.table().unwrap(); + assert!(keys == dk && coefs.iter().map(|c| c.to_bits()).eq(dc.iter().map(|c| c.to_bits())), "case {i}: the table differs"); + grew += usize::from(want.peak_terms > 50); + capped += usize::from(t > case.cfg.threshold); + } + assert!(grew > 10 && capped > 2, "the corpus exercises growth ({grew}) and the term cap ({capped})"); + } + + #[cfg(feature = "quantum_fabric")] + #[test] + fn the_cpu_reproduces_the_pinned_digest() { + let mut cert = crate::quantum_fabric::Certification::new(); + for case in SPD_DEFAULT.cases() { + let r = case.reference().unwrap(); + cert.add(&case.bytes(), &r, &r); + } + let c = cert.finish(KERNEL_SPD, "cpu", "cpu", &SPD_DEFAULT.generator()); + let hex: String = c.corpus.iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!(hex, SPD_DEFAULT_PIN); + } + + /// A device's gate encoding and conjugation table give each gate's + /// conjugation as the CPU's: decoded and applied, every term the same. + #[test] + fn the_encoded_gates_and_the_table_are_the_cpus_conjugations() { + let t = conj_table(); + for (g, op) in [Op::H(0), Op::S(0), Op::Sdg(0), Op::SX(0), Op::SXdg(0), Op::X(0), Op::Y(0), Op::Z(0)].iter().enumerate() { + for (x, z) in [(false, false), (false, true), (true, false), (true, true)] { + let e = t[4 * g + 2 * usize::from(x) + usize::from(z)]; + assert_eq!(((e >> 2) & 1 == 1, (e >> 1) & 1 == 1, e & 1 == 1), conj1(op, x, z)); + } + } + let gates = vec![Gate::Fixed(Op::CX(1, 70)), Gate::Quarter(key_of(&[(3, Pauli::Y), (66, Pauli::X)], 2), 3), Gate::Fixed(Op::SX(66)), Gate::Quarter(key_of(&[(0, Pauli::Z)], 2), 1)]; + let (ops, axes) = encode(&gates, 2); + assert_eq!(ops.len(), 16); + assert_eq!((ops[4], ops[5], ops[6]), (11, 3, 0)); + assert_eq!((ops[12], ops[13], ops[14]), (11, 1, 1)); + assert_eq!(&axes[4..8], key_of(&[(0, Pauli::Z)], 2).as_slice()); + assert_eq!(encode(&[], 1), (vec![0], vec![0])); + } + + #[test] + fn the_runtime_words_are_the_cpus_words() { + for (n, w) in [(1u32, 1usize), (64, 1), (65, 2), (128, 2), (129, 4), (256, 4), (257, 8), (512, 8), (513, 16), (1024, 16)] { + assert_eq!(words_for(n), w, "{n}"); + } + let a = key_of(&[(0, Pauli::X), (70, Pauli::Y)], 2); + let b = key_of(&[(0, Pauli::Z), (70, Pauli::Y)], 2); + assert!(!commutes(&a, &b, 2) && weight(&a, 2) == 2); + assert_eq!(mul(&a, &a, 2), (vec![0; 4], 0)); + } + + #[test] + fn a_device_s_refusals_are_the_cpus() { + let mut dev = HostDevice::default(); + let c = RotCircuit::new(4); + let e = expectation_on(&mut dev, &c, &[(1.0, vec![(9, Pauli::Z)])], &[], &SpdConfig::default(), usize::MAX).unwrap_err(); + assert_eq!(e, OnDevice::Refused(SpdError::QubitOutOfRange { qubit: 9, n: 4 })); + } +} diff --git a/wai/wai-quantum/tests/cuda_spd.rs b/wai/wai-quantum/tests/cuda_spd.rs new file mode 100644 index 000000000..0245e8740 --- /dev/null +++ b/wai/wai-quantum/tests/cuda_spd.rs @@ -0,0 +1,104 @@ +//! Sparse Pauli dynamics on the CUDA devices of the machine that runs this: +//! the CPU's result bit for bit (value, bound, threshold reached, peak, final +//! terms, term updates), 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::quantum_fabric::spd::{MUTANTS, mutate}; +use wai_quantum::quantum_fabric::{Certification, Parity}; +use wai_quantum::quantum_fabric_cuda::{CudaMember, discover}; +use wai_quantum::quantum_spd::device::{KERNEL_SPD, SPD_DEFAULT, SPD_DEFAULT_PIN, SpdCorpus, expectation_on}; +use wai_quantum::quantum_spd::{SpdConfig, expectation_capped, heavy_hex_127, kicked_ising}; + +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 { + let d = discover(); + let mut found: Vec = 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: SpdCorpus) -> 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(KERNEL_SPD, "cpu", "cpu", &corpus.generator()).corpus) +} + +#[test] +fn d0_the_propagation_is_the_cpus_bit_for_bit() { + let cpu = cpu_digest(SPD_DEFAULT); + assert_eq!(cpu, SPD_DEFAULT_PIN, "this machine's CPU does not reproduce the pin"); + for m in members() { + let spd = m.spd().unwrap_or_else(|e| panic!("{}: {e}", m.key())); + let c = spd.certify(SPD_DEFAULT).unwrap_or_else(|e| panic!("{}: {e}", m.key())); + assert_eq!(c.parity, Parity::BitExact, "{}", m.key()); + assert_eq!(hex(&c.corpus), cpu, "{}: the digest is not the CPU's", m.key()); + report(&format!("cuda spd certified: key={} member={} kernel={} implementation={} parity=bit-exact cases={} digest={}", m.key(), m.id(), c.kernel, c.implementation, c.cases, hex(&c.corpus))); + } +} + +/// Beyond the corpus: wider random cases, and the 127-qubit kicked-Ising +/// circuit on the heavy-hex map, whose sum grows past a hundred thousand +/// terms; each the CPU's result bit for bit. +#[test] +fn d1_large_sums_match_the_cpu() { + for m in members() { + let spd = m.spd().unwrap(); + let mut largest = 0; + for case in (SpdCorpus { seed: 77, cases: 24, max_qubits: 20, depth: 160 }).cases() { + let want = expectation_capped(&case.circuit, &case.observable, &case.ones, &case.cfg, 8, case.max_terms).unwrap(); + let got = expectation_on(&mut spd.run(), &case.circuit, &case.observable, &case.ones, &case.cfg, case.max_terms).unwrap(); + assert_eq!((got.0.value.to_bits(), got.0.truncation_bound.to_bits(), got.0.peak_terms, got.0.final_terms, got.0.term_updates, got.1.to_bits()), (want.0.value.to_bits(), want.0.truncation_bound.to_bits(), want.0.peak_terms, want.0.final_terms, want.0.term_updates, want.1.to_bits()), "{}", m.key()); + largest = largest.max(want.0.peak_terms); + } + let c = kicked_ising(127, &heavy_hex_127(), 0.6, 5); + let obs = vec![(1.0, vec![(62u32, wai_quantum::quantum_spd::Pauli::Z)])]; + let cfg = SpdConfig { threshold: 1e-4, max_weight: None }; + let want = expectation_capped(&c, &obs, &[], &cfg, 8, usize::MAX).unwrap(); + let got = expectation_on(&mut spd.run(), &c, &obs, &[], &cfg, usize::MAX).unwrap(); + assert_eq!((got.0.value.to_bits(), got.0.truncation_bound.to_bits(), got.0.peak_terms, got.0.final_terms, got.0.term_updates), (want.0.value.to_bits(), want.0.truncation_bound.to_bits(), want.0.peak_terms, want.0.final_terms, want.0.term_updates), "{}: kicked Ising", m.key()); + report(&format!("cuda spd large: key={} 24 random cases (peak {largest} terms) and kicked Ising on 127 qubits (peak {} terms) match the CPU bit for bit", m.key(), want.0.peak_terms)); + } +} + +#[test] +fn d2_mutants_are_caught() { + let small = SPD_DEFAULT; + let cpu = cpu_digest(small); + for m in members() { + let mut killed = 0; + for mutant in &MUTANTS { + let spd = m.spd_with_source(&mutate(mutant)).unwrap_or_else(|e| panic!("mutant {}: {e}", mutant.name)); + match spd.certify(small) { + Ok(c) => assert!(c.parity != Parity::BitExact || hex(&c.corpus) != cpu, "{}: mutant {} survived", m.key(), mutant.name), + // A mutant that makes the device fail is caught too. + Err(_) => {} + } + killed += 1; + } + report(&format!("cuda spd mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len())); + } +} diff --git a/wai/wai-quantum/tests/cuda_sv.rs b/wai/wai-quantum/tests/cuda_sv.rs index 611ff0def..bf2ecfd58 100644 --- a/wai/wai-quantum/tests/cuda_sv.rs +++ b/wai/wai-quantum/tests/cuda_sv.rs @@ -8,7 +8,7 @@ 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::sv64::{self, MUTANTS, SV_DEFAULT, SV_DEFAULT_PIN, SV_WIDE, SV_WIDE_PIN, SvCorpus}; use wai_quantum::quantum_fabric::{Certification, Parity}; use wai_quantum::quantum_fabric_cuda::{CudaMember, discover}; use wai_quantum::quantum_sv::StateVector; @@ -46,10 +46,26 @@ fn cpu_digest(corpus: SvCorpus) -> String { hex(&cert.finish(sv64::KERNEL_SV_RUN, "cpu", "cpu", &corpus.generator()).corpus) } +/// The CPU certifying itself on [`SV_WIDE`]'s wide cases. +fn cpu_wide_digest() -> String { + let mut cert = Certification::new(); + for case in SV_WIDE.wide_cases() { + let r = case.reference().unwrap(); + cert.add(&case.bytes(), &r, &r); + } + hex(&cert.finish(sv64::KERNEL_SV_RUN_WIDE, "cpu", "cpu", &SV_WIDE.wide_generator()).corpus) +} + +/// Whether a wide certificate reproduces the CPU's pinned digest. +fn wide_exact(w: &wai_quantum::quantum_fabric::Certificate) -> bool { + w.parity == Parity::BitExact && hex(&w.corpus) == SV_WIDE_PIN +} + #[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"); + assert_eq!(cpu_wide_digest(), SV_WIDE_PIN, "this machine's CPU does not reproduce the wide corpus's 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())); @@ -57,6 +73,12 @@ fn s0_the_state_vector_is_the_cpus_bit_for_bit() { 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))); + // The wider kernels, on their own corpus: bit-exact, or said not to + // be (such a member is placed only for gates on up to three qubits). + let w = sv.certify_wide(SV_WIDE).unwrap_or_else(|e| panic!("{}: {e}", m.key())); + assert_eq!(w.cases, SV_WIDE.cases as u64); + let exact = wide_exact(&w); + report(&format!("cuda sv wide certified: key={} member={} kernel={} implementation={} parity={} cases={} digest={}", m.key(), m.id(), w.kernel, w.implementation, if exact { "bit-exact" } else { "not-bit-exact" }, w.cases, hex(&w.corpus))); } } @@ -110,3 +132,38 @@ fn s2_mutants_are_caught() { report(&format!("cuda sv mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len())); } } + +/// Ground truth beyond the pinned corpus, on the resident path: random +/// circuits at every fusion width the kernels take (0 to 6), run from +/// `|0…0⟩` and, as a partition runs, from a state given (the CPU's state after +/// the first half of the gates), each the CPU's state bit for bit. +#[test] +fn s3_random_circuits_match_the_cpu_from_zero_and_from_a_state() { + let same = |a: &[f64], b: &[f64]| a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits()); + for m in members() { + let sv = m.sv().unwrap(); + let wide = wide_exact(&sv.certify_wide(SV_WIDE).unwrap()); + let mut runs = 0; + for case in sv64::random_cases(2026, 70, 16, 60) { + let launches = sv64::plan(case.n, &case.gates, case.width).unwrap(); + // A member whose wider kernels did not certify is never placed + // for them: its narrow kernels alone are held to the CPU. + if !wide && launches.iter().any(|l| sv64::launch_bits(l).len() as u32 > sv64::NARROW) { + continue; + } + let mut cpu = StateVector::zero(case.n).unwrap(); + cpu.run(&case.gates, case.width).unwrap(); + let (re, im) = sv.run_from_zero(case.n, &launches).unwrap_or_else(|e| panic!("{}: {e}", m.key())); + assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: from zero, n={} width={}", m.key(), case.n, case.width); + let half = case.gates.len() / 2; + let mut cpu = StateVector::zero(case.n).unwrap(); + cpu.run(&case.gates[..half], case.width).unwrap(); + let rest = sv64::plan(case.n, &case.gates[half..], case.width).unwrap(); + let (re, im) = sv.run_launches(&cpu.re, &cpu.im, &rest).unwrap(); + cpu.run(&case.gates[half..], case.width).unwrap(); + assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: from a state, n={} width={}", m.key(), case.n, case.width); + runs += 2; + } + report(&format!("cuda sv random: key={} {runs} resident runs on {} match the CPU bit for bit", m.key(), if wide { "every kernel" } else { "the narrow kernels" })); + } +} diff --git a/wai/wai-quantum/tests/fabric_placement.rs b/wai/wai-quantum/tests/fabric_placement.rs index fc6a38107..0a2c8e971 100644 --- a/wai/wai-quantum/tests/fabric_placement.rs +++ b/wai/wai-quantum/tests/fabric_placement.rs @@ -97,17 +97,51 @@ fn p0_each_member_gives_the_cpus_answers_byte_for_byte() { } #[test] -fn p1_auto_stays_on_the_cpu_and_accelerator_takes_the_first_member() { +fn p1_auto_follows_the_measurements_and_accelerator_takes_the_first_member() { let found = members(); - // Auto places nothing, however wide the state and whatever members there - // are: the result is the one it always was, with no `fabric`. - for n in [8u32, 21] { - let auto = ran(&job(n, json!({}))).unwrap(); - assert!(auto.get("fabric").is_none(), "auto at {n} qubits stays on the CPU, unnamed: {}", auto["fabric"]); - let plan = catalog::plan(&job(n, json!({})), CAP).unwrap(); - assert!(!plan.why.contains("its state on"), "{}", plan.why); + // Below every measured size, auto places nothing: the result is the one + // it always was, with no `fabric`. + let auto = ran(&job(8, json!({}))).unwrap(); + assert!(auto.get("fabric").is_none(), "auto at 8 qubits stays on the CPU, unnamed: {}", auto["fabric"]); + let plan = catalog::plan(&job(8, json!({})), CAP).unwrap(); + assert!(!plan.why.contains("its state on"), "{}", plan.why); + // Where the committed measurements place a state on a member found here, + // auto places it there, says why in the receipt, and gives the CPU's + // answers; a shallower job than the one measured stays on the CPU. + let listed = fabric::list(CAP); + let places: Vec = listed["auto_rule"]["places"].as_array().cloned().unwrap_or_default(); + report(&format!("fabric auto host: {} measured={}", fabric::host(), listed["auto_rule"]["measured_here"])); + let schemas = wai_quantum::engine::schema::Registry::engine(); + assert!(schemas.check("tools.v1.json#/$defs/fabric_members", &listed).is_empty(), "{listed}"); + match places.iter().min_by_key(|p| (p["from_qubits"].as_u64(), p["fusion"].as_u64())) { + Some(p) => { + let (n, fusion) = (p["from_qubits"].as_u64().unwrap() as u32, p["fusion"].as_u64().unwrap() as u32); + // Every member a rule places at this size and fusion; auto takes + // the one quickest there. + let candidates: Vec<&str> = places.iter().filter(|q| q["fusion"] == p["fusion"] && q["from_qubits"].as_u64() <= Some(u64::from(n)) && q["to_qubits"].as_u64() >= Some(u64::from(n))).filter_map(|q| q["key"].as_str()).collect(); + let placed = ran(&wai_quantum::engine::fabric_measured::job(n, fusion, "auto")).unwrap(); + let cpu = ran(&wai_quantum::engine::fabric_measured::job(n, fusion, "cpu")).unwrap(); + let f = &placed["fabric"]; + let key = f["key"].as_str().unwrap_or_default(); + assert!(candidates.contains(&key) && f["asked"] == "auto", "{f} {candidates:?}"); + assert_eq!((f["auto"]["from_qubits"].as_u64(), f["auto"]["fusion"].as_u64()), (Some(u64::from(n)), Some(u64::from(fusion))), "{f}"); + assert!(f["why"].as_str().unwrap().starts_with("auto: its state on"), "{f}"); + assert!(schemas.check("job-result.v1.json", &placed).is_empty(), "{placed}"); + assert_eq!(answers(&placed), answers(&cpu), "auto on {key}: the answers differ from the CPU's"); + let plan = catalog::plan(&wai_quantum::engine::fabric_measured::job(n, fusion, "auto"), CAP).unwrap(); + assert!(plan.why.contains("auto: its state on"), "{}", plan.why); + // This file's job has half the measured circuit's layers. + let shallow = job(n, json!({ "options": { "fusion": fusion } })); + assert!(fabric::launches(&shallow).unwrap() < p["min_launches"].as_u64().unwrap() as usize); + assert!(ran(&shallow).unwrap().get("fabric").is_none(), "a shallower job than the one measured stays on the CPU"); + report(&format!("fabric auto: by measurement, {n} qubits at fusion {fusion} on {key}, the CPU's answers byte for byte; a shallower job on the CPU")); + } + None => { + let auto = ran(&wai_quantum::engine::fabric_measured::job(21, 3, "auto")).unwrap(); + assert!(auto.get("fabric").is_none(), "no member here is measured faster, so auto stays on the CPU: {}", auto["fabric"]); + report("fabric auto: no member here measured faster; every state on the CPU, its result unnamed"); + } } - report("fabric auto: every state on the CPU, its result unnamed"); let Some((first, _)) = found.first() else { let e = catalog::plan(&job(8, json!({ "fabric": "accelerator" })), CAP).unwrap_err(); assert!(e.ends_with("[J013]") && e.contains("no certified accelerator member here"), "{e}"); @@ -159,14 +193,95 @@ fn p2_a_state_split_over_members_is_the_cpus() { #[test] fn p3_a_state_past_a_member_is_refused_before_it_runs() { for (key, max) in members() { + if max >= u64::from(wai_quantum::quantum_sv::MAX_QUBITS) { + report(&format!("fabric refused: key={key} nothing: it holds the most a state vector takes, {max} qubits")); + continue; + } let n = (max + 1) as u32; - // Planned only: the refusal comes before any memory is taken. - let e = catalog::plan(&job(n, json!({ "fabric": key, "shots": 1, "observables": [], "amplitudes": [] })), 64 << 30).unwrap_err(); + // Planned only: the refusal comes before any memory is taken. The + // job's memory is large enough that the CPU would hold the state, so + // the member's limit is what refuses it. + let e = catalog::plan(&job(n, json!({ "fabric": key, "shots": 1, "observables": [], "amplitudes": [] })), 1 << 42).unwrap_err(); assert!(e.ends_with("[J013]") && e.contains(&format!("holds at most {max}")), "{key}: {e}"); report(&format!("fabric refused: key={key} a state of {n} qubits, past the {max} it holds")); } } +/// A job whose gates act on more than three qubits at once (fusion 5) runs +/// on a member only where its kernels for them certified bit-exact here, and +/// is refused, never run elsewhere, where they did not. +#[test] +fn p5_wider_gates_run_only_where_the_wide_kernels_certified() { + let listed = fabric::list(CAP); + for (key, _) in members() { + let m = listed["members"].as_array().unwrap().iter().find(|m| m["key"] == json!(key)).unwrap(); + let exact = m["wide"]["parity"]["tier"] == "bit-exact" && m["wide"].get("why").is_none(); + let wide = json!({ "fabric": key, "options": { "fusion": 5 } }); + let mut cpu = job(14, json!({ "fabric": "cpu", "options": { "fusion": 5 } })); + cpu["shots"] = json!(500); + let mut placed = job(14, wide); + placed["shots"] = json!(500); + if exact { + let a = ran(&cpu).unwrap(); + let b = ran(&placed).unwrap_or_else(|e| panic!("{key}: {e}")); + assert_eq!(answers(&a), answers(&b), "{key}: fusion 5"); + assert_eq!(b["fabric"]["wide"]["parity"]["tier"], "bit-exact", "{}", b["fabric"]); + report(&format!("fabric wide: key={key} placed at fusion 5, the CPU's answers byte for byte")); + } else { + let e = ran(&placed).unwrap_err(); + assert!(e.ends_with("[J013]") && e.contains("run-wide"), "{key}: {e}"); + report(&format!("fabric wide: key={key} refused at fusion 5: its wider kernels did not certify bit-exact here")); + } + } +} + +/// A `pauli` job (sparse Pauli dynamics, past the state vector) on each +/// member certified here for its propagation gives the CPU's answers byte +/// for byte, its sum of terms on the device; with no such member, +/// `accelerator` is refused. `WAI_QUANTUM_FABRIC_PAULI_EXPECT` (optional) +/// declares the members' keys, comma-separated, or `none`. +#[test] +fn p6_a_pauli_job_on_each_member_is_the_cpus() { + let listed = fabric::list(CAP); + let keys: Vec = listed["pauli"]["members"].as_array().unwrap().iter().filter(|m| m["key"] != "cpu").map(|m| m["key"].as_str().unwrap().to_string()).collect(); + if let Ok(e) = std::env::var("WAI_QUANTUM_FABRIC_PAULI_EXPECT") { + assert_eq!(if keys.is_empty() { "none".to_string() } else { keys.join(",") }, e, "the pauli method's members are not the declared ones: {}", listed["pauli"]); + } + let mut q = "OPENQASM 3.0;\nqubit[60] q;\n".to_string(); + for layer in 0..5u32 { + for i in 0..60u32 { + q.push_str(&format!("rx({}) q[{i}];\n", 0.31 + 0.017 * f64::from(i + 3 * layer))); + } + for i in (layer % 2..59).step_by(2) { + q.push_str(&format!("rzz({}) q[{i}], q[{}];\n", 0.45 + 0.011 * f64::from(i), i + 1)); + } + } + let job = |f: &str| json!({ "qasm": q, "method": "pauli", "observables": ["Z30", "X29 X30", "0.5 Y31 Z32"], "options": { "threshold": 1e-5 }, "fabric": f }); + let cpu = ran(&job("cpu")).unwrap(); + for key in &keys { + let placed = ran(&job(key)).unwrap_or_else(|e| panic!("{key}: {e}")); + assert_eq!(placed["fabric"]["kernel"], "wai.quantum.spd/propagate", "{}", placed["fabric"]); + assert_eq!((placed["fabric"]["key"].as_str(), placed["fabric"]["parity"]["tier"].as_str()), (Some(key.as_str()), Some("bit-exact"))); + assert_eq!(answers(&placed), answers(&cpu), "{key}: the pauli job's answers differ from the CPU's"); + let peak = cpu["expectations"].as_array().unwrap().iter().filter_map(|e| e["peak_terms"].as_u64()).max().unwrap_or(0); + report(&format!("fabric pauli: key={key} 60 qubits, up to {peak} terms, the CPU's answers byte for byte")); + } + match keys.first() { + Some(first) => { + let placed = ran(&job("accelerator")).unwrap(); + assert_eq!(placed["fabric"]["key"].as_str(), Some(first.as_str())); + assert_eq!(answers(&placed), answers(&cpu)); + } + None => { + let e = ran(&job("accelerator")).unwrap_err(); + assert!(e.ends_with("[J013]"), "{e}"); + report("fabric pauli: no member here certified for it; accelerator refused"); + } + } + // Auto keeps it on the CPU: no member is measured faster for it. + assert!(ran(&job("auto")).unwrap().get("fabric").is_none()); +} + /// Where auto's threshold comes from: the time of the same job on the CPU and /// on each member, from 14 to 24 qubits, reported and not asserted. #[test] diff --git a/wai/wai-quantum/tests/hip_sv.rs b/wai/wai-quantum/tests/hip_sv.rs index 9f0d3564c..9f2bd4361 100644 --- a/wai/wai-quantum/tests/hip_sv.rs +++ b/wai/wai-quantum/tests/hip_sv.rs @@ -8,7 +8,7 @@ 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::sv64::{self, MUTANTS, SV_DEFAULT, SV_DEFAULT_PIN, SV_WIDE, SV_WIDE_PIN, SvCorpus}; use wai_quantum::quantum_fabric::{Certification, Parity}; use wai_quantum::quantum_fabric_hip::{HipMember, discover}; use wai_quantum::quantum_sv::StateVector; @@ -46,10 +46,26 @@ fn cpu_digest(corpus: SvCorpus) -> String { hex(&cert.finish(sv64::KERNEL_SV_RUN, "cpu", "cpu", &corpus.generator()).corpus) } +/// The CPU certifying itself on [`SV_WIDE`]'s wide cases. +fn cpu_wide_digest() -> String { + let mut cert = Certification::new(); + for case in SV_WIDE.wide_cases() { + let r = case.reference().unwrap(); + cert.add(&case.bytes(), &r, &r); + } + hex(&cert.finish(sv64::KERNEL_SV_RUN_WIDE, "cpu", "cpu", &SV_WIDE.wide_generator()).corpus) +} + +/// Whether a wide certificate reproduces the CPU's pinned digest. +fn wide_exact(w: &wai_quantum::quantum_fabric::Certificate) -> bool { + w.parity == Parity::BitExact && hex(&w.corpus) == SV_WIDE_PIN +} + #[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"); + assert_eq!(cpu_wide_digest(), SV_WIDE_PIN, "this machine's CPU does not reproduce the wide corpus's 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())); @@ -57,6 +73,12 @@ fn s0_the_state_vector_is_the_cpus_bit_for_bit() { 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!("hip sv certified: key={} member={} kernel={} implementation={} parity=bit-exact cases={} digest={}", m.key(), m.id(), c.kernel, c.implementation, c.cases, hex(&c.corpus))); + // The wider kernels, on their own corpus: bit-exact, or said not to + // be (such a member is placed only for gates on up to three qubits). + let w = sv.certify_wide(SV_WIDE).unwrap_or_else(|e| panic!("{}: {e}", m.key())); + assert_eq!(w.cases, SV_WIDE.cases as u64); + let exact = wide_exact(&w); + report(&format!("hip sv wide certified: key={} member={} kernel={} implementation={} parity={} cases={} digest={}", m.key(), m.id(), w.kernel, w.implementation, if exact { "bit-exact" } else { "not-bit-exact" }, w.cases, hex(&w.corpus))); } } @@ -110,3 +132,38 @@ fn s2_mutants_are_caught() { report(&format!("hip sv mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len())); } } + +/// Ground truth beyond the pinned corpus, on the resident path: random +/// circuits at every fusion width the kernels take (0 to 6), run from +/// `|0…0⟩` and, as a partition runs, from a state given (the CPU's state after +/// the first half of the gates), each the CPU's state bit for bit. +#[test] +fn s3_random_circuits_match_the_cpu_from_zero_and_from_a_state() { + let same = |a: &[f64], b: &[f64]| a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits()); + for m in members() { + let sv = m.sv().unwrap(); + let wide = wide_exact(&sv.certify_wide(SV_WIDE).unwrap()); + let mut runs = 0; + for case in sv64::random_cases(2026, 70, 16, 60) { + let launches = sv64::plan(case.n, &case.gates, case.width).unwrap(); + // A member whose wider kernels did not certify is never placed + // for them: its narrow kernels alone are held to the CPU. + if !wide && launches.iter().any(|l| sv64::launch_bits(l).len() as u32 > sv64::NARROW) { + continue; + } + let mut cpu = StateVector::zero(case.n).unwrap(); + cpu.run(&case.gates, case.width).unwrap(); + let (re, im) = sv.run_from_zero(case.n, &launches).unwrap_or_else(|e| panic!("{}: {e}", m.key())); + assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: from zero, n={} width={}", m.key(), case.n, case.width); + let half = case.gates.len() / 2; + let mut cpu = StateVector::zero(case.n).unwrap(); + cpu.run(&case.gates[..half], case.width).unwrap(); + let rest = sv64::plan(case.n, &case.gates[half..], case.width).unwrap(); + let (re, im) = sv.run_launches(&cpu.re, &cpu.im, &rest).unwrap(); + cpu.run(&case.gates[half..], case.width).unwrap(); + assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: from a state, n={} width={}", m.key(), case.n, case.width); + runs += 2; + } + report(&format!("hip sv random: key={} {runs} resident runs on {} match the CPU bit for bit", m.key(), if wide { "every kernel" } else { "the narrow kernels" })); + } +} diff --git a/wai/wai-quantum/tests/opencl_sv.rs b/wai/wai-quantum/tests/opencl_sv.rs index 1190ed73f..0d7165198 100644 --- a/wai/wai-quantum/tests/opencl_sv.rs +++ b/wai/wai-quantum/tests/opencl_sv.rs @@ -9,7 +9,7 @@ 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::sv64::{self, MUTANTS, SV_DEFAULT, SV_DEFAULT_PIN, SV_WIDE, SV_WIDE_PIN, SvCorpus}; use wai_quantum::quantum_fabric::{Certification, Parity}; use wai_quantum::quantum_fabric_opencl::{OpenClError, OpenClMember, discover}; use wai_quantum::quantum_sv::StateVector; @@ -47,10 +47,26 @@ fn cpu_digest(corpus: SvCorpus) -> String { hex(&cert.finish(sv64::KERNEL_SV_RUN, "cpu", "cpu", &corpus.generator()).corpus) } +/// The CPU certifying itself on [`SV_WIDE`]'s wide cases. +fn cpu_wide_digest() -> String { + let mut cert = Certification::new(); + for case in SV_WIDE.wide_cases() { + let r = case.reference().unwrap(); + cert.add(&case.bytes(), &r, &r); + } + hex(&cert.finish(sv64::KERNEL_SV_RUN_WIDE, "cpu", "cpu", &SV_WIDE.wide_generator()).corpus) +} + +/// Whether a wide certificate reproduces the CPU's pinned digest. +fn wide_exact(w: &wai_quantum::quantum_fabric::Certificate) -> bool { + w.parity == Parity::BitExact && hex(&w.corpus) == SV_WIDE_PIN +} + #[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"); + assert_eq!(cpu_wide_digest(), SV_WIDE_PIN, "this machine's CPU does not reproduce the wide corpus's pin"); for m in members() { if !m.has_binary64() { assert!(matches!(m.sv(), Err(OpenClError::Unsupported(_))), "{}", m.key()); @@ -63,6 +79,12 @@ fn s0_the_state_vector_is_the_cpus_bit_for_bit() { 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))); + // The wider kernels, on their own corpus: bit-exact, or said not to + // be (such a member is placed only for gates on up to three qubits). + let w = sv.certify_wide(SV_WIDE).unwrap_or_else(|e| panic!("{}: {e}", m.key())); + assert_eq!(w.cases, SV_WIDE.cases as u64); + let exact = wide_exact(&w); + report(&format!("opencl sv wide certified: key={} member={} kernel={} implementation={} parity={} cases={} digest={}", m.key(), m.id(), w.kernel, w.implementation, if exact { "bit-exact" } else { "not-bit-exact" }, w.cases, hex(&w.corpus))); } } @@ -116,3 +138,38 @@ fn s2_mutants_are_caught() { report(&format!("opencl sv mutants: key={} killed {killed} of {}", m.key(), MUTANTS.len())); } } + +/// Ground truth beyond the pinned corpus, on the resident path: random +/// circuits at every fusion width the kernels take (0 to 6), run from +/// `|0…0⟩` and, as a partition runs, from a state given (the CPU's state after +/// the first half of the gates), each the CPU's state bit for bit. +#[test] +fn s3_random_circuits_match_the_cpu_from_zero_and_from_a_state() { + let same = |a: &[f64], b: &[f64]| a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits()); + for m in members().into_iter().filter(|m| m.has_binary64()) { + let sv = m.sv().unwrap(); + let wide = wide_exact(&sv.certify_wide(SV_WIDE).unwrap()); + let mut runs = 0; + for case in sv64::random_cases(2026, 70, 16, 60) { + let launches = sv64::plan(case.n, &case.gates, case.width).unwrap(); + // A member whose wider kernels did not certify is never placed + // for them: its narrow kernels alone are held to the CPU. + if !wide && launches.iter().any(|l| sv64::launch_bits(l).len() as u32 > sv64::NARROW) { + continue; + } + let mut cpu = StateVector::zero(case.n).unwrap(); + cpu.run(&case.gates, case.width).unwrap(); + let (re, im) = sv.run_from_zero(case.n, &launches).unwrap_or_else(|e| panic!("{}: {e}", m.key())); + assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: from zero, n={} width={}", m.key(), case.n, case.width); + let half = case.gates.len() / 2; + let mut cpu = StateVector::zero(case.n).unwrap(); + cpu.run(&case.gates[..half], case.width).unwrap(); + let rest = sv64::plan(case.n, &case.gates[half..], case.width).unwrap(); + let (re, im) = sv.run_launches(&cpu.re, &cpu.im, &rest).unwrap(); + cpu.run(&case.gates[half..], case.width).unwrap(); + assert!(same(&re, &cpu.re) && same(&im, &cpu.im), "{}: from a state, n={} width={}", m.key(), case.n, case.width); + runs += 2; + } + report(&format!("opencl sv random: key={} {runs} resident runs on {} match the CPU bit for bit", m.key(), if wide { "every kernel" } else { "the narrow kernels" })); + } +} diff --git a/wai/wai-quantum/views/manifest.json b/wai/wai-quantum/views/manifest.json index 995fbf707..39ddb8e67 100644 --- a/wai/wai-quantum/views/manifest.json +++ b/wai/wai-quantum/views/manifest.json @@ -12,11 +12,11 @@ "engine_js": 401202 }, "sha256": { - "page": "8bf0b0cfd064a5fb53fedbe34d15bd2954e756fdf985452dbb08c9e5722311ff", + "page": "192789a4c036869b0b04b692232271661aa292b61857f19663f7967eda5d3362", "script": "152484a251cd06eea4e65c45f6e6b8810be8d0932ec4dcdf3ae3cb4131e1f036", "styles": "11ebcd4416c68e9eaf1156ca6f7c92b9a96744895ed13c377eba98fdd8dca9a6", - "engine_wasm": "ba6e11c8c0a256723b826be4167a973cf8bc82193e73d5205455e5d3454fa3f1", - "engine_js": "a7e3cf66d42fce8af4afc4395ebdacc76831a1ab54f826054ce9fa5f8df16f34" + "engine_wasm": "d2f0c301fdf52373581e1639599d93431c32e47b7a5090b246feb165dbe2803e", + "engine_js": "e5b10aeb5f6adcf291b463f97557cfa004a67e3251ec28322db44db18daf742f" }, "sources": { "wai/quantum-studio/build-views.mjs": "da5b161829a990b3d1bb4c1b3ec7c8f254ff7b5078476156e504ae322bc4f010", diff --git a/wai/wai-quantum/views/wai-quantum-views.html b/wai/wai-quantum/views/wai-quantum-views.html index 9a412b916..4254f2428 100644 --- a/wai/wai-quantum/views/wai-quantum-views.html +++ b/wai/wai-quantum/views/wai-quantum-views.html @@ -11,7 +11,7 @@
-