open-standards/.github/workflows/wai-quantum-pulse.yml
Transaction Science 888cc3ba20 CI: a check holds every workflow to the one pinned wasmtime and both its settings
The install steps' comment said every WAI workflow that runs wasmtime pins one
version and passes both determinism settings, but nothing enforced it: a step
that ran a stock or differently pinned wasmtime would have merged green.
scripts/check-wasmtime-pin.py now fails, in the repository checks, any workflow
that

1. downloads a wasmtime release other than 50.0.1, or checks it against any
   SHA-256 but that archive's;
2. installs wasmtime another way (cargo install, the install script, an action);
3. runs wasmtime (run, serve, compile, or a module given to it) or names it as
   a CARGO_TARGET_*_RUNNER without -W nan-canonicalization=y and
   -W relaxed-simd-deterministic=y.

It also fails when it finds no download or no run, so a check that looked in
the wrong place cannot pass. On this tree it finds the downloads and runs it
should. Its self-test (scripts/check-wasmtime-pin-selftest.sh) passes a scratch
workflow shaped like these and fails each way of breaking it that the
self-test lists, matching the reason in the output.

The comment's relaxed_madd claim was measured on arm64 only. It now rests on
both architectures, measured on an arm64 macOS host and an x86_64 Linux host
with a small probe (wasmtime 50.0.1's release for the host, checked against
the digest the release lists):

    host Darwin arm64; wasmtime 50.0.1 (c307ec81d 2026-10-29)
    f32.add(bits 0x7fa00001, 1.0)                     [no flags]: 0x7fe00001
    f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [no flags]: 0x33800000
    f32.add(bits 0x7fa00001, 1.0)                     [both flags]: 0x7fc00000
    f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [both flags]: 0x33800000
    an unknown -W option is refused: error: failed to parse -W / --wasm option:
      unknown option: no-such-option

    host Linux x86_64; wasmtime 50.0.1 (c307ec81d 2026-10-29)
    (the same five lines, bit for bit)

0x33800000 is 2^-24, the fused result; the unfused fadd(fmul) gives 0. So the
NaN setting takes effect on both, and relaxed_madd is fused on both with and
without the relaxed setting. That matches 50.0.1's source (the
wasmtime-internal-cranelift crate, read from the registry): the translator
lowers F32x4/F64x2RelaxedMadd through fma_f32x4/fma_f64x2
(src/translate/code_translator.rs L2530-2539), which emit a native fma on a
host that has one, a fused library call under relaxed-simd-deterministic on a
host that has none, and the unfused fmul and fadd only on a host with no FMA
and the setting off (src/func_environ.rs L5907-5925). The setting is what
makes the result fused on every host. And since an unknown -W option stops
wasmtime, a run that exits 0 with the two flags has parsed them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 12:28:47 -05:00

209 lines
11 KiB
YAML

name: wai-quantum pulse and dynamics
# Pulse programs (wai/wai-quantum): OpenPulse read into a schedule on frames
# (qasm::pulse), the pulse simulator that integrates it under a Lindblad
# master equation (quantum_pulse), and pulse runs as jobs, their schedules
# and their checks (engine::pulse).
#
# The simulator is held to closed forms within the error it states (a Rabi
# turn, a quarter turn of phase, a detuned drive, T1 decay, T2 dephasing),
# and its result is the same bits on every platform: a test pins the final
# density matrix's digest, and this workflow runs it on x86_64 Linux, arm64
# Linux, arm64 macOS and wasm32-wasip2 (wasmtime). The browser build runs a
# pulse job to the same pinned digest (wai-web-wasm.yml, wai-web's wasm32
# tests).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths:
- 'wai/wai-quantum/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-pulse.yml'
pull_request:
paths:
- 'wai/wai-quantum/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-pulse.yml'
jobs:
relevant:
name: does this change touch it
runs-on: linux
outputs:
run: ${{ steps.gate.outputs.run }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- id: gate
run: |
set -euo pipefail
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-pulse.yml \
"${{ github.event.pull_request.base.sha }}" \
"${{ github.event.pull_request.head.sha }}")
echo "this change touches the pulse layer: $r"
echo "run=$r" >> "$GITHUB_OUTPUT"
native:
name: reader, simulator and pulse jobs · linux, release
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: linux
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- name: Install Rust 1.98.0
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# A module filter is satisfied by any one test of the module, so the
# count is pinned and each test is required by name to have run and passed.
- name: The pulse reader, the simulator and pulse jobs
shell: bash
working-directory: wai/wai-quantum
run: |
set -euo pipefail
cargo test --locked --release --features full --lib -- qasm::pulse:: quantum_pulse:: engine::pulse:: 2>&1 | tee "$RUNNER_TEMP/pulse.log"
grep -qE '^test result: ok\. 26 passed; 0 failed' "$RUNNER_TEMP/pulse.log" || { echo "::error::the pulse tests did not all pass (26 expected)"; exit 1; }
for t in qasm::pulse::tests::a_program_reads_to_its_schedule_on_frames \
qasm::pulse::tests::barriers_align_frames_and_a_gate_starts_its_frames_together \
qasm::pulse::tests::phase_and_frequency_follow_the_frame_and_the_carrier_runs_on \
qasm::pulse::tests::each_template_samples_as_its_formula_says \
qasm::pulse::tests::a_parametric_calibration_binds_its_angle_and_a_fixed_one_wins \
qasm::pulse::tests::durations_in_nanoseconds_must_be_whole_samples \
qasm::pulse::tests::ports_map_by_name_and_by_the_options \
qasm::pulse::tests::what_the_pulse_reader_does_not_take_is_refused_with_its_code_and_place \
qasm::pulse::tests::the_schedule_writes_as_json_with_every_instruction \
quantum_pulse::tests::a_resonant_drive_turns_the_qubit_as_rabi_says \
quantum_pulse::tests::a_quarter_turn_of_phase_turns_the_axis_to_minus_y \
quantum_pulse::tests::a_detuned_drive_turns_less_and_faster \
quantum_pulse::tests::relaxation_and_dephasing_decay_as_t1_and_t2_say \
quantum_pulse::tests::the_same_schedule_gives_the_same_bits \
quantum_pulse::tests::simultaneous_captures_give_the_joint_reading \
quantum_pulse::tests::three_levels_show_leakage_and_keep_the_trace \
quantum_pulse::tests::the_bound_is_stated_and_a_step_past_it_is_refused \
quantum_pulse::tests::what_the_model_does_not_hold_is_refused \
quantum_pulse::tests::a_detuned_play_split_in_two_is_the_same_play \
engine::pulse::tests::a_pulse_job_runs_end_to_end_and_its_answer_follows_its_schemas \
engine::pulse::tests::a_target_gives_the_qubits_their_frequency_and_times_and_its_hash_is_bound \
engine::pulse::tests::what_a_pulse_job_cannot_be_is_refused_with_its_code \
engine::pulse::tests::a_pulse_job_is_checked_by_running_it_again \
engine::pulse::tests::a_schedule_is_drawn_with_its_digests \
engine::pulse::tests::the_request_schema_holds_the_options_the_engine_reads \
engine::pulse::tests::a_pulse_job_is_the_same_bits_on_every_platform; do
grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/pulse.log" || { echo "::error::$t did not run and pass"; exit 1; }
done
# Without warnings: a feature gate that leaves code unreachable or a
# value unused in one feature set shows here.
- name: Each feature set that holds the pulse layer builds, without warnings
shell: bash
working-directory: wai/wai-quantum
env:
RUSTFLAGS: -D warnings
run: |
set -euo pipefail
cargo check --locked --no-default-features --features quantum_pulse --lib
cargo check --locked --no-default-features --features quantum_engine --lib
cargo check --locked --no-default-features --features quantum_engine,quantum_receipt --lib
# The same bits on other processors and operating systems: the pinned
# digests of the simulator's and a pulse job's final density matrix.
bits:
name: the same bits · ${{ matrix.label }}
needs: relevant
if: needs.relevant.outputs.run == 'true'
strategy:
fail-fast: false
matrix:
label: [linux-arm64, macos]
runs-on: ${{ matrix.label }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
# launchd's PATH omits cargo on the macOS host runner.
- name: Install Rust 1.98.0
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: The pinned digests, in a debug build and a release build
shell: bash
working-directory: wai/wai-quantum
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
for profile in "" "--release"; do
nice -n 10 cargo test --locked $profile --no-default-features --features quantum_engine,quantum_receipt --lib -- \
quantum_pulse::tests::the_same_schedule_gives_the_same_bits \
engine::pulse::tests::a_pulse_job_is_the_same_bits_on_every_platform 2>&1 | tee "$RUNNER_TEMP/bits.log"
grep -qE '^test result: ok\. 2 passed; 0 failed' "$RUNNER_TEMP/bits.log" || { echo "::error::the pinned bits differ here ($(uname -sm), ${profile:-debug})"; exit 1; }
done
wasip2:
name: reader, simulator and pulse jobs under wasm32-wasip2 (wasmtime)
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: linux
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- name: Install Rust 1.98.0 with the wasm32-wasip2 target
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
rustup target add --toolchain 1.98.0 wasm32-wasip2
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# The release binary, pinned by version and SHA-256, rather than a build from source.
# Every WAI workflow that runs wasmtime pins this one version, and every run passes
# -W nan-canonicalization=y -W relaxed-simd-deterministic=y: every NaN a float
# instruction makes is the canonical one, and each relaxed SIMD instruction has one
# behaviour on every host. A stock wasmtime leaves both off. The fixed relaxed
# behaviour is the engine's own: on 50.0.1 relaxed_madd is fused (measured on arm64 and
# x86_64), where WebAssembly's deterministic profile prescribes the unfused result.
# The repository checks (scripts/check-wasmtime-pin.py) fail a workflow that pins another
# release or runs wasmtime without both settings.
- name: Install wasmtime 50.0.1
shell: bash
run: |
set -euo pipefail
[ "$(uname -m)" = x86_64 ] || { echo "::error::the pinned wasmtime archive is x86_64; this host is $(uname -m)"; exit 1; }
if ! command -v xz >/dev/null 2>&1; then
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq xz-utils
fi
v=50.0.1
f="wasmtime-v$v-x86_64-linux.tar.xz"
curl --proto '=https' --tlsv1.2 -sSfL -o "$RUNNER_TEMP/$f" \
"https://github.com/bytecodealliance/wasmtime/releases/download/v$v/$f"
echo "685466455dd42a14774a86c36c8f2e45cbb9bd6d92a850139315df60a75498a5 $RUNNER_TEMP/$f" | sha256sum -c -
tar -xJf "$RUNNER_TEMP/$f" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/wasmtime-v$v-x86_64-linux" >> "$GITHUB_PATH"
# A debug build, so integer overflow panics instead of wrapping.
- name: The pulse tests under wasmtime, to the pinned bits
shell: bash
working-directory: wai/wai-quantum
env:
CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime run -W nan-canonicalization=y -W relaxed-simd-deterministic=y
run: |
set -euo pipefail
cargo test --locked --target wasm32-wasip2 --features full --lib -- qasm::pulse:: quantum_pulse:: engine::pulse:: 2>&1 | tee "$RUNNER_TEMP/wasip2.log"
grep -qE '^test result: ok\. 26 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the pulse tests did not all pass under wasip2 (26 expected)"; exit 1; }