diff --git a/.github/workflows/wai-quantum-landscape-fpga.yml b/.github/workflows/wai-quantum-landscape-fpga.yml index 1fa41fddb..1715bc5f9 100644 --- a/.github/workflows/wai-quantum-landscape-fpga.yml +++ b/.github/workflows/wai-quantum-landscape-fpga.yml @@ -93,9 +93,9 @@ jobs: run: | set -euo pipefail cargo run --locked --release -- selftest --scale 0.05 --seeds 5 | tee "$RUNNER_TEMP/selftest.log" - grep -q 'certified by the host on the conformance set: settle ' "$RUNNER_TEMP/selftest.log" - grep -qE '^settle cases the engine holds: ([0-9]+)/\1 bit for bit' "$RUNNER_TEMP/selftest.log" - grep -qE '^chains the engine holds: ([0-9]+)/\1 bit for bit' "$RUNNER_TEMP/selftest.log" + grep -q 'certified by the host on the conformance set, within its declared capacity: settle ' "$RUNNER_TEMP/selftest.log" + grep -qE '^settle cases within its declared capacity: ([0-9]+)/\1 bit for bit' "$RUNNER_TEMP/selftest.log" + grep -qE '^chains within its declared capacity: ([0-9]+)/\1 bit for bit' "$RUNNER_TEMP/selftest.log" grep -qE '^[0-9]+ checks, 0 failed$' "$RUNNER_TEMP/selftest.log" - name: A job on the model, certified and sealed, with the reference's answer shell: bash diff --git a/.github/workflows/wai-quantum-landscape.yml b/.github/workflows/wai-quantum-landscape.yml index 561456f18..553044042 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -125,8 +125,18 @@ jobs: run: | set -euo pipefail cargo test --locked --release --features full --lib -- quantum_landscape:: 2>&1 | tee "$RUNNER_TEMP/landscape.log" - grep -qE '^test result: ok\. 95 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (95 expected)"; exit 1; } - for t in quantum_landscape::classical::tests::a_solver_keeps_to_its_budget_and_repeats_itself \ + grep -qE '^test result: ok\. 106 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (106 expected)"; exit 1; } + for t in quantum_landscape::capacity::tests::a_capacity_declines_exactly_what_is_past_it \ + quantum_landscape::capacity::tests::a_capacity_is_stated_in_numbers_and_in_words \ + quantum_landscape::capacity::tests::the_floor_is_the_core_set_s_own_reach \ + quantum_landscape::capacity::tests::every_declared_number_has_vectors_at_its_edge \ + quantum_landscape::capacity::tests::a_table_and_a_span_are_reached_exactly \ + quantum_landscape::capacity::tests::generated_vectors_cost_what_the_spec_says \ + quantum_landscape::capacity::tests::with_no_capacity_the_generated_set_reaches_what_a_job_can \ + quantum_landscape::capacity::tests::long_settles_bands_cells_and_spans_are_generated \ + quantum_landscape::conformance::tests::the_likelihood_ratio_bound_holds_exactly \ + quantum_landscape::capacity::tests::the_hottest_sweep_has_the_widest_uncertain_span \ + quantum_landscape::classical::tests::a_solver_keeps_to_its_budget_and_repeats_itself \ quantum_landscape::classical::tests::each_solver_finds_the_exact_lowest_on_small_instances_with_a_generous_budget \ quantum_landscape::classical::tests::with_no_budget_a_solver_misses \ quantum_landscape::conformance::tests::a_state_s_index_is_its_digits \ @@ -143,7 +153,7 @@ jobs: quantum_landscape::job::tests::a_member_that_fails_a_job_passes_it_on \ quantum_landscape::job::tests::an_uncertified_kernel_is_never_placed \ quantum_landscape::job::tests::every_answer_says_who_ran_it_at_what_parity \ - quantum_landscape::job::tests::a_member_may_decline_only_what_does_not_fit_32_bits \ + quantum_landscape::job::tests::a_member_is_held_to_every_vector_within_its_declared_capacity \ quantum_landscape::job::tests::a_sealed_answer_verifies_and_follows_its_schema \ quantum_landscape::job::tests::a_seed_keeps_all_64_bits \ quantum_landscape::job::tests::a_temperature_in_text_keeps_every_bit \ @@ -190,6 +200,7 @@ jobs: quantum_landscape::run::tests::a_member_s_claim_of_parity_is_not_taken_on_its_word \ quantum_landscape::run::tests::a_run_s_reading_is_its_calls_added_up \ quantum_landscape::run::tests::a_run_too_big_is_refused_before_it_starts \ + quantum_landscape::run::tests::the_audit_s_threads_are_capped_over_the_process_and_run_low \ quantum_landscape::run::tests::a_sweep_keeps_the_budget_and_the_cold_row_of_a_minimize_runs_once \ quantum_landscape::run::tests::the_same_seed_gives_the_same_answer_and_another_seed_another_run \ quantum_landscape::run::tests::the_temperature_picks_the_mode_and_the_rate \ @@ -229,7 +240,7 @@ jobs: run: | set -euo pipefail cargo test --locked --release --features quantum_landscape --test landscape_conformance 2>&1 | tee "$RUNNER_TEMP/conformance.log" - grep -qE '^test result: ok\. 12 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (12 expected)"; exit 1; } + grep -qE '^test result: ok\. 54 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (54 expected)"; exit 1; } # With the module's feature alone: a temperature given as a JSON number is read to the nearest double. - name: Numbers read to the nearest double shell: bash @@ -304,9 +315,25 @@ jobs: quantum_landscape::job::tests::every_platform_gives_the_same_bits \ quantum_landscape::settle::tests::four_recall_vectors_are_pinned 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; } - nice -n 10 cargo test --locked $profile --no-default-features --features quantum_landscape --test landscape_conformance 2>&1 | tee "$RUNNER_TEMP/vectors.log" - grep -qE '^test result: ok\. 12 passed; 0 failed' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } + # The reviews' probes, the audit's tests and the battery's grading test certification, + # the audit and the battery, not the bits, and run in the conformance job. + nice -n 10 cargo test --locked $profile --no-default-features --features quantum_landscape --test landscape_conformance -- --skip review --skip audit_ --skip the_battery_tells --skip a_member_is_graded 2>&1 | tee "$RUNNER_TEMP/vectors.log" + grep -qE '^test result: ok\. 10 passed; 0 failed; 0 ignored; 0 measured; 45 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } done + # The store of demotions and the audit's threads rest on this platform's file system and + # scheduler (chflags, sticky folders, flock, setpriority): their tests run here too. + - name: The store of demotions and the audit's threads on this platform + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + export PATH="$HOME/.cargo/bin:$PATH" + nice -n 10 cargo test --locked --release --no-default-features --features quantum_landscape --lib -- \ + quantum_landscape::run::tests::the_audit_s_threads_are_capped_over_the_process_and_run_low 2>&1 | tee "$RUNNER_TEMP/threads.log" + grep -qE '^test result: ok\. 1 passed; 0 failed' "$RUNNER_TEMP/threads.log" || { echo "::error::the audit's threads are not held here ($(uname -sm))"; exit 1; } + nice -n 10 cargo test --locked --release --no-default-features --features quantum_landscape --test landscape_conformance -- \ + review4_the_store review4_stores review5_the_store review6_the_store review6_a_demotion review6_a_store review6_the_probe review6_a_link review6_a_hard review6_a_killed 2>&1 | tee "$RUNNER_TEMP/store.log" + grep -qE '^test result: ok\. 11 passed; 0 failed' "$RUNNER_TEMP/store.log" || { echo "::error::the store of demotions is not held here ($(uname -sm))"; exit 1; } wasip2: name: the module under wasm32-wasip2 (wasmtime) @@ -354,7 +381,7 @@ jobs: run: | set -euo pipefail cargo test --locked --target wasm32-wasip2 --no-default-features --features quantum_engine,quantum_receipt --lib -- quantum_landscape:: 2>&1 | tee "$RUNNER_TEMP/wasip2.log" - grep -qE '^test result: ok\. 94 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (94 expected)"; exit 1; } + grep -qE '^test result: ok\. 104 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (104 expected)"; exit 1; } # The browser build's answers against the command line's, byte for byte, in Node. parity: diff --git a/wai/changes/2026-11-07-landscape-declared-capacity.md b/wai/changes/2026-11-07-landscape-declared-capacity.md new file mode 100644 index 000000000..d456905c2 --- /dev/null +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -0,0 +1,21 @@ +### wai-quantum: a member of an energy-landscape job declares its capacity, and is held to everything within it + +- **A declared capacity** (`extensions/quantum-landscape.md` §5; `quantum_landscape::capacity`): a member states once, as fixed numbers, what its kernels hold: cells and couplings; on each side of temperature 0, its largest field, field bound and cost; above 0, the cut points a sweep or the span of uncertain fields it holds; the sweeps of a run; and the chains it takes a call, which is a batch, not a limit: the host splits a job, and certification splits every vector, into calls of that many. The host declines for it exactly the jobs past those numbers, by a function of the job and the numbers alone, so no member declines by choice. `Member::capacity` takes the place of `Member::takes`. +- **Graded within it, and at its edges.** `member::certify` reads the capacity once, and runs every vector and battery workload within it, and, for each number declared below what a job can reach, vectors generated at exactly that number (`Capacity::edges`, the reference their oracle): so a limit set between the set's fixed points is held at the limit, not at the nearest vector under it. The grade holds within the capacity: `member.capacity` in every answer from a member that declares one (none from the reference, whose answers keep their bytes), a sentence in the label, and the number a job is past in its refusal. A member that refuses or fails a case within its capacity loses the grade. +- **A floor.** A capacity under the least one that takes every case of the core set (32 cells, fields to ±40, a field bound of 41, a cost of 8, a run of 5,000 sweeps, and the table and span those cases need) is not certified at all. The core set holds a long schedule, 5,000 sweeps on 32 cells within the floor, so that no capacity a member may declare leaves it untested across a device's windows; the full set adds 200,000 sweeps of two cells, across a window by work. +- The GPU members declare their real limits (WebGPU a field bound of 2³¹ − 1 on both sides of temperature 0); the CPU reference declares none. The FPGA reference design declares 64 cells and, above temperature 0, fields to ±524,287, a cost to 1,048,575 and an uncertain span of 8,192, and the host certifies its register-level model `bit-exact` within that capacity on the set as it stands; a test holds the declared numbers to the engine's own limits case by case. +- An adversarial check found five ways to be certified `bit-exact` past what the vectors had tested: a window fault at the floor itself, a batch size that skipped vectors, limits set between the vectors' points, a capacity read more than once, and grade text without the capacity. Each of its probes is kept as a test, and every fault test also runs declaring the most hostile capacity it may: the least, at or above the floor, that still takes a job the fault gets wrong. +- A second adversarial check found ten more fault classes that a finite set missed: cells past a couplings limit, the last sweep of an annealing job at 0 on large fields, workgroup and warp remainders, rows longer than 32, a field and a cost past a lane together, fields at a lane's wrap points, draw indices past 24 bits and past a signed 32-bit index, tables and spans short of the declared number, and nothing past 1,000 cells with no capacity at all. So certification now generates vectors by rule for every capacity (`Capacity::generate`, §7.1): every cell count at and beside a power of two to the most a workload can hold, 4,096 with none; every call size to 32 chains, each band of a wavefront and a whole batch; the longest row; fields at the wrap points of 16, 24 and 32-bit lanes, alone, through couplings and beside the largest cost, at 0, above it and ending at 0; a corner with every number at its most; and a run past draw index 2²⁴; a narrower index past that is left to the audit, as a run past 2³¹ would cost minutes on a device. The edges reach a declared table and span exactly, and the battery gets workloads at a declared cell count, couplings and schedule length, reading a few blocks of many (`BatteryCase::read`). The cost is bounded and stated in §7.1. +- **The host's audit** (§7.3): every answer from a kernel graded `bit-exact` has the last chain of each call and one chain of each run in it, drawn with a key of the host's own, run again on the reference, and every settle within a work budget (one in 8 past it). At the first difference the member is demoted to `uncertified` for both kernels, the job is answered from the reference, and later answers say so (`member.demoted`). +- The reference's sampler skips a run of sweeps at 0 after a fixed point, as they cannot move it; its bits are unchanged. +- A decline for capacity now reads "The host declined this job for …", not "the member refused or failed", and the label says the host runs the member on no job past its capacity; a member run by name without certification claims no grade. +- **Breaking:** `api:rust:wai_quantum::quantum_landscape::member::Member::takes`: removed; a member declares a capacity instead (`Member::capacity`). +- **Breaking:** `api:rust:wai_quantum::quantum_landscape::conformance::MUST_TAKE_FIELD_BOUND`: removed; what a member must take is its declared capacity, at or above the floor. +- **Breaking:** `api:rust:wai_quantum::quantum_landscape::job::Prepared::sampling`: removed; `Prepared::schedules` and `Prepared::settles` hold what the job runs. +- **Breaking:** `api:rust:wai_quantum::quantum_landscape::conformance::BatteryCase::read`: a new field, the blocks a battery workload counts; empty counts every block, as before. +- **Breaking:** `api:rust:impl core::marker::Freeze for wai_quantum::quantum_landscape::member::Certified`: a certified member now holds the audit's demotion, so it is no longer `Freeze`. +- A third adversarial check found the audit resting on what a member reports or can predict, and gaps in generation. Now: the audit runs one chain of every seed's run, as well as the last of every call, in replay, sweep and compare; it decides a settle's re-run from the work the host asked for, never the sweeps the member reports, and compares the reference's settle as far as the budget goes; it draws with a host key made at start and a count every draw moves on, and each audited answer records what it checked (`audit`). A demotion is kept by identity (`Member::fingerprint`, `member::Demotions`), in a file where the host names one (the FPGA host does), and certifying again or declaring another capacity does not clear it. A kernel graded `statistical`, which the audit cannot hold to bits, is placed only where the job asks for it (`"parity": "statistical"`). Generation: lane vectors are packed to what the capacity takes; ladder sizes in a remainder band are quenched too; a wave settles down as many cells as a workload holds for every capacity; the corner moves cells at 0 and reaches the field bound through as many couplings as a cell can have; the battery's workload of alike cells holds a declared cell count exactly. The bound on what generation costs holds over random capacities, and the text states the bound the test holds. +- The CUDA kernel's mark on a field past its table can be overwritten by a later sweep; the vectors and the audit catch such an answer, and the text no longer says the mark does. +- A fourth adversarial check found faults past the generated set and the audit. Now: ladder sizes in a remainder band are also run hot and then at 0 for one sweep, an engine's final sweep; the wave comes to rest within a declared sweep limit instead of being dropped, and the widest wave is also stopped at its bound while it still moves, past 2⁹, 2¹⁰ and 2¹¹ sweeps and one sweep before it would rest; the battery's alike cells follow a declared cell count to 4,096; the uncertain span is reached where a table limit is the narrower, and where no span is declared; and the workloads a capacity adds to the battery hold each block at a chance of 10⁻⁸ of failing a correct sampler, and read few blocks (§7.2). The audit draws one chain of each run in every call, so a member that times the host learns nothing of the next call, and runs its checks across the host's cores. An audited answer no longer carries a key digest (`audit.key`), which nothing could open. The store of demotions reads its file at each certification and merges each demotion under a lock, so hosts and jobs sharing it keep one another's; it fails closed, grading no kernel `bit-exact`, where the file cannot be read, parsed or written, or the FPGA host has no `HOME` (`Demotions::get` and `record` return what failed; `Demotions::broken`, `keep_demotions`). `"parity": null` is the default, as for every optional field. +- A fifth adversarial check found the store failing open where its folder stopped taking writes after first use, and gaps in generation and the battery. Now: each certification writes, syncs and removes a file as large as the store's next rewrite, so a folder no longer written to, a full disk or a file-size limit grades nothing `bit-exact`; an empty `WAI_LANDSCAPE_DEMOTIONS` is as if unset and a path naming no file is a broken store; a linked store is its target's, and a link to nothing fails closed; the store's lock is waited for at most 5 s (`member::STORE_LOCK_WAIT`); a rewrite keeps the file's mode and removes files a crashed writer left. The wave is stopped one sweep before its rest, and a settle of two-cell units that moves for `2n − 10` sweeps is stopped past 2⁹ to 2¹² sweeps and at its last moving sweep. The final sweep at 0 after sweeps above 0 runs at odd, even and middle remainders of a partial warp and workgroup, after two, three and a cold sweep above 0. The battery's alike cells reach every cell a workload can hold (4,096 with none), every cell read, with a second count at a partial group's most; the workloads a capacity adds run in calls ending in a partial warp, and are held by a likelihood-ratio bound that holds whatever the law (`conformance::likelihood_ratio`, `likelihood_ratio_critical`). The audit's re-runs run on the job's own thread and on at most a quarter of the host's cores more, 4 at most over the process, at a low priority (`run::audit_threads`), and §7.3 states the audit's cost as work, with wall time measured over five runs a device. +- A sixth adversarial check found the store's probe proving less than a demotion needs, and gaps in generation and the battery's stated power. Now: each certification rewrites the store's file as a demotion does, as large as the largest rewrite one can make (a reason is kept to its first 1,024 bytes, `member::STORE_REASON_BYTES`), and renames it over the file, so a file another user owns in a sticky folder, one marked immutable or append-only, a store written compact and a long identity grade nothing `bit-exact`; a demotion is kept at once in a file of its own beside the store, which every certification reads, and is merged under the lock, so a busy lock no longer drops it; the store's temporary files are matched exactly and removed only once unchanged for 10 s and held by no writer (`member::STORE_STALE_AFTER`), so a certification removes what a killed one left and no store removes another's; a link in a sticky folder anyone may write is not followed, no file of the store's is opened through a link, and a file with two names is refused. Generation stops units of three cells, which move to sweep 14,956 on 4,095 cells, past 2⁹ to 2¹³ sweeps and at their last move (units of two where couplings are scarce); the claim of a longest-moving settle is gone; two-cycles end at sweep 449, the first of a later batch, and 4,101. The battery's added workloads run in calls ending in every partial warp and workgroup; §7.2 states what the likelihood-ratio bound misses (a law some 10% off, a count of cells the battery does not run), and every answer from a kernel graded `statistical` says so (`member::STATISTICAL_GRADE`). The audit's docs say the job's own thread runs its share at the job's priority, an audit thread that cannot be lowered says so, and a test run on Linux and macOS holds the threads' number and priority. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 75dbab1d4..2f9f897d3 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -55,8 +55,11 @@ A job is one JSON request and one operation. The request: | `temperature` | optional | the starting temperature, over the program's own; 0 is the deterministic mode. A JSON number, which a reader MUST read to the nearest double, or a string `[0-9]+(.[0-9]+)?` of at most 40 characters, read to the nearest double: no sign, no exponent, no other form | | `seeds` | `sweep`, `compare`, `replay` | how many seeds, from `seed` up, 1 to 100,000; default 20 | | `temperatures` | `sweep` | 1 to 64 of the same; by default 0, 0.25, 0.5, 1, 2, 4 and 8, with the default start (§4.3) and the largest row of couplings added where they are not already there, in increasing order | +| `parity` | optional, default `bit-exact` | `bit-exact`, or `statistical`, which lets a placing host give the job to a kernel graded `statistical` too (§5.1) | | `protocol` | optional | if given, it MUST be this protocol's name | +An optional field given as `null` is absent. + The operations are `run`, `sweep` (the program at each temperature, with the same budget at each), `compare` (§6), `replay` (many seeds, one line each) and `examples`. @@ -181,7 +184,7 @@ A `run` answers with these fields (the schema has them all): |---|---| | `protocol` | `wai.quantum.landscape/1` | | `label` | what kind of run this is, in the member's own words (§5.3) | -| `member` | what ran it: `id`, `class`, `kernel` and `parity` (§5) | +| `member` | what ran it: `id`, `class`, `kernel`, `parity`, the `capacity` that parity holds within, absent where none is declared (§5), and `demoted`, why the host holds it at `uncertified`, where it does (§7.3) | | `program_digest` | BLAKE3 of the program's bytes | | `seed`, `mode`, `temperature`, `temperature_from`, `restarts`, `sweeps` | how it ran; `seed` is a decimal string; `mode` is `settle`, `quench` or `sample` | | `state`, `energy` | the state of lowest energy found, and its energy from the program | @@ -195,12 +198,15 @@ A `run` answers with these fields (the schema has them all): | `joules` | §4.4 | | `state_hash`, `replay` | §3.2 | | `refusals` | each member that a placing host passed over, with why (§5); absent where there was none | +| `audit` | what the host's audit ran again on the reference, for an answer from a kernel graded `bit-exact`: `checked`, each run's `seed` with the `chains` of it checked, or how much of a `settle` (§7.3); absent where nothing was audited | A `replay` answers with `protocol`, `label`, `member` (`id`, `class`, `kernel`, -`parity`), `seeds` and `lines`. A `sweep` and a `compare` give `member` as `id`, -`class` and `kernels`, each kernel the job used with the parity the host found for it; +`parity`, `capacity`, `demoted`), `seeds` and `lines`. A `sweep` and a `compare` give +`member` as `id`, `class`, `capacity`, `demoted` and `kernels`, each kernel the job used +with the parity the host found for it; each row of a `sweep` gives its own `kernel` and `parity`, and each of a `compare` the -member's `parity`, null for a classical method. Each carries `refusals` as a `run` does. +member's `parity`, null for a classical method. Each carries `refusals` and `audit` as a +`run` does. An accumulate is one read of one coupling into one cell's field, and is the unit of work. The accumulates of a sweep are the non-zero entries of `W`, counting each @@ -399,17 +405,65 @@ batch of chains at a time (§2.0). What a member gives back, for a settle or each chain: the state, the sweeps, how it ended, the accumulates and the draws. -**Declining.** Before anything runs, a member MAY decline a workload whose field bound -(§4.1) is above 2³¹ − 1, which a device with 32-bit lanes cannot hold, saying why in -words that are true of the workload. It MAY decline no other workload a job can give; -§2.0's limits keep every other need of a job, such as a sweep's table (§4.2), within -what any member can hold. A member may decline for one kernel and not the other: the -rule is the same for each. A host runs a member on no workload it declines, however the -member was reached. The reference declines nothing. +**Declared capacity.** Real hardware holds so many cells, fields so wide, a table so large. +A member declares what its kernels hold, once and before anything runs, as fixed numbers +(`member.capacity`, each of them optional, none declared being no limit): + +| number | what it bounds | +|---|---| +| `cells` | the cells of a workload | +| `couplings` | its couplings, each pair counted twice (`2P`) | +| `at_zero`: `field`, `field_bound`, `cost` | for a run with a sweep at temperature 0 (a settle, a quench, the end of a schedule): the largest `\|b_i\|`, field bound (§4.1) and cost | +| `above_zero`: `field`, `field_bound`, `cost` | the same, for a run with a sweep above temperature 0 | +| `table_entries` | above temperature 0: one sweep's cut points over the fields the cells can reach (§4.2) | +| `uncertain_span` | above temperature 0: the span, first to last, of the fields the cells can reach whose draw is uncertain at a sweep's temperature, where it is not the threshold's `(0, 0)`, `(0, 2⁵³)` or `(2⁵³, 2⁵³)`. Those fields at a temperature hold those at any lower one, so the hottest sweep's span is the widest, and it is the one held to the number | +| `sweeps` | the sweeps of one run: a settle's bound, a schedule's length | +| `chains_per_call` | the chains it takes in one call. Not a limit on a job: a host splits a job into calls of at most this many, and certification splits every vector the same way (§5.1), so no job goes elsewhere for it | + +A workload fits when every declared number but `chains_per_call` holds for it. Whether it does is a function of +the workload, its schedule and the declared numbers alone: the host works it out, and +declines for the member exactly the workloads that do not fit, saying which number each +is past. A member declines nothing by choice, job by job, and is run on no workload past +its capacity, however it was reached. A member that refuses or fails a workload within +its capacity has failed it. The reference declares no capacity. + +**The floor.** A member's declared numbers MUST be at least the floor's, or the host does +not certify it at all, so that no member is certified by declaring itself too small to +be held to anything. The floor is the least capacity that takes every case of the core +set (§7.1): the settle, chains and battery cases the set held before it grew to reach +large jobs. Its numbers are 32 cells and 992 couplings (the recall vector of 32 cells, +every pair coupled); on each side of temperature 0, fields to ±40, a field bound of 41 +and a cost of 8 (the wide-fields chains, run cooling and quenched at 0); 31 entries of +cut points a sweep (the dense chains of 16 cells, every pair coupled, at their hottest); +an uncertain span of 61 (the wide-fields chains at their hottest); and a run of 5,000 +sweeps (the core set's long schedule, §7.1). It declares no batch: any number of chains a +call is certified by splitting. A device that cannot hold these cannot be held to the +core set, and the grade would vouch for nothing. + +**Generated vectors.** A finite set samples each axis of a workload at a few points, +and a declared number takes vectors out of it. So the host also holds a member to vectors +it generates from the member's capacity, by the rules of §7.1, with the reference's bits +as their expected bits and the same on every host. Some are generated for every +capacity, the reference's none among them, so that a member is held across each axis up +to what its capacity lets a workload reach: every cell count at and beside a power of +two, every call size to 32 chains and each band of a wavefront past that, the longest +row, fields at each lane's wrap points, a corner with every number at its most, and a +run past a 24-bit draw index. Others +are generated for each number a member declares below what a job can reach (§2.0: 4096 +cells; 838,860 couplings, two for each ten bytes of the longest program; a field and a +cost of 2⁴⁰; a field bound of 2⁴⁰ + 4095; a table of twice the couplings and the cells; +a span of twice the field bound and one; 1,000,000 sweeps), at exactly that number, each +also within every other number declared. A table or a span is reached through couplings; +where the other declared numbers do not let a workload reach it, the vector reaches as +far as they do. Only vectors the capacity admits are run. So a grade is earned across a +capacity and at its edges, not only at the fixed vectors inside it. No finite set catches +every fault, so the host then audits every answer from a kernel it graded `bit-exact` +(§7.3). **Placing a job.** A host offered several members gives the job to the first that can -take it: one whose kernel is certified for every kernel the job runs (§5.1), that does -not decline the workload, and that gives an answer the host's checks keep. Each member +take it: one whose kernel is certified for every kernel the job runs (§5.1), within +whose capacity the workload fits at every schedule the job runs, and that gives an +answer the host's checks keep. Each member passed over, for any of those, is named under `refusals`, in the order offered, with why; with none, `refusals` is absent. A member that took the job and failed it, or whose answer the checks refused, is passed over the same way and the next is asked. @@ -439,11 +493,16 @@ took before it came to rest can be checked only by running it again: a `bit-exac certificate is what vouches for it. The reference implementation has three GPU members besides the CPU reference, each -certified by the host on its own run of the conformance set (informative): through WebGPU -(32-bit lanes; it declines a workload whose field bound is above 2³¹ − 1, holds a cost -above every field at its lanes' top, where it holds every cell at 0 as it does at its -own size, and its coloured kernel can be `statistical` at most), through CUDA and through -Metal (64-bit fields, so they take every workload). All three run the sampling kernel one +certified by the host on its own run of the conformance set within the capacity it +declares (informative): through WebGPU (a field bound of 2³¹ − 1 on both sides of +temperature 0, which its 32-bit lanes hold; cells and couplings within a 32nd of a +storage binding; 4,194,304 entries of cut points a sweep. It holds a cost above every +field at its lanes' top, where it holds every cell at 0 as it does at its own size, and +its coloured kernel can be `statistical` at most), and through CUDA and Metal (64-bit +fields, and 4,194,304 entries of cut points a sweep, which no job reaches). An FPGA board +(`wai/wai-quantum-landscape-fpga`) declares 64 cells and, above temperature 0, fields to +±524,287, a cost to 1,048,575 and an uncertain span of 8,192, its registers and its +table's window. All three run the sampling kernel one chain a thread, the cells in the reference's order, with the reference's draws and the host's cut points for the fields the cells can reach, a window of sweeps a dispatch, each window waited for before the next is sent; and give its bits. @@ -455,20 +514,28 @@ at most 64 characters in all. `cpu:reference` is the reference's, and no other m ### 5.1. Parity A member does not state its parity: the host finds it, by running the member's kernels -on the conformance set (§7), and an answer states what the host found for the kernel the -run used (`member.kernel`, `member.parity`). +on the conformance set (§7) within the member's declared capacity (§5) and at its edges, +and an answer +states what the host found for the kernel the run used (`member.kernel`, +`member.parity`) and the capacity it holds within (`member.capacity`, absent where none +is declared, so that the reference's answers are what they were). A parity is a statement about the jobs within that capacity, and no others. | parity | granted when | |---|---| | `reference` | to the host's own reference kernels, and to nothing else | -| `bit-exact` | settle: the member gives every `settle` vector it takes exactly. Sample: it gives every `chains` vector it takes exactly, each case's chains in one call. Either way it takes every vector §5 does not let it decline | -| `statistical` | sample: not `bit-exact`, and every workload of the `statistical` battery passes (§7.2) | +| `bit-exact` | settle: the member gives every `settle` vector within its capacity, and every one generated for it (§5), exactly. Sample: it gives every `chains` vector within its capacity and every one generated for it exactly, each case's chains in calls of as many as a host would give it at run time: the batch of §2.0, or `chains_per_call` where it declares fewer | +| `statistical` | sample: not `bit-exact`, and every workload of the `statistical` battery within its capacity, and every battery workload generated for it, passes (§7.2), in calls of the same size | | `uncertified` | anything else, or a member the host was asked to run by name without certifying it | -A member is held to every vector it takes, and declining a vector §5 does not let it -decline is failing that vector. The set holds vectors on both sides of the line, at a -field bound of exactly 2³¹ − 1 and just above it by the couplings alone, so that a -member is held to the edge of what it takes. +The host reads a member's capacity once, before anything runs: the floor is checked +against that reading, the vectors are held to it, and every answer states it. A member +is held to every vector within its capacity, and refusing one is failing it. A grade +holds while the host's audit finds nothing (§7.3): at the first answer from a kernel +graded `bit-exact` that differs from the reference's, the member is `uncertified` for +both kernels from then on. +The set holds vectors on both sides of a field bound of 2³¹ − 1, at it and just above it +by the couplings alone, so that a member that declares 32-bit lanes is held to their +edge. A capacity under the floor (§5) is not certified. A settle kernel can earn `bit-exact` on any device. A sampling kernel can, if it uses the reference's draws and order and the cut points of §4.2. @@ -476,7 +543,7 @@ reference's draws and order and the cut points of §4.2. A host placing a job never places an uncertified kernel (§5). A host MAY still run one when it is asked for that member by name, without placing it, which is how a board gets its first answers; the answer says `uncertified`, and nothing should be concluded from -its state but its energy. +its state but its energy. Even then the host runs it on nothing past its capacity. ### 5.2. The receipt @@ -500,7 +567,10 @@ A member states its own, and MUST say what device ran the job, whether its joule priced or read from a meter, and that no quantum effect is involved where none is. It calls a device what it is: a software driver that runs on the processor is not a GPU. An answer adds that a reading stands beside the priced figures, and what it covers, only -where that answer carries one (§4.4). +where that answer carries one (§4.4); and, for a member that declares a capacity, that +the host runs it on no job past that capacity, in its numbers, and, where the host +certified it, that its parity holds within it (§5). A member run by name without +certification claims no grade, and one the audit demoted says so and why (§7.3). ## 6. The comparison @@ -521,6 +591,14 @@ vectors`, and a test holds the file to the code. Each part pins a rule of this t that an implementation written from the text alone, which computes any of these another way, is caught. +The **core set** is the part of it the set held before it grew to reach large jobs, and a +long schedule: the `settle` vectors but `fields-at-the-bound`, the first fifteen `chains` +vectors (three landscapes cooling at five schedules), `a-long-schedule-across-windows`, +and the first eight battery workloads. It sets the floor of a declared capacity (§5), so +no capacity a member may declare leaves out the long schedule. The vectors generated for +a capacity (§5) are not in the file: a host builds them from the capacity, by the rules +below. + ### 7.1. Bits - `settle`: whole workloads, starts and bounds, each with its field bound, and the state, @@ -548,10 +626,114 @@ way, is caught. product, a cut point compared by its high word alone); 16,384 draws in one sweep; more chains than a workgroup; chains that stop hot, in which the order of the cells shows; chains long enough to cross a device's windows of sweeps, by table and by - work, hot in every window; and the same fields past 16 and 32 bits and at the line as - the settle vectors, at 0 and at a temperature of their own size. A sampling kernel is - `bit-exact` when it gives every case it takes exactly, each from one call, and takes - every one §5 does not let it decline. + work, hot in every window: among them, in the core set, 5,000 sweeps on 32 cells each + coupled to 15 others, within the floor on every number, which cross windows of both + kinds, and 200,000 sweeps of two cells, across a window by work on the smallest + workload; and the same fields past 16 and 32 bits and at the line as the settle + vectors, at 0 and at a temperature of their own size. A sampling kernel is + `bit-exact` when it gives every case it takes exactly, each case's chains in calls of + as many as a host would give it (§5.1), and takes every one §5 does not let it + decline. +- Generated, by rule, from a member's capacity (§5), where `C` is the most cells it lets + a workload hold (4096 with none), `P` the most coupled pairs, `B` the chains of one call + (4096, or `chains_per_call` where fewer), and each vector run only where the capacity + admits it: + - cells: for each `n` of 1, 2 and 3, each power of two to `C` with one less and one + more, and `C` and `C − 1`: `n` cells, each with a field of 1 or 2 of either sign so + that every one moves in its first sweep, coupled each to the next (spread along the + whole line where `P` is fewer); settled for 4 sweeps, and held at 1.5 for 3; and, + where a last partial warp of 32 or workgroup of 64 holds more than half of it, + quenched at 0 for 3 sweeps; and a wave settling down as many cells as a workload can + hold (`C`, or `P + 1` where fewer), one cell a sweep, coming to rest within the most + sweeps a run may have (of fewer cells where those are fewer): as long as a wave can + be; and the same wave stopped at its bound one sweep before it would rest, its last + sweep moving a cell; + - the final sweep: a sweep at 0 straight after sweeps above 0, as an engine runs it at + the end of a job, on as many uncoupled cells as a last, partial warp of 32 or + workgroup of 64 holds past half of it: 17, 18, 24, 30 and 31, and 33, 34, 48, 62 and + 63 (odd and even, at the band's edges and in its middle), each after two sweeps at + 1.5, after three (the sweep at 0 at an odd index), and after two and one at 1/64 (a + cold sweep, as an annealing job ends); and, after two sweeps at 1.5, three quarters, + all but two and all but one of a group of 128, 256, 512 and 1,024. Each cell's field + equals the cost of 1, a tie between two states above 0 that the sweep at 0 settles to + 0 however cold the sweep before it, so that the sweep moves cells; + - a settle that keeps moving: `u` units of three cells, each unit's third cell agreeing + with the other two and each unit coupled to the next two by fixed patterns (`13u − 16` + couplings), with fields `−2, 1, 6` on the first unit, `0, 3, −1` between and `0, −5, 0` + on the last, which from every cell at 0 moves a cell in every sweep to sweep `11u − 59` + and rests at `11u − 58` (for 10 units or more); or, where couplings are too few for + those to move longer, units of two, each cell opposed to its partner and to the other + cell of the next unit, with fields `0, −5` on the first unit, `1, 2` between and + `−4, 5` on the last, which move to sweep `4u − 10`. Stopped at its bound just past 2⁹, + 2¹⁰, 2¹¹, 2¹² and 2¹³ sweeps, where a narrow sweep counter wraps, each on as few units + as still move there, and at its last moving sweep (or the most a run may have) on as + many cells as the capacity takes: with none, 1,365 units of three, 4,095 cells, moving + to sweep 14,956. + A search over units of three found these; no longest-moving settle within the rules is + known, and a sweep counter of 14 bits or more, which none of these wraps, or a stale + read at a bound past the sweeps they reach, is left to the audit (§7.3); + - two-cycles that end late: the same units of three beside two cells that alternate from + the start (agreeing, with fields 1 and −1), so that the whole comes to a two-cycle at + sweep `11u − 57`: with 46 units at sweep 449, the first sweep of a later batch of 64, + where a host that runs a settle in batches must carry the state two sweeps back from + the batch before; and with 378 units at sweep 4,101, past 2¹², where a sweep counter + of 9 to 12 bits read at the ending gives the other phase of the cycle (on as many + units as the capacity takes, to 378). A fault at a two-cycle ending later than these is + left to the audit; + - calls: one call of each number of chains from 1 to 32, of 33, 48, 63, 64, 65, 96, 127 + and 128, and of `B` and `B − 1`; + - a row: one cell coupled to as many as a workload within the capacity can give it, + their fields holding them so that its whole row gives it a field of −1 and any + shorter part of the row 0 or more; settled, held at 0.5, and ending at 0; + - lanes: on each side of 0, a cell at each of `2^(L−1) − 1`, `2^(L−1)`, `3·2^(L−2)` + and `2^L`, for `L` of 16, 24 and 32, that the field reaches, of both signs, and a cell + whose field is reached exactly through two couplings at each one the field bound + reaches; and the same beside the largest cost (to `2¹⁵ − 2`), with a cell each side + of the threshold it sets: packed into as few workloads as the capacity takes, so that + a small capacity holds each in a workload of its own size. Each is settled for 3 + sweeps and for 1 and quenched at 0; held at `2¹⁵` and at 1/64 above 0; and at `2¹⁵`, + at 1/64 and at 0, and at 1/64 and then 0, where both sides take it, the shape an + annealing job ends in; + - a corner on each side of 0, as the capacity takes it: the largest field of both + signs beside the largest cost under it, so that those cells move at 0, among as + many cells and couplings as the capacity allows (fewer where a table or span limit + asks); and a cell whose field is reached exactly through as many couplings as a cell + can have, the field bound at its most; and a cell at field 0 whose row of cells held + at +1 sums to 128, and to as many as a cell can have, past what a narrow lane holds. + Each at 0, cold above 0, and ending at 0; + - a span: where no span is declared, two hubs on shared cells as wide apart as the + other numbers let them reach (the widest a declared table takes, where it is the + narrower), held at a temperature of the span's size and ending at 0; + - draw indices: one run of `C` cells carried past `2²⁴` where `C` times the most sweeps + reaches it, at 0 until the sweeps that cross it and hot to the end. A run past `2³¹` + would be at least `2³¹` updates in one chain, minutes on a device that runs a chain a + thread, so a draw index narrower than 64 bits past 24 is left to the audit (§7.3); + - at each number declared below what a job can reach: a wave settling down `C` cells; + `P` couplings, each cell's as near as the field bounds allow; the field bound at 0 + through couplings and by a field alone, settled to the end and for one sweep, and + above 0 alone, coupled and reached exactly; the field and the cost at 0, and above 0 + hot, cold and ending at 0; a table of exactly that many entries and a span exactly as + wide, through hubs coupled to shared cells where the other numbers allow it (where a + declared table or the couplings do not take that span, the widest under it they do); + and a schedule of exactly that many sweeps; + - for the battery (§7.2), where `C` is past 24: `C` alike cells, every cell read in + blocks of three (the last block ending at the last cell), and, where `C` is not one + less than a power of two, as many alike cells as the largest such count under it + from 31 to 1,023, every cell read, so that a last, partial warp or workgroup holds as + many as it can; at declared couplings, alike triangles, every one read; at a declared + schedule length, one alike triangle warm and then cold, to 1,024 sweeps; at a + declared field bound, field and cost, three alike cells at it, and the field bound + through couplings; at a declared span, its two hubs where they hold no other cell. + Identical blocks cannot reach a table, so a table has none. + + The rules bound what certification costs, whatever the capacity: at most 300 generated + vectors, at most 450 million cell sweeps hot or settled, and at most 20 million at + temperature 0 (the run past `2²⁴`, at a fixed point, which the reference skips). With no + capacity they come to about 262 million cell sweeps, two fifths of them battery + workloads, which run only for a kernel that is not `bit-exact`; the most, for a + capacity of 4,096 cells that declares every other number below what a job can reach, is + about 430 million, half of them battery workloads. A test holds the bounds over the + named capacities and over random ones, and prints each one's share. - `jobs`: whole requests and what their answers hold, refusals included: every worked example at seed 1, and requests that pin a rule each (a `minimize` above temperature 0, a temperature given as text, a pin's size, quantised weights, the state a two-cycle @@ -565,7 +747,9 @@ way, is caught. A sampling kernel with draws of its own is held to `statistical`: workloads of two to six cells, and one of eight blocks of three side by side, none coupled to another, each with a schedule. A member may decline a workload of the battery only where §5 lets it decline -a vector, and it then passes that workload without running it. Workload `k` runs `statistical.chains` (20,000) chains, chain `r` with the +a vector, and it then passes that workload without running it. A workload of many blocks +may name the blocks it reads (`read`); only those are counted, so that many tests at once +do not fail a correct sampler by chance. Workload `k` runs `statistical.chains` (20,000) chains, chain `r` with the chain seed `out(statistical.seed + k, r + 1)` and its start from that seed (§4.2), through the workload's temperatures. The final states of each `block` of cells are counted over its `3ⁿ` states, in the order `Σ (s_i + 1)·3ⁱ`, and held to `exact`. Where @@ -579,6 +763,34 @@ bin expected fewer than `statistical.pool_below` (5) times pooled into one; with bins less one, a block passes when it is at most `df + 5·√(2·df)`, and a workload when every block does. +The workloads a capacity adds (§7.1) are held by a bound that holds whatever the law, +however small its bins, so that they fail a correct sampler, whatever its draws, less +than once in a million between them. They run in calls of 63, 62 and on down to 17 chains +in turn (no more than the member takes), so that their calls end in every partial warp of +more than 16 chains and every partial workgroup of more than 32. Every block is read. A +block's statistic is the likelihood ratio `G = 2 Σ O ln(O/E)` over every +bin, none pooled: `2N` times the divergence of its counts from its law, `N` the chains. +By the method of types, the chance that `N` draws over `k` bins fall at a divergence `t` or +more from their law is at most `C(N + k − 1, k − 1)·e^(−N·t)`, the number of ways `N` draws +can fall in `k` bins times the chance of the likeliest of them. So a block passes when `G` +is at most `2·(ln C(N + k − 1, k − 1) + ln(B/10⁻⁸))`, `B` the blocks the workload reads: a +workload fails a correct sampler with chance at most 10⁻⁸ (for 27 bins and one block, +429.3; for 10 bins, 189.5). A test sums the chance exactly over every way 1,000 draws can +fall in three bins, for laws with bins a pooled chi-square mishandles. A sampler that +leaves a cell unmoved, draws one at random, or leaves a call's last partial warp at its +starts lies far past the bound on the block that holds it: a cell left at its start in a +block of three gives `G` near 12,000 at `N` = 20,000. + +The bound is loose, and what it misses is part of the grade. It sits at a divergence of +about 0.011 a block (429.3 over `2N`), so a law near the exact one passes: three alike +cells at field 1 drawn at a temperature 10% too high are 0.0055 from their law, `G` near +250, and pass; 15% too high, `G` near 490, and fail. The battery's cells are the fixed +workloads' (2 to 24), `C` and the largest `2^j − 1` under it: a fault keyed to another +count of cells, such as the lanes past 32 of a partial last workgroup only where it holds +an even count, is never run. A test holds each of these statements. A kernel graded +`statistical` is never audited (§7.3), and every answer from one says what the grade rests +on and what it misses. + The workloads hold pairs of coupled cells next to each other, two apart and three apart, a triangle no two colours can split, fields to 40 and a cost of 8 at a high temperature, a small field at a low one, a schedule that ends at 0, one that cools from 6 to 0.7, a ring @@ -592,6 +804,121 @@ where they started fails at least one of them; the reference's own kernel, one t takes its cells last to first pass every one. A test holds both statements, with kernels that are wrong in each of those ways. +### 7.3. The audit + +A finite set cannot catch every fault, so a `bit-exact` grade rests on a continuous check +as well as on certification, and the check rests only on what the host computes and +remembers. For every answer from a member's kernel graded `bit-exact`, the host runs part +of the job again on the reference and compares the bits: + +- sampling: in every job (a run, a replay, a sweep or a comparison), in every call to the + member, the call's last chain and, for each seed's run the call holds chains of, one of + those chains, each drawn afresh: what the host checks in one call says nothing of what + it checks in the next, so a member that times the host between its calls learns + nothing it can use. The host runs a call's checked chains again after the call returns + and before it makes the next: on the job's own thread, at the job's priority, and on as + many of the audit's threads as are free, a quarter of the host's cores and at most 4 + over the whole process at once (none on a host of fewer than 4 cores), each at a low + priority (a nice value 10 higher, to 19, on Linux; the background band on macOS). So an + audited job takes at most its own thread and the audit's threads; several audited jobs + at once each run a share on their own threads at their own priority, beside the + audit's threads, and a host that must keep audited jobs from crowding out others lowers + those jobs' own priority. An audit thread whose priority cannot be lowered says so on + standard error, once, and runs its share all the same; +- settling: the reference's settle, as far as a budget of 2²⁴ of work goes, the work + being the cells and couplings times the sweeps the host asked for, never the sweeps the + member reports. Where the reference comes to rest within the budget, or the whole bound + fits it, that is the whole answer, and the member's is compared with it. Past it, the + whole settle is run in one job in 8; in the others a member whose settle ends before + the reference's had is wrong. The checks of §5 hold as before. + +The chains, and which settles past the budget are run whole, are drawn with a key the +host makes when it starts, from its operating system's randomness, and a count that +every draw moves on: no member can tell which of its chains are checked, and the same job +sent again is drawn afresh. Every audited answer records what was checked +(`audit.checked`: each run's seed and its chains checked, or how much of a settle). The +draws are the host's, as the rest of its work is: nothing in an answer lets a reader check +afterwards that the chains were drawn and not chosen, and a reader who needs that runs +the job on the reference. + +At the first difference, or the first answer the checks refuse, the host demotes the +member to `uncertified` for both kernels, at once. It answers that job from the reference, +naming the member under `refusals` with why, and states the demotion in every later +answer from the member (`member.demoted`) and in its label. A host placing jobs never +places it again. The host keeps the demotion under the member's identity, its id and its +fingerprint (a bitstream's or a firmware's digest, a driver's version: `Member::fingerprint`), +in a file where it names one, so that it outlives the process: certifying the member again, +or its declaring another capacity, does not clear it. Only another identity is certified +afresh. Hosts and jobs may share the file. The host keeps a demotion at once in a file of +its own beside it (`..demotion.<32 hex digits>.json`, with the file's mode), with no +lock to wait for, and then merges it into the file under the file's lock (the file of its +name with `.lock` added), waited for at most 5 s (`member::STORE_LOCK_WAIT`): written +whole to a file of its own with the file's mode, synced and renamed into place, the +merged files of their own then removed. Where that wait runs out, the demotion stays in +its own file until a later demotion merges it, and is kept all the same: none is lost +and none loses another's. Whenever it certifies a member, the host reads the file and +every demotion in a file of its own afresh, under the lock held shared, and then rewrites +the file as it is, the way a demotion rewrites it: a file of its own as large as the +largest rewrite a demotion can make (the records with the largest entry one can add, a +reason being kept to its first 1,024 bytes), synced, cut back and renamed over the file. +So a folder no longer written to, a full disk, a limit on a file's size, and a file that +cannot be replaced (another user's in a folder with the sticky bit, one marked immutable +or append-only) are each found before a member is graded on the store. Its temporary +files are named for its file alone (`.....tmp`, +matched exactly, so that no other store's file is taken for one of its own), each held +locked by its writer while it lives; one unchanged for 10 s that no writer holds is a +killed writer's, and the next certification or demotion removes it. Where the path is a +link, the store is the link's target, followed link by link: its lock, its files and its +rename; a link in a folder anyone may write that has the sticky bit is not followed, +whoever made it, and no file of the store's is opened through a link. The store fails +closed. The host says so on its standard error, grades no kernel `bit-exact`, and holds +the member at `uncertified`, saying why in `member.demoted`, where the path names no file +(an empty `WAI_LANDSCAPE_DEMOTIONS` names none, and is as if unset), where it is a link to +nothing or a link in such a folder, where the file has more than one name (a hard link, +which a rewrite would split), where the file or a demotion in a file of its own cannot be +read or does not parse, where the file cannot be rewritten as a demotion rewrites it, +where a write fails, or where a host that keeps one has no file to name. A demotion it +could neither keep in its own file nor merge it keeps for as long as it runs, and says +so. Where the lock is held past 5 s, the certification that waited grades no kernel +`bit-exact` and says why, and the next waits again, so that a board job holds the board +no longer than that waiting on the store. + +A kernel graded `statistical` gives draws of its own, which no audit can hold to the +reference's bits. A host placing jobs places it only where the job asks for it +(`"parity": "statistical"`); otherwise it passes over it, saying why, and a job is +answered by a kernel whose bits are audited, or by the reference. Asked for by name, it +is run, and its answer says `statistical`. + +Its cost: in every call, its last chain and one chain of each run it holds chains of, so +for a run of `R` chains in calls of `B` at most `2⌈R/B⌉/R` of its sampling work again, +and for a job of many seeds' runs in one call, one chain of each and one more; for a +settle, at most 2²⁴ of work, and past it the whole settle again one job in 8. The bound +is that work. It is work on the host's CPU, which a device outruns, so in wall time the +audit costs more than its share of the work, most where a job is many short runs, and by +how much depends on the host, its load and the device: a host that needs a wall-time +figure measures it on its own hardware (the `timings` test of `landscape_devices` prints +five runs of each of four jobs, audited against the same member run by name). +Ten runs a device on two shared hosts, with the audit's threads as above, give +an idea: a replay of 256 seeds of 8 chains of 64 cells for 500 sweeps took 108% to 171% +longer audited through Metal on an M3 Ultra's GPU, and 85% to 122% through CUDA on an RTX +4050 laptop GPU; a replay of 256 seeds of one chain each, where every chain is checked, +112% to 183% and 90% to 120% longer, so that through Metal it took up to 1.18 times what +the reference alone took; a run of 4,096 chains of 64 cells at most 18% longer, and one of +1,024 chains of 1,024 cells at most 4%. What it catches: a fault that makes a fraction `q` +of a run's chains wrong, on each such run with a chance of at least `q`, whatever chains +it chooses, since it cannot tell which are checked; one that makes the last chain of a +call wrong (a partial last warp or workgroup, a call longer than the member keeps), on +the first job that shows it; one in a settle whose reference comes to rest within the +budget, on the first job that shows it; one that ends a settle at or before the last +sweep the budget reaches while the reference's goes on, on the first such job; and any +other in a settle past the budget, one that ends it later than that sweep and sooner +than the reference among them, only within `N` such jobs, with a chance of +`1 − (7/8)^N` (0.74 at 10 jobs, 0.99 at 35), each job drawn afresh, repeats of one job +included. A draw index narrower than 64 bits, which +certification holds only to `2²⁴` (§7.1), is caught on the first job whose run passes its +width, since every chain of that run is then wrong. The audit is a check that runs as long +as the member does. It is not a proof. + ## 8. What does not run on this model The worked examples include each of these, and they fail there, on purpose: problems of three diff --git a/wai/quantum-conformance/landscape/vectors.json b/wai/quantum-conformance/landscape/vectors.json index 448b05fe3..ada6ec3c0 100644 --- a/wai/quantum-conformance/landscape/vectors.json +++ b/wai/quantum-conformance/landscape/vectors.json @@ -53049,6 +53049,1355 @@ } ] }, + { + "name": "a-long-schedule-across-windows", + "landscape": { + "n": 32, + "couplings": [ + [ + 0, + 1, + 1 + ], + [ + 0, + 2, + -1 + ], + [ + 0, + 3, + 1 + ], + [ + 0, + 4, + -1 + ], + [ + 0, + 5, + 1 + ], + [ + 0, + 6, + -1 + ], + [ + 0, + 7, + 1 + ], + [ + 0, + 16, + -1 + ], + [ + 0, + 25, + 1 + ], + [ + 0, + 26, + -1 + ], + [ + 0, + 27, + 1 + ], + [ + 0, + 28, + -1 + ], + [ + 0, + 29, + 1 + ], + [ + 0, + 30, + -1 + ], + [ + 0, + 31, + 1 + ], + [ + 1, + 2, + 1 + ], + [ + 1, + 3, + -1 + ], + [ + 1, + 4, + 1 + ], + [ + 1, + 5, + -1 + ], + [ + 1, + 6, + 1 + ], + [ + 1, + 7, + -1 + ], + [ + 1, + 8, + 1 + ], + [ + 1, + 17, + -1 + ], + [ + 1, + 26, + 1 + ], + [ + 1, + 27, + -1 + ], + [ + 1, + 28, + 1 + ], + [ + 1, + 29, + -1 + ], + [ + 1, + 30, + 1 + ], + [ + 1, + 31, + -1 + ], + [ + 2, + 3, + 1 + ], + [ + 2, + 4, + -1 + ], + [ + 2, + 5, + 1 + ], + [ + 2, + 6, + -1 + ], + [ + 2, + 7, + 1 + ], + [ + 2, + 8, + -1 + ], + [ + 2, + 9, + 1 + ], + [ + 2, + 18, + -1 + ], + [ + 2, + 27, + 1 + ], + [ + 2, + 28, + -1 + ], + [ + 2, + 29, + 1 + ], + [ + 2, + 30, + -1 + ], + [ + 2, + 31, + 1 + ], + [ + 3, + 4, + 1 + ], + [ + 3, + 5, + -1 + ], + [ + 3, + 6, + 1 + ], + [ + 3, + 7, + -1 + ], + [ + 3, + 8, + 1 + ], + [ + 3, + 9, + -1 + ], + [ + 3, + 10, + 1 + ], + [ + 3, + 19, + -1 + ], + [ + 3, + 28, + 1 + ], + [ + 3, + 29, + -1 + ], + [ + 3, + 30, + 1 + ], + [ + 3, + 31, + -1 + ], + [ + 4, + 5, + 1 + ], + [ + 4, + 6, + -1 + ], + [ + 4, + 7, + 1 + ], + [ + 4, + 8, + -1 + ], + [ + 4, + 9, + 1 + ], + [ + 4, + 10, + -1 + ], + [ + 4, + 11, + 1 + ], + [ + 4, + 20, + -1 + ], + [ + 4, + 29, + 1 + ], + [ + 4, + 30, + -1 + ], + [ + 4, + 31, + 1 + ], + [ + 5, + 6, + 1 + ], + [ + 5, + 7, + -1 + ], + [ + 5, + 8, + 1 + ], + [ + 5, + 9, + -1 + ], + [ + 5, + 10, + 1 + ], + [ + 5, + 11, + -1 + ], + [ + 5, + 12, + 1 + ], + [ + 5, + 21, + -1 + ], + [ + 5, + 30, + 1 + ], + [ + 5, + 31, + -1 + ], + [ + 6, + 7, + 1 + ], + [ + 6, + 8, + -1 + ], + [ + 6, + 9, + 1 + ], + [ + 6, + 10, + -1 + ], + [ + 6, + 11, + 1 + ], + [ + 6, + 12, + -1 + ], + [ + 6, + 13, + 1 + ], + [ + 6, + 22, + -1 + ], + [ + 6, + 31, + 1 + ], + [ + 7, + 8, + 1 + ], + [ + 7, + 9, + -1 + ], + [ + 7, + 10, + 1 + ], + [ + 7, + 11, + -1 + ], + [ + 7, + 12, + 1 + ], + [ + 7, + 13, + -1 + ], + [ + 7, + 14, + 1 + ], + [ + 7, + 23, + -1 + ], + [ + 8, + 9, + 1 + ], + [ + 8, + 10, + -1 + ], + [ + 8, + 11, + 1 + ], + [ + 8, + 12, + -1 + ], + [ + 8, + 13, + 1 + ], + [ + 8, + 14, + -1 + ], + [ + 8, + 15, + 1 + ], + [ + 8, + 24, + -1 + ], + [ + 9, + 10, + 1 + ], + [ + 9, + 11, + -1 + ], + [ + 9, + 12, + 1 + ], + [ + 9, + 13, + -1 + ], + [ + 9, + 14, + 1 + ], + [ + 9, + 15, + -1 + ], + [ + 9, + 16, + 1 + ], + [ + 9, + 25, + -1 + ], + [ + 10, + 11, + 1 + ], + [ + 10, + 12, + -1 + ], + [ + 10, + 13, + 1 + ], + [ + 10, + 14, + -1 + ], + [ + 10, + 15, + 1 + ], + [ + 10, + 16, + -1 + ], + [ + 10, + 17, + 1 + ], + [ + 10, + 26, + -1 + ], + [ + 11, + 12, + 1 + ], + [ + 11, + 13, + -1 + ], + [ + 11, + 14, + 1 + ], + [ + 11, + 15, + -1 + ], + [ + 11, + 16, + 1 + ], + [ + 11, + 17, + -1 + ], + [ + 11, + 18, + 1 + ], + [ + 11, + 27, + -1 + ], + [ + 12, + 13, + 1 + ], + [ + 12, + 14, + -1 + ], + [ + 12, + 15, + 1 + ], + [ + 12, + 16, + -1 + ], + [ + 12, + 17, + 1 + ], + [ + 12, + 18, + -1 + ], + [ + 12, + 19, + 1 + ], + [ + 12, + 28, + -1 + ], + [ + 13, + 14, + 1 + ], + [ + 13, + 15, + -1 + ], + [ + 13, + 16, + 1 + ], + [ + 13, + 17, + -1 + ], + [ + 13, + 18, + 1 + ], + [ + 13, + 19, + -1 + ], + [ + 13, + 20, + 1 + ], + [ + 13, + 29, + -1 + ], + [ + 14, + 15, + 1 + ], + [ + 14, + 16, + -1 + ], + [ + 14, + 17, + 1 + ], + [ + 14, + 18, + -1 + ], + [ + 14, + 19, + 1 + ], + [ + 14, + 20, + -1 + ], + [ + 14, + 21, + 1 + ], + [ + 14, + 30, + -1 + ], + [ + 15, + 16, + 1 + ], + [ + 15, + 17, + -1 + ], + [ + 15, + 18, + 1 + ], + [ + 15, + 19, + -1 + ], + [ + 15, + 20, + 1 + ], + [ + 15, + 21, + -1 + ], + [ + 15, + 22, + 1 + ], + [ + 15, + 31, + -1 + ], + [ + 16, + 17, + 1 + ], + [ + 16, + 18, + -1 + ], + [ + 16, + 19, + 1 + ], + [ + 16, + 20, + -1 + ], + [ + 16, + 21, + 1 + ], + [ + 16, + 22, + -1 + ], + [ + 16, + 23, + 1 + ], + [ + 17, + 18, + 1 + ], + [ + 17, + 19, + -1 + ], + [ + 17, + 20, + 1 + ], + [ + 17, + 21, + -1 + ], + [ + 17, + 22, + 1 + ], + [ + 17, + 23, + -1 + ], + [ + 17, + 24, + 1 + ], + [ + 18, + 19, + 1 + ], + [ + 18, + 20, + -1 + ], + [ + 18, + 21, + 1 + ], + [ + 18, + 22, + -1 + ], + [ + 18, + 23, + 1 + ], + [ + 18, + 24, + -1 + ], + [ + 18, + 25, + 1 + ], + [ + 19, + 20, + 1 + ], + [ + 19, + 21, + -1 + ], + [ + 19, + 22, + 1 + ], + [ + 19, + 23, + -1 + ], + [ + 19, + 24, + 1 + ], + [ + 19, + 25, + -1 + ], + [ + 19, + 26, + 1 + ], + [ + 20, + 21, + 1 + ], + [ + 20, + 22, + -1 + ], + [ + 20, + 23, + 1 + ], + [ + 20, + 24, + -1 + ], + [ + 20, + 25, + 1 + ], + [ + 20, + 26, + -1 + ], + [ + 20, + 27, + 1 + ], + [ + 21, + 22, + 1 + ], + [ + 21, + 23, + -1 + ], + [ + 21, + 24, + 1 + ], + [ + 21, + 25, + -1 + ], + [ + 21, + 26, + 1 + ], + [ + 21, + 27, + -1 + ], + [ + 21, + 28, + 1 + ], + [ + 22, + 23, + 1 + ], + [ + 22, + 24, + -1 + ], + [ + 22, + 25, + 1 + ], + [ + 22, + 26, + -1 + ], + [ + 22, + 27, + 1 + ], + [ + 22, + 28, + -1 + ], + [ + 22, + 29, + 1 + ], + [ + 23, + 24, + 1 + ], + [ + 23, + 25, + -1 + ], + [ + 23, + 26, + 1 + ], + [ + 23, + 27, + -1 + ], + [ + 23, + 28, + 1 + ], + [ + 23, + 29, + -1 + ], + [ + 23, + 30, + 1 + ], + [ + 24, + 25, + 1 + ], + [ + 24, + 26, + -1 + ], + [ + 24, + 27, + 1 + ], + [ + 24, + 28, + -1 + ], + [ + 24, + 29, + 1 + ], + [ + 24, + 30, + -1 + ], + [ + 24, + 31, + 1 + ], + [ + 25, + 26, + 1 + ], + [ + 25, + 27, + -1 + ], + [ + 25, + 28, + 1 + ], + [ + 25, + 29, + -1 + ], + [ + 25, + 30, + 1 + ], + [ + 25, + 31, + -1 + ], + [ + 26, + 27, + 1 + ], + [ + 26, + 28, + -1 + ], + [ + 26, + 29, + 1 + ], + [ + 26, + 30, + -1 + ], + [ + 26, + 31, + 1 + ], + [ + 27, + 28, + 1 + ], + [ + 27, + 29, + -1 + ], + [ + 27, + 30, + 1 + ], + [ + 27, + 31, + -1 + ], + [ + 28, + 29, + 1 + ], + [ + 28, + 30, + -1 + ], + [ + 28, + 31, + 1 + ], + [ + 29, + 30, + 1 + ], + [ + 29, + 31, + -1 + ], + [ + 30, + 31, + 1 + ] + ], + "field": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "cost": 0 + }, + "field_bound": 15, + "seed": "13", + "chains": [ + { + "chain": "14180207640020093695", + "start": "+0--00000++0+-++000+++-0-00---0-", + "expect": { + "state": "--0-00-0-00+0-00++-++0+0--++-+00", + "accumulates": 2400000, + "draws": 160000, + "term": "bound", + "state_hash": "1900ba257002f120" + } + }, + { + "chain": "6063221543909367921", + "start": "0-0+++--+-+-0+0--+--0-0+0++00++0", + "expect": { + "state": "++-0+0+0+00-++-+--00--+0-+++--++", + "accumulates": 2400000, + "draws": 160000, + "term": "bound", + "state_hash": "10c8a1a24e7e0f89" + } + }, + { + "chain": "11674071465944544456", + "start": "000-0-+0-00--+00+0+-0+-+0+--+0-+", + "expect": { + "state": "-0-+0-0-++-+--0+--00+000+++-+0+-", + "accumulates": 2400000, + "draws": 160000, + "term": "bound", + "state_hash": "5fc9d915ca1df295" + } + }, + { + "chain": "5378838118255245427", + "start": "0-+--000-+0+-00--0+---0++000----", + "expect": { + "state": "-+0-0--00-++++-0--00++-++++--+0-", + "accumulates": 2400000, + "draws": 160000, + "term": "bound", + "state_hash": "c63eade7feda3d5b" + } + } + ], + "held": [ + { + "temperature": 20.0, + "temperature_bits": "4034000000000000", + "sweeps": 5000 + } + ] + }, + { + "name": "a-schedule-across-a-work-window", + "landscape": { + "n": 2, + "couplings": [ + [ + 0, + 1, + 1 + ] + ], + "field": [ + 1, + 0 + ], + "cost": 0 + }, + "field_bound": 2, + "seed": "14", + "chains": [ + { + "chain": "7685909621375755838", + "start": "-0", + "expect": { + "state": "-+", + "accumulates": 400000, + "draws": 400000, + "term": "bound", + "state_hash": "08328607b4eb6c87" + } + }, + { + "chain": "1315361123233219194", + "start": "0+", + "expect": { + "state": "++", + "accumulates": 400000, + "draws": 400000, + "term": "fixed-point", + "state_hash": "08395607b4f139a5" + } + } + ], + "held": [ + { + "temperature": 1.5, + "temperature_bits": "3ff8000000000000", + "sweeps": 200000 + } + ] + }, { "name": "fields-past-16-bits/at-0", "landscape": { diff --git a/wai/wai-quantum-landscape-fpga/README.md b/wai/wai-quantum-landscape-fpga/README.md index 82b9fc342..d28bd5385 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -58,20 +58,44 @@ the draw the reference takes next. **The member.** `src/member.rs` implements `wai_quantum::quantum_landscape::member::Member`. It is named `fpga:kv260-landscape-64`. It says nothing of its own parity: the host certifies it (`member::certify`), running the conformance set's `settle` and `chains` cases on the device and -granting `bit-exact` to a kernel only if every case it takes comes back bit for bit (and, for a -sampling kernel that misses, trying the statistical battery). It declines only what the spec lets a -member decline (§5: sampling fields above 2^31 − 1). +granting `bit-exact` to a kernel only if every case within its declared capacity comes back bit for +bit, and every case the host generates for it: by rule, every cell count to 64 (each power of two, +one less and one more), every call size to 32 chains, each band of a wavefront and a whole batch, a row of 63 couplings, fields +at the wrap points of 16-bit lanes, a run past draw index 2²⁴, and a corner with every number at its +most; and at each number it declares, a wave down 64 cells, fields at ±524,287 and a cost of 1,048,575 +drawn hot, cold and hot then at 0, and two fields spanning 8,192 drawn at temperature 8,192. For a +sampling kernel that misses, the host tries the statistical battery within it. After that, the host +audits its answers (§7.3 of the extension) and demotes it at the first that differs. This host keeps +the demotion in `~/.local/state/wai-quantum-landscape-fpga/demotions.json` (or the file +`WAI_LANDSCAPE_DEMOTIONS` names), under the board's id and its engine's ID and version, so a demoted +board is not certified again on its next job; a new engine is. It reads the file when it certifies +the board, after it holds the board's lock, with every demotion kept in a file of its own beside it, +and rewrites it as a demotion would; it keeps a demotion at once in a file of its own and merges it +into the file under the file's own lock, waited for at most 5 s (where that runs out, the demotion +stays in its own file, which every certification reads), so jobs and hosts sharing it keep one +another's; a link is followed to its target, except a link in a sticky folder anyone may write. +Where the file cannot be read, parsed or rewritten (a full disk, a read-only folder, a file another +user owns in a sticky folder, one marked immutable), it has a second name (a hard link), the lock is +held past 5 s, or `HOME` is unset and no file is named (an empty `WAI_LANDSCAPE_DEMOTIONS` names +none), it says so on standard error and grades no kernel `bit-exact`. -**What it cannot hold, and what that costs.** The engine has 64 cells, 20-bit field and cost -registers for sampling, and a table window of 8,192 fields a temperature. The conformance set as it -stands now holds cases past all three (settle cases of 65 to 1,000 cells, chains of 128 and 1,024 -cells, sampled fields up to 2^31 − 1, a battery workload whose uncertain fields span 135,000), and -the spec does not let a member decline them. The board refuses each with a sentence, never with -wrong bits, and every case it can hold it gives bit for bit (`conform`). But a refused case is a -missed vector, so the host certifies both kernels `uncertified` under the current set, and a job -on the board says so. The board was certified `bit-exact` for both kernels under the set of -2026-11-06 (`vectors.json` sha256 `43e543a634e0ffdc...`), before the set grew; the measurements -below were taken then. It runs whatever schedule the job gives, one +**Its declared capacity** (`member::CAPACITY`, §5 of the extension): 64 cells; above temperature 0, +fields to ±524,287 and a cost to 1,048,575, its 20-bit registers, and uncertain fields within a span +of 8,192 at a temperature, its table's window. At temperature 0 it takes any field (below). The host +runs it on no job past those numbers, and a test holds the declared numbers to the engine's own +limits, case by case over the whole set and the generated cases +(`the_declared_capacity_is_exactly_what_the_engine_holds`), and on 3,000 random workloads and +schedules (`the_capacity_and_the_engine_agree_on_random_jobs`). + +**What it cannot hold.** The conformance set holds cases past all three of its limits (settle +cases of 65 to 1,000 cells, chains of 128 and 1,024 cells, sampled fields up to 2^31 − 1, a battery +workload whose uncertain fields span 135,000). Those are past its declared capacity, so the host +neither runs them on it nor counts them against it: on the register-level model, which the +testbenches hold the logic to, the host certifies both kernels `bit-exact` within that capacity +against the set as it stands (`the_model_is_certified_bit_exact_within_its_declared_capacity`), and +every job's answer states the capacity. The board itself was certified `bit-exact` for both kernels +under the set of 2026-11-06 (`vectors.json` sha256 `43e543a634e0ffdc...`), before the set grew; +the measurements below were taken then, and it is to be certified again on the set as it stands. It runs whatever schedule the job gives, one temperature a sweep: a run of sweeps above 0 as sampled segments, a run at 0 in T0 mode, sweep `k` drawing from `k·n + 1` whatever came before it. The reader, the truth, the energy, the verdict, the checks on what a member returns, the replay digest and the seal stay on the host @@ -255,9 +279,8 @@ agreement checks also on the EMIO build, with the same results (`measurements/20 - **Certified by the host: settle `bit-exact`, sample `bit-exact`, under the set of that day.** The 13 settle cases (the one with fields of 2^40 included) and the 42 chains of the `chains` cases, bit for bit, each answer also held to the job by `check_settle` and `check_chain`. Under the set - as it stands now, on the model, which the testbenches hold the logic to: every case the engine can - hold, 21 settle cases and 227 chains, bit for bit, and 13 cases refused as beyond it, so - `uncertified` (above). + as it stands now, on the model, which the testbenches hold the logic to: every case within its + declared capacity bit for bit, and so `bit-exact` within it (above). - **Cut points and draws:** the 650 `cells` cut points the host computes equal the set's; 29 of those cells drawn 100,000 times each on the device, every draw the reference's and every frequency within 4 standard errors. @@ -385,11 +408,12 @@ with its couplings fixed in the logic. - **The port widths are the boot's.** The build assumes the processing system's master port and its high-performance port at 128 bits, as this board's boot sets them; the host reads both and refuses otherwise, and does not set them. Another boot image may need another build. -- **Not certified under the current set.** 64 cells, sampled fields of ±524,287 and a cost of - 1,048,575, and a window of 8,192 fields a temperature: the set now holds cases past each, which a - member may not decline, so both kernels come out `uncertified` (above). At temperature 0 large - fields are mapped exactly; more cells would need the engine to tile its cells, wider sampling - fields wider registers, and a wider spread of uncertain fields more than one window. +- **Certified within a capacity, not beyond it.** 64 cells, sampled fields of ±524,287 and a cost of + 1,048,575, and a window of 8,192 fields a temperature: its grade holds within those, and the host + runs it on no job past them. A host offered other members gives such a job to the next; this + board's own host, with one member, refuses it, saying which number it is past. At temperature 0 large fields are mapped exactly; more cells + would need the engine to tile its cells, wider sampling fields wider registers, and a wider spread + of uncertain fields more than one window. - **The joint test's histogram is the reference's.** The board is bit-exact, so the test holds the shared kernel to exact enumeration on the board's outputs, not an independent sampler. - **A cell update takes four clocks**, and in sequential mode one of 64 cones is used at a time. diff --git a/wai/wai-quantum-landscape-fpga/src/conform.rs b/wai/wai-quantum-landscape-fpga/src/conform.rs index 2eff51609..7f40bb1ba 100644 --- a/wai/wai-quantum-landscape-fpga/src/conform.rs +++ b/wai/wai-quantum-landscape-fpga/src/conform.rs @@ -15,7 +15,7 @@ use crate::port::Port; use crate::regs::*; use serde_json::{Value, json}; use std::fmt::Write as _; -use wai_quantum::quantum_landscape::conformance::{MUST_TAKE_FIELD_BOUND, battery, chain_cases, run_battery, settle_cases}; +use wai_quantum::quantum_landscape::conformance::{battery, chain_cases, run_battery, settle_cases}; use wai_quantum::quantum_landscape::energy::Landscape; use wai_quantum::quantum_landscape::member::{Certified, Chain, Member, check_chain, check_settle, named}; use wai_quantum::quantum_landscape::program::read; @@ -66,15 +66,10 @@ fn vectors() -> Value { /// (`check_settle`, `check_chain`): what the host's certification grades. pub fn bits(m: &BoardMember

) -> Result { let mut text = String::new(); - let may_decline = |w: &Workload| w.field_bound() > MUST_TAKE_FIELD_BOUND; - let (mut s_ok, mut s_n, mut s_declined, mut s_wrong) = (0, 0, 0, 0); + let (mut s_ok, mut s_n, mut s_wrong) = (0, 0, 0); let mut settle_beyond = Vec::new(); for c in settle_cases() { let w = c.landscape.lower(); - if m.takes(&w, false).is_err() && may_decline(&w) { - s_declined += 1; - continue; - } let want = w.settle(&c.start, c.bound); let got = m.settle(&w, &c.start, c.bound); if let Some(why) = beyond(&w, &[]) { @@ -100,16 +95,12 @@ pub fn bits(m: &BoardMember

) -> Result { } } } - let (mut c_ok, mut c_n, mut c_declined, mut c_wrong) = (0, 0, 0, 0); + let (mut c_ok, mut c_n, mut c_wrong) = (0, 0, 0); let mut chains_beyond = Vec::new(); for c in chain_cases() { let w = c.landscape.lower(); let temps = c.temperatures.each(); let chains = c.chains(); - if m.takes(&w, temps.iter().any(|&t| t > 0.0)).is_err() && may_decline(&w) { - c_declined += chains.len(); - continue; - } let got = m.chains(&w, &chains, &temps); if let Some(why) = beyond(&w, &temps) { match got { @@ -142,12 +133,12 @@ pub fn bits(m: &BoardMember

) -> Result { } let _ = writeln!( text, - "settle cases the engine holds: {s_ok}/{s_n} bit for bit; {} beyond its capacity; {s_declined} declined, as the spec lets a member", + "settle cases within its declared capacity: {s_ok}/{s_n} bit for bit; {} past it, refused", settle_beyond.len() ); let _ = writeln!( text, - "chains the engine holds: {c_ok}/{c_n} bit for bit; {} cases beyond its capacity; {c_declined} chains declined, as the spec lets a member", + "chains within its declared capacity: {c_ok}/{c_n} bit for bit; {} cases past it, refused", chains_beyond.len() ); for b in settle_beyond.iter().chain(&chains_beyond) { @@ -228,12 +219,10 @@ pub fn full(m: &BoardMember

, scale: f64) -> Result { // the statistical battery, and the all-at-once control it is built to catch let held: Vec = battery().iter().map(|c| beyond(&c.landscape.lower(), &c.temperatures).is_none()).collect(); for (r, holds) in run_battery(m).into_iter().zip(&held) { - if !holds && !r.declined { + if !holds || r.declined { let c = battery().into_iter().find(|c| c.name == r.name).expect("the battery's own case"); let why = beyond(&c.landscape.lower(), &c.temperatures).unwrap_or_default(); - rep.check(!r.passed, format!("battery {}: beyond the engine ({why}), refused", r.name)); - } else if r.declined { - rep.check(r.passed, format!("battery {}: declined, as the spec lets a member", r.name)); + rep.check(r.declined && !holds, format!("battery {}: past its declared capacity ({why}), not run", r.name)); } else { rep.check(r.passed, format!("battery {}: chi-square {:.1} on {} degrees of freedom (critical {:.1})", r.name, r.chi_square, r.degrees, r.critical)); } diff --git a/wai/wai-quantum-landscape-fpga/src/main.rs b/wai/wai-quantum-landscape-fpga/src/main.rs index d2fe8bde4..61b6d00f3 100644 --- a/wai/wai-quantum-landscape-fpga/src/main.rs +++ b/wai/wai-quantum-landscape-fpga/src/main.rs @@ -162,7 +162,7 @@ fn on_board(cmd: &str, args: &[String], port: P, info: BoardInfo) { "agree" => { let t = m.earn().unwrap_or_else(|e| die(e)); print!("{t}"); - println!("parity earned on this board: settle {}, sample {}", m.settle_parity.label(), m.sample_parity.label()); + println!("parity earned on this board, within its declared capacity: settle {}, sample {}", m.settle_parity.label(), m.sample_parity.label()); let rep = conform::agree(&m, flag(args, "--seeds").unwrap_or(200)).unwrap_or_else(|e| die(e)); finish(&rep) } @@ -297,6 +297,20 @@ macro_rules! with_link { fn main() { let args: Vec = std::env::args().skip(1).collect(); + // The host's audit demotions outlive a job (§7.3): they are kept in a file, so a board + // the audit demoted is not certified again on its next job, here or after a restart. + // The file is read when the board is certified, which is after its lock is taken; a + // demotion is kept at once in a file of its own beside it and then merged under the + // file's lock, waited for at most `member::STORE_LOCK_WAIT` while the board is held + // (where that runs out, it stays in its own file, which every certification reads); + // with no file to name (an empty variable names none), no kernel is graded bit-exact. + if std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").is_none_or(|v| v.is_empty()) { + use wai_quantum::quantum_landscape::member::{Demotions, keep_demotions}; + keep_demotions(match std::env::var_os("HOME").filter(|h| !h.is_empty()) { + Some(home) => Demotions::open(Some(std::path::PathBuf::from(home).join(".local/state/wai-quantum-landscape-fpga/demotions.json"))), + None => Demotions::broken("HOME is not set, so this host has no file to keep demotions in (set HOME, or WAI_LANDSCAPE_DEMOTIONS to a file)".into()), + }); + } let cmd = args.first().map(|s| s.as_str()).unwrap_or_else(|| usage()); match cmd { "selftest" => { diff --git a/wai/wai-quantum-landscape-fpga/src/member.rs b/wai/wai-quantum-landscape-fpga/src/member.rs index c0ea2f027..5e8405047 100644 --- a/wai/wai-quantum-landscape-fpga/src/member.rs +++ b/wai/wai-quantum-landscape-fpga/src/member.rs @@ -28,6 +28,11 @@ //! state, as the reference's do (`check_chain` fixes them for every member); the engine's own count //! of sweeps is held to its segment here. //! +//! **Its capacity** is declared, as numbers fixed by the engine ([`CAPACITY`]): 64 cells; above +//! temperature 0, fields to ±524,287 and a cost to 1,048,575, its 20-bit registers, and uncertain +//! fields within a span of 8,192 at a temperature, its table's window. Sweeps at 0 take any field. +//! The host runs it on no job past those numbers, and [`beyond`] says the same of every workload. +//! //! What it has earned is not assumed, and not said by the member: the host certifies it //! (`wai_quantum::quantum_landscape::member::certify`) by running the conformance set's `settle` //! and `chains` vectors on the device, and, where those miss, the statistical battery. @@ -36,7 +41,7 @@ use crate::engine::{Engine, Loaded}; use crate::port::Port; use crate::regs::*; use std::cell::RefCell; -use wai_quantum::quantum_landscape::conformance::MUST_TAKE_FIELD_BOUND; +use wai_quantum::quantum_landscape::capacity::{Capacity, Limits}; use wai_quantum::quantum_landscape::member::{Chain, KERNEL_SAMPLE, KERNEL_SETTLE, Member, Parity, certify}; use wai_quantum::quantum_landscape::sample::cuts; use wai_quantum::quantum_landscape::settle::{Outcome, Term, Workload, threshold}; @@ -44,6 +49,15 @@ use wai_quantum::quantum_landscape::settle::{Outcome, Term, Workload, threshold} /// The conformance set a member is held to, as the repository holds it. pub const VECTORS: &str = include_str!("../../quantum-conformance/landscape/vectors.json"); +/// What the engine holds, declared as `extensions/quantum-landscape.md` §5 asks: its cells; above +/// temperature 0, its field and cost registers and its table's window of fields a temperature. +pub const CAPACITY: Capacity = Capacity { + cells: Some(CELLS as u64), + above_zero: Limits { field: Some(FIELD_MAX), field_bound: None, cost: Some(COST_MAX) }, + uncertain_span: Some(TABLE as u64), + ..Capacity::NONE +}; + /// What the board is, as data: the member's name and its answers' label carry it. #[derive(Clone, Debug)] pub struct BoardInfo { @@ -205,6 +219,12 @@ pub fn plan(l: &Loaded, temps: &[f64]) -> Result, String> { let span = (hi - lo + 1) as u64; let wbits = 64 - (span - 1).leading_zeros().min(64); let wbits = if span <= 1 { 0 } else { wbits }; + // Fewer temperatures a table, down to one, before the window is past what it holds: + // only a single temperature's uncertain fields can be past it, as CAPACITY declares. + if wbits > MAX_WBITS && take > 1 { + take = (take / 2).max(1); + continue; + } if wbits > MAX_WBITS { return Err(format!( "the fields whose draw is uncertain run from {lo} to {hi}, {span} of them, and the engine's table holds {TABLE} a temperature" @@ -287,7 +307,7 @@ impl BoardMember

{ }; self.settle_parity = settle; self.sample_parity = sample; - Ok(format!("certified by the host on the conformance set: settle {}, sample {}\n", settle.label(), sample.label())) + Ok(format!("certified by the host on the conformance set, within its declared capacity: settle {}, sample {}. {}\n", settle.label(), sample.label(), CAPACITY.words(true).unwrap_or_default())) } } @@ -308,19 +328,16 @@ impl Member for BoardMember

{ ) } - /// Declines only what `extensions/quantum-landscape.md` §5 lets a member decline: sampling a - /// workload whose field bound is above 2^31 − 1. (Sweeps at 0 it takes at any field: the host - /// maps them exactly, `lowered_settle`.) Everything else it takes, and what the engine cannot - /// hold, more than 64 variables or sampling fields beyond its 20-bit registers, it then fails - /// with a sentence: the spec does not let a member decline those. - fn takes(&self, w: &Workload, sampling: bool) -> Result<(), String> { - if sampling && w.field_bound() > MUST_TAKE_FIELD_BOUND { - return Err(format!( - "its fields reach {}, past 2^31 - 1, and the engine samples fields of at most ±{FIELD_MAX}", - w.field_bound() - )); - } - Ok(()) + /// The engine's capacity, [`CAPACITY`]: the host declines for it exactly the jobs past it. + /// (Sweeps at 0 it takes at any field: the host maps them exactly, `lowered_settle`.) + fn capacity(&self) -> Capacity { + CAPACITY + } + + /// The engine the logic reports (its ID and version registers) and the board's + /// name: a demotion is kept under it, so a new engine is certified afresh. + fn fingerprint(&self) -> String { + format!("engine {:#010x} {:#010x} on {}", ID_VALUE, VERSION_VALUE, self.info.name) } fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { @@ -461,6 +478,111 @@ mod tests { assert_eq!(lowered_settle(&w), lowered(&w)); } + /// The declared capacity is the engine's, exactly: every settle and chains vector and every + /// battery workload the engine cannot hold is past the capacity, and every one it can is + /// within it. + #[test] + fn the_declared_capacity_is_exactly_what_the_engine_holds() { + use wai_quantum::quantum_landscape::conformance::{battery, chain_cases, settle_cases}; + let (mut within, mut past) = (0, 0); + let mut agree = |name: &str, w: &Workload, temps: &[f64]| { + let engine = beyond(w, temps); + let declared = CAPACITY.admits(w, temps); + assert_eq!(engine.is_some(), declared.is_err(), "{name}: the engine says {engine:?}, the capacity {declared:?}"); + if engine.is_some() { past += 1 } else { within += 1 } + }; + for c in settle_cases() { + agree(&c.name, &c.landscape.lower(), &[]); + } + for c in chain_cases() { + agree(&c.name, &c.landscape.lower(), &c.temperatures.each()); + } + for c in battery() { + agree(c.name, &c.landscape.lower(), &c.temperatures); + } + assert!(within > 0 && past > 0, "{within} within, {past} past"); + // The vectors certification adds at the capacity's edges are all within it, and the engine holds each. + let edges = CAPACITY.edges(); + for c in &edges.settle { + assert_eq!(beyond(&c.landscape.lower(), &[]), None, "{}", c.name); + } + for c in &edges.chains { + assert_eq!(beyond(&c.landscape.lower(), &c.temperatures.each()), None, "{}", c.name); + } + for c in &edges.battery { + assert_eq!(beyond(&c.landscape.lower(), &c.temperatures), None, "{}", c.name); + } + assert!(edges.settle.len() + edges.chains.len() >= 6, "an edge for each number it declares"); + } + + /// The review's probe (#678), kept: on 3,000 random workloads and schedules, cooling ones and + /// lists with zeros inside and out of order, the declared capacity and the engine agree on + /// every one, and on the model every one within it that is run gives the reference's bits. + #[test] + fn the_capacity_and_the_engine_agree_on_random_jobs() { + use wai_quantum::quantum_landscape::sample::{anneal_with, chain_seed, schedule, start}; + let mut x: u64 = 0x0F96_A678; + let mut next = move || { + x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + x >> 17 + }; + let m = model(); + let (mut admitted, mut declined, mut ran) = (0, 0, 0); + for trial in 0..3000u64 { + let n = 2 + (next() % 63) as usize; + let mut l = Landscape::new(n); + let density = [2u64, 5, 20, 60][(next() % 4) as usize]; + for i in 0..n { + for j in (i + 1)..n { + if next() % density == 0 { + l.couple(i, j, if next() % 2 == 0 { 1 } else { -1 }); + } + } + } + let scale = [2i64, 40, 3000, 100_000, 524_287, 600_000][(next() % 6) as usize]; + for i in 0..n { + if next() % 2 == 0 { + l.set_field(i, (next() as i64 % (2 * scale + 1)) - scale); + } + } + l.set_cost([0i64, 1, 8, 5000, 1_048_575][(next() % 5) as usize]); + let w = l.lower(); + let temps: Vec = if next() % 2 == 0 { + schedule(10f64.powf((next() % 1000) as f64 / 1000.0 * 9.0 - 3.0), 1 + (next() % 70) as u32) + } else { + (0..(1 + next() % 90)).map(|_| if next() % 5 == 0 { 0.0 } else { 10f64.powf((next() % 1000) as f64 / 1000.0 * 9.0 - 3.0) }).collect() + }; + let declared = CAPACITY.admits(&w, &temps); + let engine = beyond(&w, &temps); + assert_eq!(declared.is_ok(), engine.is_none(), "trial {trial}: the capacity says {declared:?}, the engine {engine:?}"); + if declared.is_err() { + declined += 1; + continue; + } + admitted += 1; + if ran < 250 && n <= 40 && temps.len() <= 40 { + ran += 1; + let chains: Vec = (0..2u64).map(|r| chain_seed(trial, r)).map(|s| Chain { start: start(s, n), seed: s }).collect(); + let got = m.chains(&w, &chains, &temps).unwrap_or_else(|e| panic!("trial {trial}: refused within its capacity: {e}")); + let want: Vec = chains.iter().map(|c| anneal_with(&w, &c.start, &temps, c.seed)).collect(); + assert_eq!(got, want, "trial {trial}: n={n}, {} sweeps", temps.len()); + } + } + eprintln!("random jobs: {admitted} admitted, {declined} declined, {ran} run on the model"); + assert!(admitted > 0 && declined > 0 && ran == 250, "{admitted} admitted, {declined} declined, {ran} run"); + } + + /// The host certifies the engine bit-exact for both kernels within its declared capacity, on + /// the register-level model the testbenches hold the logic to, against the set on main. + #[test] + fn the_model_is_certified_bit_exact_within_its_declared_capacity() { + use wai_quantum::quantum_landscape::member::certify; + let m = model(); + let c = certify(&m).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact)); + assert_eq!(c.capacity(), CAPACITY); + } + #[test] fn sampling_beyond_the_registers_is_refused() { let m = model(); diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index 8339148a7..fa965fc17 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -80,7 +80,7 @@ never creates information. | `quantum_resource_receipt` | a signed, energy-accounted receipt over an estimate, re-checkable bit for bit on any machine | | `quantum_qio` | **quantum-inspired optimisation**: Ising and Max-Cut problems (G-set reader, spin-glass generator), simulated bifurcation in its ballistic, discrete and heated forms (batched eight trials per pass, bit-identical to one at a time), simulated annealing, single-flip descent, an exact Gray-code referee up to 34 spins, and step-to-solution. Reaches the best-known cut on seven G-set instances | | `quantum_landscape` | **energy-landscape jobs**: a program in epuML states an energy over variables of three states (spin-1, with a single-ion cost), run on a classical emulator of the settling model: a deterministic kernel in whole numbers and a sampling kernel of one draw between two cut points, both the same bits on every platform; every arrangement checked up to 16 variables, each lowest one graded against the problem's own check; the joules priced at rates whose sources are named, never measured here; the same instance on simulated annealing, tabu search and greedy descent at the same budget, with the published table; and the seam a GPU or an FPGA board runs behind (`extensions/quantum-landscape.md`). `quantum_engine` and `full` both turn it on | -| `quantum_landscape_webgpu`, `quantum_landscape_cuda`, `quantum_landscape_metal` | **energy-landscape jobs on a GPU**: the settle and sampling kernels through WebGPU (32-bit lanes, and a coloured kernel that can be statistical at most), CUDA and Metal (64-bit fields), each member certified by the host on the conformance set and given only the workloads it takes, a job passing over an uncertified kernel or a member that fails it (`job::call_placed`) | +| `quantum_landscape_webgpu`, `quantum_landscape_cuda`, `quantum_landscape_metal` | **energy-landscape jobs on a GPU**: the settle and sampling kernels through WebGPU (32-bit lanes, and a coloured kernel that can be statistical at most), CUDA and Metal (64-bit fields), each member certified by the host on the conformance set within the capacity it declares and given no workload past it, a job passing over an uncertified kernel or a member that fails it (`job::call_placed`) | | `quantum_uf` | **union-find decoding in near-linear time** over the same matching graph: event-driven cluster growth on integer weights (a radix heap orders the fill events), merges, and peeling. Measured against exact matching on the same shots: within 0.92–1.18× the failures, up to 77× faster at `d = 11` | | `quantum_dmrg` | **ground states of spin chains by DMRG**: matrix-product operators for nearest-neighbour chains (XXZ Heisenberg in a field, transverse-field Ising), two-site sweeps with Lanczos and density-matrix truncation, entanglement entropies, a symmetric eigensolver, and two exact referees: full diagonalisation up to 22 sites, and the free-fermion solution of the Ising chain at any length. The critical Ising chain is reproduced to about `10⁻¹¹` per site at 200 sites | | `quantum_tdvp` | **time evolution of spin chains by TDVP**: two-site sweeps forward and back, Lanczos exponentials that halve a step they cannot converge, bonds split by singular value decomposition, and every basis completed with null vectors so a product state's small bonds do not hold the evolution back. Measures site and bond observables, entanglement at every bond, energy and the Loschmidt echo. Held to two exact referees: Krylov evolution of all amplitudes up to 20 sites, and the free-fermion solution of an Ising quench at any length. On a 100-site critical quench at bond 64 it stays within `10⁻⁹` of the exact answer until the entanglement fills the bond, and past that its error follows the discarded weight it reports | diff --git a/wai/wai-quantum/README.zh-CN.md b/wai/wai-quantum/README.zh-CN.md index cac2d0a01..6a09394e6 100644 --- a/wai/wai-quantum/README.zh-CN.md +++ b/wai/wai-quantum/README.zh-CN.md @@ -63,7 +63,7 @@ wai-quantum lab qutrit # d = 3,不只是量子比特 | `quantum_resource_receipt` | 针对一次估算的、带能耗核算的签名回执,可在任何机器上逐位重新校验 | | `quantum_qio` | **量子启发式优化**:Ising 与 Max-Cut 问题(G-set 读取器、自旋玻璃生成器),弹道式、离散式和加热式的模拟分岔(每遍批量运行八次试验,与逐次运行的结果逐位一致),模拟退火,单翻转下降,最多 34 个自旋的精确 Gray 码参照,以及求解所需步数(step-to-solution)。在七个 G-set 实例上达到已知最优割 | | `quantum_landscape` | **能量景观作业**:epuML 程序对三态变量(自旋 1,带单离子代价)给出能量,在沉降模型的经典模拟器上运行:整数运算的确定性核,以及在两个切点之间做一次抽样的采样核,两者在每个平台上比特一致;最多 16 个变量时检查每一种排列,并按问题自身的检查为每个最低能量状态评分;焦耳按注明来源的费率定价,这里从不测量;同一实例上以相同预算运行模拟退火、禁忌搜索和贪心下降,并发布结果表;以及 GPU 或 FPGA 板运行所经由的接缝(`extensions/quantum-landscape.md`)。`quantum_engine` 和 `full` 都会启用它 | -| `quantum_landscape_webgpu`、`quantum_landscape_cuda`、`quantum_landscape_metal` | **GPU 上的能量景观作业**:沉降核与采样核分别通过 WebGPU(32 位通道,另有至多只能达到统计一致的着色核)、CUDA 与 Metal(64 位场)运行,每个成员都由主机在一致性集合上认证,且只接收它能承接的工作负载;作业会跳过未经认证的核或运行失败的成员(`job::call_placed`) | +| `quantum_landscape_webgpu`、`quantum_landscape_cuda`、`quantum_landscape_metal` | **GPU 上的能量景观作业**:沉降核与采样核分别通过 WebGPU(32 位通道,另有至多只能达到统计一致的着色核)、CUDA 与 Metal(64 位场)运行,每个成员都由主机在其声明的容量内于一致性集合上认证,且不会被分到超出该容量的工作负载;作业会跳过未经认证的核或运行失败的成员(`job::call_placed`) | | `quantum_uf` | **近线性时间的并查集(union-find)解码**,基于同一个匹配图:在整数权重上由事件驱动的簇生长(用基数堆为填充事件排序)、合并以及剥离(peeling)。在相同的采样上与精确匹配对比:失败次数为其 0.92–1.18 倍,在 `d = 11` 时最多快 77 倍 | | `quantum_dmrg` | **用 DMRG 求自旋链的基态**:最近邻链的矩阵乘积算符(外场中的 XXZ Heisenberg 链、横场 Ising 链),带 Lanczos 和密度矩阵截断的双格点扫描,纠缠熵,一个对称矩阵本征求解器,以及两个精确参照:最多 22 个格点的完全对角化,以及任意长度 Ising 链的自由费米子解。在 200 个格点上,临界 Ising 链的复现精度约为每格点 `10⁻¹¹` | | `quantum_tdvp` | **用 TDVP 计算自旋链的时间演化**:前后往返的双格点扫描,遇到无法收敛的步长就将其减半的 Lanczos 指数运算,用奇异值分解拆分键,并用零空间向量补全每个基,使乘积态的小键不会阻碍演化。测量格点和键上的可观测量、每个键上的纠缠、能量以及 Loschmidt 回波。以两个精确参照为准:最多 20 个格点的全振幅 Krylov 演化,以及任意长度 Ising 淬火的自由费米子解。在键维数 64、100 个格点的临界淬火中,直到纠缠填满键之前,它与精确解的偏差都保持在 `10⁻⁹` 以内;超过之后,其误差随它所报告的舍弃权重变化 | diff --git a/wai/wai-quantum/docs/landscape.md b/wai/wai-quantum/docs/landscape.md index 606b9eeb6..999ba5585 100644 --- a/wai/wai-quantum/docs/landscape.md +++ b/wai/wai-quantum/docs/landscape.md @@ -176,12 +176,13 @@ The kernels run on a GPU too, as members the host certifies and checks like any | feature | member | lanes | parity it can earn | |---|---|---|---| -| `quantum_landscape_webgpu` | `quantum_landscape::webgpu`: Metal, Vulkan or DX12 through WebGPU | 32-bit: it declines a workload whose field bound is above 2³¹ − 1 | bit-exact for both kernels; its coloured kernel, a colour class of cells at once, statistical at most | -| `quantum_landscape_cuda` | `quantum_landscape::cuda`: an NVIDIA GPU, compiled at run time | 64-bit: it takes every workload | bit-exact for both kernels | -| `quantum_landscape_metal` | `quantum_landscape::metal`: an Apple GPU, macOS | 64-bit: it takes every workload | bit-exact for both kernels | +| `quantum_landscape_webgpu` | `quantum_landscape::webgpu`: Metal, Vulkan or DX12 through WebGPU | 32-bit: it declares a field bound of 2³¹ − 1, cells and couplings within a 32nd of a storage binding, and 4,194,304 entries of cut points a sweep | bit-exact for both kernels within that capacity; its coloured kernel, a colour class of cells at once, statistical at most | +| `quantum_landscape_cuda` | `quantum_landscape::cuda`: an NVIDIA GPU, compiled at run time | 64-bit fields; it declares 4,194,304 entries of cut points a sweep, which no job reaches | bit-exact for both kernels within that capacity | +| `quantum_landscape_metal` | `quantum_landscape::metal`: an Apple GPU, macOS | 64-bit fields; it declares 4,194,304 entries of cut points a sweep, which no job reaches | bit-exact for both kernels within that capacity | A parity in this table is what a member can earn, not what any one device has: the host finds it -on each device, every time it certifies one, and states it in every answer. +on each device, every time it certifies one, within the capacity the member declares and at that +capacity's edges, and states both in every answer. The settle kernel runs one thread a cell and one dispatch a sweep. The sampling kernel runs one thread a chain, its cells in the reference's order, with the reference's draws and the cut points the @@ -191,9 +192,9 @@ starts of a run and the seeds of a sweep, a comparison or a replay. A member run a dispatch and waits for each before it sends the next. A workload's field bound is the largest field any state can give a cell, `max_i (|b_i| + deg_i)`. -A member may decline a workload only where that is above 2³¹ − 1, and the host never runs it on one -it declines. Placing a job, the host passes over a member whose kernel for the job is not certified, -one that declines the workload, and one that fails the job or whose answer its checks refuse, and +Each member declares its capacity (`Member::capacity`) and the host runs it on nothing past it. +Placing a job, the host passes over a member whose kernel for the job is not certified, one whose +capacity the workload does not fit, and one that fails the job or whose answer its checks refuse, and names each under `refusals`; offered last, the reference answers what the others did not. ```rust norun=account @@ -228,3 +229,51 @@ the host checks every answer a member returns, and finds its parity by running i [the conformance set](../../quantum-conformance/landscape/vectors.json) (`member::certify`). A member says nothing of its own parity. [The extension](../../extensions/quantum-landscape.md#5-members) states what a member takes and returns, what the host checks, and what each parity takes. + +A member declares what its kernels hold, once, as fixed numbers (`member::Member::capacity`, a +[`capacity::Capacity`](../src/quantum_landscape/capacity.rs)): its cells and couplings; on each side +of temperature 0, its largest field, field bound and cost; above 0, how many cut points a sweep it +holds or how wide a span of uncertain fields; how many sweeps a run; and how many chains a call, +which is a batch and not a limit: the host splits a job, and certification every vector, into calls +of that many. The host declines for it exactly the jobs past those numbers (`Capacity::admits`, a +function of the job and the numbers alone). It certifies it on every vector and battery workload +within them, and on vectors it generates from them by rule (`Capacity::generate`): every cell +count at and beside a power of two, every call size to 32 chains and each band of a wavefront, the longest row, fields at each +lane's wrap points, a corner with every number at its most, a run past a 24-bit draw index, and +workloads at exactly each number declared. It states the numbers in every answer: `member.capacity`, +and a sentence in the label. A member that refuses or fails a case within its capacity loses the +grade, and one that declares less than the floor (`capacity::floor`, what the conformance set's core +cases need: 32 cells, fields to ±40, a cost of 8, a run of 5,000 sweeps and so on) is not certified +at all. + +Then the host audits it. For every answer from a kernel graded `bit-exact`, it runs again on the +reference, in every call, the call's last chain and one chain of each seed's run in it, drawn afresh +with a key the host makes at start, before the next call: on the job's own thread at the job's +priority, and on as many of the audit's threads as are free, a quarter of the host's cores and 4 at +most over the whole process, each at a low priority (`run::audit_threads`); and the +reference's settle as far as a budget of 2²⁴ of the work the host asked for (the whole settle past +that one job in 8). Each audited answer says what was checked (`audit`). At the first difference it +demotes the member to `uncertified` for both kernels, answers that job from the reference with the +member named under `refusals`, and every later answer says so (`member.demoted`). The demotion is +kept under the member's id and fingerprint (`member::Demotions`, in the file +`WAI_LANDSCAPE_DEMOTIONS` names, or a link's target: kept at once in a file of its own beside it and +merged under a lock waited for at most 5 s; read, with every demotion in a file of its own, at every +certification, which then rewrites the file as a demotion would), so certifying again does not +clear it; where that file cannot be read or rewritten, has a second name, or is reached by a link in +a sticky folder anyone may write, no kernel is graded `bit-exact`. A kernel graded `statistical` is +placed only where a job asks for it (`"parity": "statistical"`), and its answers say what the grade +rests on: the battery's workloads alone, which a fault at a count of cells they do not run, or a law +near the exact one, can pass. + +```rust norun=account +use wai_quantum::quantum_landscape::capacity::{Capacity, Limits}; + +// A board of 64 cells whose sampling registers hold fields to ±524,287 and a cost to 1,048,575, +// and whose table holds a window of 8,192 fields a temperature; at temperature 0, any field. +let board = Capacity { + cells: Some(64), + above_zero: Limits { field: Some(524_287), field_bound: None, cost: Some(1_048_575) }, + uncertain_span: Some(8_192), + ..Capacity::NONE +}; +``` diff --git a/wai/wai-quantum/schemas/landscape.v1.json b/wai/wai-quantum/schemas/landscape.v1.json index b2f6ee230..6008c954d 100644 --- a/wai/wai-quantum/schemas/landscape.v1.json +++ b/wai/wai-quantum/schemas/landscape.v1.json @@ -128,6 +128,62 @@ "uncertified" ], "description": "What the member has earned for this kernel." + }, + "demoted": { + "description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.", + "type": "string" + }, + "capacity": { + "description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.", + "type": "object", + "properties": { + "cells": { + "type": "integer" + }, + "couplings": { + "type": "integer" + }, + "at_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "sweeps": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" + } + } } } }, @@ -651,6 +707,21 @@ "type": "string", "description": "BLAKE3 of the protocol, the program, the seed, the plan, the state and its energy." }, + "audit": { + "type": "object", + "description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "checked" + ], + "properties": { + "checked": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, "refusals": { "type": "array", "description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).", @@ -754,6 +825,62 @@ } } } + }, + "demoted": { + "description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.", + "type": "string" + }, + "capacity": { + "description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.", + "type": "object", + "properties": { + "cells": { + "type": "integer" + }, + "couplings": { + "type": "integer" + }, + "at_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "sweeps": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" + } + } } } }, @@ -857,6 +984,21 @@ } } }, + "audit": { + "type": "object", + "description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "checked" + ], + "properties": { + "checked": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, "refusals": { "type": "array", "description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).", @@ -957,6 +1099,62 @@ } } } + }, + "demoted": { + "description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.", + "type": "string" + }, + "capacity": { + "description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.", + "type": "object", + "properties": { + "cells": { + "type": "integer" + }, + "couplings": { + "type": "integer" + }, + "at_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "sweeps": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" + } + } } } }, @@ -1047,6 +1245,21 @@ } } }, + "audit": { + "type": "object", + "description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "checked" + ], + "properties": { + "checked": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, "refusals": { "type": "array", "description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).", @@ -1116,6 +1329,62 @@ "uncertified" ], "description": "What the member has earned for this kernel." + }, + "demoted": { + "description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.", + "type": "string" + }, + "capacity": { + "description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.", + "type": "object", + "properties": { + "cells": { + "type": "integer" + }, + "couplings": { + "type": "integer" + }, + "at_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "sweeps": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" + } + } } } }, @@ -1125,6 +1394,21 @@ "lines": { "type": "string" }, + "audit": { + "type": "object", + "description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "checked" + ], + "properties": { + "checked": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, "refusals": { "type": "array", "description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).", diff --git a/wai/wai-quantum/src/quantum_landscape.rs b/wai/wai-quantum/src/quantum_landscape.rs index 60cbf154b..34d015213 100644 --- a/wai/wai-quantum/src/quantum_landscape.rs +++ b/wai/wai-quantum/src/quantum_landscape.rs @@ -82,6 +82,7 @@ //! coupling read, one cell updated or one move considered, whoever does it. //! It says nothing of cells that settle side by side. +pub mod capacity; pub mod classical; pub mod conformance; pub mod energy; diff --git a/wai/wai-quantum/src/quantum_landscape/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs new file mode 100644 index 000000000..f24ed9ee8 --- /dev/null +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -0,0 +1,1780 @@ +//! A member's declared capacity (`extensions/quantum-landscape.md` §5): the +//! fixed numbers a device's kernels need, stated before anything runs, and the +//! one pure function of a job and those numbers that says whether it fits. +//! +//! Real hardware holds so many cells, fields so wide, a table so large. A +//! member says so once, as a [`Capacity`], and the host declines for it +//! exactly the jobs past those numbers ([`Capacity::admits`]): never a choice +//! the member makes job by job, so it cannot decline a case it would get +//! wrong. The host certifies it on every vector and battery workload within +//! its capacity, and every answer and refusal states that capacity. +//! +//! A capacity under [`floor`] cannot be certified: the floor is the least +//! capacity that takes every case of the core set, so a member cannot be +//! certified by declaring itself too small to be held to anything. +//! +//! A declared number takes vectors out of what a member is held to, and the +//! set samples each axis at a few points only. So for every number a member +//! declares below what a job can reach, certification adds vectors at that +//! number ([`Capacity::edges`]): the grade is earned at the edge of the +//! capacity, not at the nearest vector under it. `chains_per_call` is not a +//! limit but a batch: the host splits a job, and certification splits every +//! vector, into calls of that many. + +use super::conformance; +use super::energy::Landscape; +use super::reach; +use super::sample::cuts; +use super::settle::{Workload, threshold}; +use serde_json::{Map, Value, json}; +use std::sync::OnceLock; + +/// Limits on one side of temperature 0. `None` is no limit. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Limits { + /// The largest field as a program gives it, `|b_i|`. + pub field: Option, + /// The largest field bound, `max_i (|b_i| + deg_i)` (§4.1). + pub field_bound: Option, + /// The largest cost. + pub cost: Option, +} + +impl Limits { + pub const NONE: Limits = Limits { field: None, field_bound: None, cost: None }; + + fn is_none(&self) -> bool { + *self == Limits::NONE + } +} + +/// What a member's kernels hold, fixed and declared up front. `None` is no limit. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Capacity { + /// The most cells a workload may have. + pub cells: Option, + /// The most couplings, each pair counted twice (`2P`). + pub couplings: Option, + /// For a run whose sweeps include one at temperature 0: a settle, a quench, the end of a schedule. + pub at_zero: Limits, + /// For a run with any sweep above temperature 0. + pub above_zero: Limits, + /// Above temperature 0: the most entries in one sweep's table of cut points + /// over the fields the cells can reach ([`reach::reach`]). + pub table_entries: Option, + /// Above temperature 0: the widest span, first to last, of the fields the + /// cells can reach whose draw is uncertain at a sweep's temperature. A draw + /// is certain where its cut points are the threshold's, `(0, 0)`, + /// `(0, 2⁵³)` or `(2⁵³, 2⁵³)`, and a device can take such a field without + /// a table entry. Those fields at a temperature hold those at any lower + /// one, so the hottest sweep's span is the widest. + pub uncertain_span: Option, + /// The most sweeps one run may have: a settle's bound, a schedule's length. + pub sweeps: Option, + /// The most chains it takes in one call. Not a limit on a job: the host + /// splits a job, and certification splits every vector, into calls of at + /// most this many. + pub chains_per_call: Option, +} + +/// The fields of a draw that is certain: the threshold's cut points. +fn certain(h: i64, cost: i64) -> (u64, u64) { + const TWO53: u64 = 1 << 53; + match threshold(h, cost) { + 1 => (0, 0), + -1 => (TWO53, TWO53), + _ => (0, TWO53), + } +} + +/// The span, first to last, of the fields the cells of `w` can reach whose draw +/// is uncertain at temperature `t`; 0 where none is. +pub fn uncertain_span(w: &Workload, t: f64) -> u64 { + let beta = 1.0 / t; + let c = w.cost(); + let r = reach::reach(w); + let uncertain = |h: i64| cuts(h, c, beta) != certain(h, c); + let first = r.fields().find(|&h| uncertain(h)); + let last = r.fields_back().find(|&h| uncertain(h)); + match (first, last) { + (Some(a), Some(b)) => (b - a) as u64 + 1, + _ => 0, + } +} + +fn group(v: impl Into) -> String { + super::price::group(v.into().unsigned_abs() as u64) +} + +impl Capacity { + /// No declared limit: the CPU reference's. + pub const NONE: Capacity = Capacity { cells: None, couplings: None, at_zero: Limits::NONE, above_zero: Limits::NONE, table_entries: None, uncertain_span: None, sweeps: None, chains_per_call: None }; + + /// Whether any limit is declared. + pub fn is_declared(&self) -> bool { + *self != Capacity::NONE + } + + /// Whether a settle of `w` of at most `bound` sweeps fits. + pub fn admits_settle(&self, w: &Workload, bound: u32) -> Result<(), String> { + if let Some(s) = self.sweeps + && u64::from(bound) > s + { + return Err(format!("past its declared capacity: a settle of up to {bound} sweeps, and it holds {s}")); + } + self.admits(w, &[]) + } + + /// Whether a workload run at `temperatures` fits, or which declared number + /// it is past, in a sentence. A settle's limits are those at temperature 0 + /// ([`Capacity::admits_settle`] adds its bound). A pure function of the job + /// and the declared numbers. + pub fn admits(&self, w: &Workload, temperatures: &[f64]) -> Result<(), String> { + let past = |what: String| Err(format!("past its declared capacity: {what}")); + if let Some(s) = self.sweeps + && temperatures.len() as u64 > s + { + return past(format!("{} sweeps, and it holds {s}", temperatures.len())); + } + let n = w.n() as u64; + if let Some(c) = self.cells + && n > c + { + return past(format!("{n} cells, and it holds {c}")); + } + if let Some(c) = self.couplings + && w.nonzeros() > c + { + return past(format!("{} couplings, and it holds {c}", w.nonzeros())); + } + let hot: Vec = temperatures.iter().copied().filter(|&t| t > 0.0).collect(); + let cold = temperatures.is_empty() || hot.len() < temperatures.len(); + let zero = vec![0i8; w.n()]; + let field = (0..w.n()).map(|i| w.field(&zero, i).unsigned_abs()).max().unwrap_or(0); + let held = |l: &Limits, side: &str| -> Result<(), String> { + if let Some(c) = l.field + && field > c.unsigned_abs() + { + return past(format!("a field of {field} {side}, and it holds fields to ±{c}")); + } + if let Some(c) = l.field_bound + && w.field_bound() > c + { + return past(format!("its field bound is {} {side}, and it holds {c}", w.field_bound())); + } + if let Some(c) = l.cost + && w.cost() > c + { + return past(format!("a cost of {} {side}, and it holds {c}", w.cost())); + } + Ok(()) + }; + if cold { + held(&self.at_zero, "at temperature 0")?; + } + if !hot.is_empty() { + held(&self.above_zero, "above temperature 0")?; + if let Some(c) = self.table_entries { + let e = reach::reach(w).entries; + if e > c { + return past(format!("a sweep's cut points are {e} entries, and it holds {c}")); + } + } + if let Some(c) = self.uncertain_span { + let t = hot.iter().copied().fold(f64::MIN, f64::max); + let s = uncertain_span(w, t); + if s > c { + return past(format!("its uncertain fields at temperature {t} span {s}, and it holds a span of {c}")); + } + } + } + Ok(()) + } + + /// The least capacity that takes a run of `w` at `temperatures`, `sweeps` + /// sweeps long (a settle: no temperatures, and its bound): each number the + /// run's own, 0 on a side of temperature 0 it has no sweep on, and no batch. + pub fn least(w: &Workload, temperatures: &[f64], sweeps: u64) -> Capacity { + let zero = vec![0i8; w.n()]; + let field = (0..w.n()).map(|i| w.field(&zero, i).abs()).max().unwrap_or(0); + let hot: Vec = temperatures.iter().copied().filter(|&t| t > 0.0).collect(); + let cold = temperatures.is_empty() || hot.len() < temperatures.len(); + let side = |on: bool| if on { Limits { field: Some(field), field_bound: Some(w.field_bound()), cost: Some(w.cost()) } } else { Limits { field: Some(0), field_bound: Some(0), cost: Some(0) } }; + let hottest = hot.iter().copied().fold(f64::MIN, f64::max); + Capacity { + cells: Some(w.n() as u64), + couplings: Some(w.nonzeros()), + at_zero: side(cold), + above_zero: side(!hot.is_empty()), + table_entries: Some(if hot.is_empty() { 0 } else { reach::reach(w).entries }), + uncertain_span: Some(if hot.is_empty() { 0 } else { uncertain_span(w, hottest) }), + sweeps: Some(sweeps), + chains_per_call: None, + } + } + + /// Each number the larger of the two; no limit where either declares none. + pub fn join(&self, o: &Capacity) -> Capacity { + fn u(a: Option, b: Option) -> Option { + Some(a?.max(b?)) + } + fn i(a: Option, b: Option) -> Option { + Some(a?.max(b?)) + } + let l = |a: &Limits, b: &Limits| Limits { field: i(a.field, b.field), field_bound: i(a.field_bound, b.field_bound), cost: i(a.cost, b.cost) }; + Capacity { + cells: u(self.cells, o.cells), + couplings: u(self.couplings, o.couplings), + at_zero: l(&self.at_zero, &o.at_zero), + above_zero: l(&self.above_zero, &o.above_zero), + table_entries: u(self.table_entries, o.table_entries), + uncertain_span: u(self.uncertain_span, o.uncertain_span), + sweeps: u(self.sweeps, o.sweeps), + chains_per_call: u(self.chains_per_call, o.chains_per_call), + } + } + + /// The most chains the host gives it in one call, of `host_most`. + pub fn batch(&self, host_most: usize) -> usize { + self.chains_per_call.map_or(host_most, |c| (c.min(host_most.max(1) as u64) as usize).max(1)) + } + + /// Where a declared number is under the floor's, a sentence for each. + pub fn under(&self, floor: &Capacity) -> Vec { + let mut out = Vec::new(); + let mut check = |what: &str, v: Option, f: Option| { + if let (Some(v), Some(f)) = (v, f) + && v < f + { + out.push(format!("{what} {v}, and the floor is {f}")); + } + }; + let i = |v: Option| v.map(i128::from); + let s = |v: Option| v.map(i128::from); + check("cells", i(self.cells), i(floor.cells)); + check("couplings", i(self.couplings), i(floor.couplings)); + for (side, l, f) in [("at temperature 0", &self.at_zero, &floor.at_zero), ("above temperature 0", &self.above_zero, &floor.above_zero)] { + check(&format!("fields {side}"), s(l.field), s(f.field)); + check(&format!("a field bound {side}"), s(l.field_bound), s(f.field_bound)); + check(&format!("a cost {side}"), s(l.cost), s(f.cost)); + } + check("table entries", i(self.table_entries), i(floor.table_entries)); + check("an uncertain span", i(self.uncertain_span), i(floor.uncertain_span)); + check("sweeps", i(self.sweeps), i(floor.sweeps)); + check("chains a call", i(self.chains_per_call), i(floor.chains_per_call)); + out + } + + /// The declared numbers as an answer states them; null where none is + /// declared, and an answer then leaves `capacity` out. + pub fn json(&self) -> Value { + if !self.is_declared() { + return Value::Null; + } + let mut o = Map::new(); + let mut put = |k: &str, v: Option| { + if let Some(v) = v { + o.insert(k.into(), v); + } + }; + let limits = |l: &Limits| { + (!l.is_none()).then(|| { + let mut m = Map::new(); + for (k, v) in [("field", l.field), ("field_bound", l.field_bound), ("cost", l.cost)] { + if let Some(v) = v { + m.insert(k.into(), json!(v)); + } + } + Value::Object(m) + }) + }; + put("cells", self.cells.map(Value::from)); + put("couplings", self.couplings.map(Value::from)); + put("at_zero", limits(&self.at_zero)); + put("above_zero", limits(&self.above_zero)); + put("table_entries", self.table_entries.map(Value::from)); + put("uncertain_span", self.uncertain_span.map(Value::from)); + put("sweeps", self.sweeps.map(Value::from)); + put("chains_per_call", self.chains_per_call.map(Value::from)); + Value::Object(o) + } + + /// The declared numbers in words, for an answer's label; `None` where none + /// is declared. `certified` is whether the host certified the member, or + /// runs it by name without a grade. + pub fn words(&self, certified: bool) -> Option { + if !self.is_declared() { + return None; + } + let mut parts = Vec::new(); + if let Some(c) = self.cells { + parts.push(format!("at most {} cells", group(c))); + } + if let Some(c) = self.couplings { + parts.push(format!("at most {} couplings", group(c))); + } + let side = |l: &Limits| { + let mut s = Vec::new(); + if let Some(c) = l.field { + s.push(format!("fields to ±{}", group(c))); + } + if let Some(c) = l.field_bound { + s.push(format!("a field bound to {}", group(c))); + } + if let Some(c) = l.cost { + s.push(format!("a cost to {}", group(c))); + } + s + }; + let at = side(&self.at_zero); + if !at.is_empty() { + parts.push(format!("at temperature 0, {}", at.join(", "))); + } + let mut above = side(&self.above_zero); + if let Some(c) = self.table_entries { + above.push(format!("{} entries of cut points a sweep", group(c))); + } + if let Some(c) = self.uncertain_span { + above.push(format!("uncertain fields within a span of {} at a temperature", group(c))); + } + if !above.is_empty() { + parts.push(format!("above temperature 0, {}", above.join(", "))); + } + if let Some(c) = self.sweeps { + parts.push(format!("at most {} sweeps a run", group(c))); + } + let batch = self.chains_per_call.map(|c| { + if certified { + format!("It takes at most {} chains a call: the host splits a job, as certification split every vector, into calls of that many.", group(c)) + } else { + format!("It takes at most {} chains a call, and the host splits a job into calls of that many.", group(c)) + } + }); + let limits = (!parts.is_empty()).then(|| { + if certified { + format!("Its parity holds within the capacity it declares, and the host runs it on no job past it: {}.", parts.join("; ")) + } else { + format!("It declares a capacity, and the host runs it on no job past it: {}.", parts.join("; ")) + } + }); + Some([limits, batch].into_iter().flatten().collect::>().join(" ")) + } +} + +/// The least capacity that takes every case of the core set +/// ([`conformance::core_settle_cases`], [`conformance::core_chain_cases`], +/// [`conformance::core_battery`]): a member that declares less cannot be +/// certified. Worked out from those cases, so it moves only with them. It +/// declares no batch: any number of chains a call is certified by splitting. +pub fn floor() -> Capacity { + static FLOOR: OnceLock = OnceLock::new(); + *FLOOR.get_or_init(|| { + let settles = conformance::core_settle_cases().into_iter().map(|c| Capacity::least(&c.landscape.lower(), &[], u64::from(c.bound))); + let chains = conformance::core_chain_cases().into_iter().map(|c| { + let t = c.temperatures.each(); + Capacity::least(&c.landscape.lower(), &t, t.len() as u64) + }); + let battery = conformance::core_battery().into_iter().map(|c| Capacity::least(&c.landscape.lower(), &c.temperatures, c.temperatures.len() as u64)); + settles.chain(chains).chain(battery).reduce(|a, b| a.join(&b)).expect("the core set has cases") + }) +} + +// ── generated vectors ─────────────────────────────────────────────────────────────────────── + +/// The most a job can reach on each axis (§2.0). A declared number at or above +/// one removes no job. +pub mod reachable { + use super::super::{MAX_FIELD, MAX_PROGRAM_BYTES, MAX_STEPS, MAX_VARS}; + /// Cells. + pub const CELLS: u64 = MAX_VARS as u64; + /// Couplings, each pair counted twice: the shortest line that couples two + /// cells is ten bytes, so a program holds at most a tenth of its bytes in pairs. + pub const COUPLINGS: u64 = 2 * (MAX_PROGRAM_BYTES as u64 / 10); + /// A field `|b_i|`, and a cost. + pub const FIELD: i64 = MAX_FIELD; + /// A field bound: a field and a coupling to every other cell. + pub const FIELD_BOUND: i64 = MAX_FIELD + MAX_VARS as i64 - 1; + /// One sweep's cut points: at most `2·couplings + cells`. + pub const TABLE_ENTRIES: u64 = 2 * COUPLINGS + CELLS; + /// The span of the fields the cells can reach. + pub const UNCERTAIN_SPAN: u64 = 2 * FIELD_BOUND as u64 + 1; + /// The sweeps of one run. + pub const SWEEPS: u64 = MAX_STEPS as u64; +} + +/// The widths of lane whose wrap points the generated vectors hold fields at: +/// `2^(L−1) − 1`, `2^(L−1)`, `3·2^(L−2)` and `2^L`, of both signs, wherever a +/// capacity lets a field or a field bound reach them. +pub const LANES: [u32; 3] = [16, 24, 32]; + +/// The most vectors [`Capacity::generate`] makes for any capacity (§7.1). +pub const MOST_GENERATED: u64 = 300; +/// The most cell sweeps, hot or settled, its vectors and battery workloads hold. +pub const MOST_GENERATED_WORK: u64 = 450_000_000; +/// The most cell sweeps at temperature 0 its vectors hold. +pub const MOST_GENERATED_COLD: u64 = 20_000_000; + +/// The draw indices the generated vectors carry a run past, wherever a +/// capacity lets `sweeps · cells` reach them: a counter of 24 bits. A run past +/// 2³¹ costs at least 2³¹ updates in one chain, minutes on a device that runs a +/// chain a thread, so a narrower index than 64 bits past 24 is left to the +/// audit (§7.3), which catches it on the first job past its width. +pub const DRAW_INDICES: [u32; 1] = [24]; + +/// The cell counts of the generated ladder: every power of two, one less and one more, from 1 to `most`, and `most` +/// and one less. +pub fn ladder_sizes(most: usize) -> Vec { + let mut out = Vec::new(); + let mut p = 2usize; + while p - 1 <= most { + out.extend([p - 1, p, p + 1]); + p *= 2; + } + out.extend([most.saturating_sub(1), most]); + out.retain(|&n| (1..=most).contains(&n)); + out.sort_unstable(); + out.dedup(); + out +} + +/// One generated vector. +pub enum Generated { + Settle(conformance::SettleCase), + Chains(conformance::ChainCase), + Battery(conformance::BatteryCase), +} + +/// Which kinds of generated vector to make: the others are not built at all. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Want { + pub settle: bool, + pub chains: bool, + pub battery: bool, +} + +impl Want { + pub const ALL: Want = Want { settle: true, chains: true, battery: true }; + pub const SETTLE: Want = Want { settle: true, chains: false, battery: false }; + pub const CHAINS: Want = Want { settle: false, chains: true, battery: false }; + pub const BATTERY: Want = Want { settle: false, chains: false, battery: true }; +} + +/// The vectors certification generates for a capacity ([`Capacity::edges`]): +/// their expected bits are the reference's. +#[derive(Default)] +pub struct Edges { + pub settle: Vec, + pub chains: Vec, + pub battery: Vec, +} + +fn landscape(n: usize, couplings: &[(usize, usize, i8)], fields: &[(usize, i64)], cost: i64) -> Landscape { + let mut l = Landscape::new(n); + for &(a, b, w) in couplings { + l.couple(a, b, w); + } + for &(i, b) in fields { + l.set_field(i, b); + } + l.set_cost(cost); + l +} + +/// `n` cells and `pairs` couplings, each as close to its neighbours as can be: +/// cell `i` to `i + 1`, then to `i + 2`, and on, no cell coupled to more than +/// `degree` others, alternating in sign by stride. A field of 3 on cell 0 sets +/// a wave down it. +fn banded(n: usize, pairs: usize, degree: usize) -> Landscape { + let mut l = Landscape::new(n); + let mut left = pairs; + let mut deg = vec![0usize; n]; + 'strides: for k in 1..n { + for i in 0..n - k { + if left == 0 { + break 'strides; + } + if deg[i] < degree && deg[i + k] < degree { + l.couple(i, i + k, if k % 2 == 1 { 1 } else { -1 }); + deg[i] += 1; + deg[i + k] += 1; + left -= 1; + } + } + if 2 * k >= degree + 2 { + break; + } + } + if n > 0 { + l.set_field(0, 3); + } + l +} + +/// Three cells: cell 0 with field `b`, coupled to cells 1 and 2, whose fields +/// are its sign. Its field bound is `|b| + 2`. +fn star(b: i64) -> Landscape { + landscape(3, &[(0, 1, 1), (0, 2, 1)], &[(0, b), (1, b.signum()), (2, b.signum())], 0) +} + +/// Three cells: cell 0 coupled to cells 1 and 2, whose fields of the same sign +/// and up to 100 in size hold them at that sign, so that from the second sweep +/// cell 0's field is `±b` exactly: a field bound of `b`, its fields within +/// `most`. None where `b` is too small for one. +fn pinned(b: i64, sign: i64, most: i64, cost: i64) -> Option { + let (p, f0) = (100.min(b - 1).min(most), (b - 2).min(most)); + (p >= 1 && f0 >= 0).then(|| landscape(3, &[(0, 1, 1), (0, 2, 1)], &[(0, sign * f0), (1, sign * p), (2, sign * p)], cost)) +} + +/// A battery workload of three alike cells at field `f` and cost `c`, held at +/// `t`: one block of 27 states, which a correct sampler fails far more rarely +/// than a block of one cell's three. +fn alike3(name: &'static str, f: i64, c: i64, t: f64) -> conformance::BatteryCase { + conformance::BatteryCase { name, landscape: singles(&[f, f, f], c), temperatures: vec![t; 32], law: conformance::Law::Settled, block: 3, read: Vec::new() } +} + +/// Units of two cells, each cell opposed to its partner and to the other cell of the +/// next unit, with fields `[0, −5]` on the first unit, `[1, 2]` between and `[−4, 5]` +/// on the last: from every cell at 0 it moves a cell in every sweep up to sweep +/// `2n − 10` (`4u − 10` for `u` units), and rests at `2n − 9`. It is `3u − 2` +/// couplings: fewer sweeps a cell than [`slower`] moves, and more a coupling, so where +/// couplings are scarce it is the longer-moving of the two. +fn slow(units: usize) -> Landscape { + let mut l = Landscape::new(2 * units); + for k in 0..units { + l.couple(2 * k, 2 * k + 1, -1); + if k + 1 < units { + l.couple(2 * k, 2 * k + 3, -1).couple(2 * k + 1, 2 * k + 2, -1); + } + let f: [i64; 2] = if k == 0 { [0, -5] } else if k + 1 == units { [-4, 5] } else { [1, 2] }; + l.set_field(2 * k, f[0]).set_field(2 * k + 1, f[1]); + } + l +} + +/// The last sweep in which [`slow`]`(units)` moves a cell. +fn slow_moves_to(units: usize) -> u32 { + (4 * units as u32).saturating_sub(10) +} + +/// Units of three cells, each unit's third cell agreeing with the other two, and +/// each unit coupled to the next two by fixed patterns, with fields `[−2, 1, 6]` +/// on the first unit, `[0, 3, −1]` between and `[0, −5, 0]` on the last: from every +/// cell at 0 it moves a cell in every sweep up to sweep `11u − 59` (`u` units, at +/// least 10), and rests at `11u − 58`, three and two thirds sweeps a cell. It is +/// `13u − 16` couplings. A search over units of three found it. It is no bound on +/// how long a settle can move: none is known, and the rules allow far more +/// couplings a cell than it has. +fn slower(units: usize) -> Landscape { + slower_and(units, 0) +} + +/// [`slower`]`(units)`, and `more` cells after it with nothing set. +fn slower_and(units: usize, more: usize) -> Landscape { + const WITHIN: [(usize, usize, i8); 2] = [(0, 2, 1), (1, 2, 1)]; + const NEXT: [(usize, usize, i8); 6] = [(0, 0, 1), (0, 1, -1), (1, 2, 1), (2, 0, -1), (2, 1, 1), (2, 2, 1)]; + const AFTER: [(usize, usize, i8); 5] = [(0, 0, 1), (0, 2, -1), (2, 0, -1), (2, 1, -1), (2, 2, 1)]; + let mut l = Landscape::new(3 * units + more); + for k in 0..units { + for (to, pattern) in [(0, &WITHIN[..]), (1, &NEXT[..]), (2, &AFTER[..])] { + if k + to < units { + for &(a, b, w) in pattern { + l.couple(3 * k + a, 3 * (k + to) + b, w); + } + } + } + let f: [i64; 3] = if k == 0 { [-2, 1, 6] } else if k + 1 == units { [0, -5, 0] } else { [0, 3, -1] }; + for (a, f) in f.into_iter().enumerate() { + l.set_field(3 * k + a, f); + } + } + l +} + +/// The last sweep in which [`slower`]`(units)` moves a cell. +fn slower_moves_to(units: usize) -> u32 { + (11 * units as u32).saturating_sub(59) +} + +/// [`slower`]`(units)` beside two cells that alternate from the start (agreeing, +/// with fields 1 and −1, so that they go from 0 to `+−` and back): the whole comes +/// to a two-cycle two sweeps after the units' last move, at sweep `11u − 57`. +fn slower_then_a_cycle(units: usize) -> Landscape { + let mut l = slower_and(units, 2); + let n = 3 * units; + l.couple(n, n + 1, 1).set_field(n, 1).set_field(n + 1, -1); + l +} + +/// Uncoupled cells with these fields, at cost `cost`. +fn singles(fields: &[i64], cost: i64) -> Landscape { + let f: Vec<(usize, i64)> = fields.iter().copied().enumerate().collect(); + landscape(fields.len(), &[], &f, cost) +} + +/// Six cells in a ring, their fields seven apart from −18 to 17: thirty entries +/// of cut points a sweep, every field uncertain at temperature 20, and few +/// enough cells that a schedule of a million sweeps is quick to check: a +/// declared schedule length's edge. +fn long_ring() -> Landscape { + let mut l = Landscape::new(6); + for i in 0..6 { + l.couple(i, (i + 1) % 6, if i % 2 == 0 { 1 } else { -1 }); + l.set_field(i, 7 * i as i64 - 18); + } + l +} + +/// `n` cells, each with a field of 1 or 2 in size, of both signs, so that every +/// one of them moves from 0 in its first sweep; and `pairs` couplings, a cell to +/// the next, spread over the whole line where there are fewer than `n − 1`. +fn ladder(n: usize, pairs: usize) -> Landscape { + let mut l = Landscape::new(n); + let p = pairs.min(n.saturating_sub(1)); + for k in 0..p { + let a = if p == n - 1 { k } else { k * (n - 1) / p }; + l.couple(a, a + 1, if k % 2 == 0 { 1 } else { -1 }); + } + for i in 0..n { + l.set_field(i, [1, -2, 2, -1][i % 4]); + } + l +} + +/// A cell coupled to `d` others whose fields of 2 hold them at their sign, the +/// first `⌊d/2⌋` at +1 and the rest at −1, and a field of its own of 0 or −1: +/// its whole row gives it a field of −1, and any part of its row shorter than +/// the whole a field of 0 or more. A row as long as `d`. +fn row(d: usize) -> Landscape { + let mut l = Landscape::new(d + 1); + l.set_field(0, if d % 2 == 1 { 0 } else { -1 }); + for j in 1..=d { + l.couple(0, j, 1); + l.set_field(j, if j <= d / 2 { 2 } else { -2 }); + } + l +} + +/// A workload whose cells reach `t` fields, or as many under it as `cells` +/// cells, `pairs` couplings and fields within `field` and `bound` allow. `k` +/// hubs are coupled to the same `l` cells at field 0, which reach `−k..k`; the +/// hubs' own fields are spaced so that each reaches `2l + 1` fields no other +/// cell does; a last hub with fewer couplings, and a cell alone at a field of +/// its own, make up the count. +fn table_reaching(t: u64, cells: usize, pairs: usize, field: i64, bound: i64) -> Option { + let mut best: Option<(u64, usize, usize, usize, bool)> = None; + let mut l = 1usize; + while l + 2 <= cells { + // How many hubs fit by cells and couplings, and by the room for their fields. + let room = |k: usize| -> usize { + let first = k as i64 + 1 + l as i64; + let top = field.min(bound - l as i64); + if top < first { 0 } else { (2 * ((top - first) / (2 * l as i64 + 1) + 1)).min(1 << 24) as usize } + }; + let most = (cells - l - 1).min(pairs / l); + for full in (0..=most.min(t as usize)).rev().take(4096) { + if full > room(full + 1) { + continue; + } + let base = (2 * full as u64 + 1) + full as u64 * (2 * l as u64 + 1); + if base > t { + continue; + } + let rem = t - base; + // A last hub of `part` couplings adds 2 to the cells at field 0, and its own reach. + let (part, alone) = if rem >= 3 && full < most && pairs >= full * l + 1 && full < room(full + 1) { + let part = (((rem - 3) / 2) as usize).min(l).min(pairs - full * l); + (part, rem - 3 - 2 * part as u64 >= 1) + } else { + (0, rem >= 1) + }; + let alone = alone && full + usize::from(part > 0) + l + 1 <= cells; + let got = base + if part > 0 { 3 + 2 * part as u64 } else { 0 } + u64::from(alone); + if best.is_none_or(|b| got > b.0) { + best = Some((got, l, full, part, alone)); + } + break; + } + l *= 2; + } + let (_, l, full, part, alone) = best?; + let hubs = full + usize::from(part > 0); + let n = hubs + l + usize::from(alone); + let mut w = Landscape::new(n); + let kd = hubs as i64; + let mut far = 0i64; + for h in 0..hubs { + let at = kd + 1 + l as i64 + (h / 2) as i64 * (2 * l as i64 + 1); + let b = if h % 2 == 0 { at } else { -at }; + w.set_field(l + h, b); + far = far.max(at + l as i64); + let deg = if h == full { part } else { l }; + for j in 0..deg { + w.couple(l + h, j, 1); + } + } + if alone { + let b = if hubs == 0 { kd + 1 } else { -(far + 1) }; + if b.abs() > field { + return None; + } + w.set_field(n - 1, b); + } + Some(w) +} + +/// Two hubs at fields `−a` and `b`, coupled to `dA` and `dB` cells at field 0 +/// they share, so that the fields the cells can reach run from `−a − dA` to +/// `b + dB`: a span of `s`, or the widest under it that `cells`, `pairs`, +/// `field` and `bound` allow. +fn span_reaching(s: u64, cells: usize, pairs: usize, field: i64, bound: i64) -> Landscape { + let d = s.saturating_sub(1) as i64; + let a = field.min(d / 2); + let b = field.min(d - a); + let rem = (d - a - b).max(0); + let leaves = cells.saturating_sub(2) as i64; + let cap_a = leaves.min(bound - a).min(pairs as i64).max(0); + let mut da = (rem / 2 + rem % 2).min(cap_a); + let cap_b = leaves.min(bound - b).min(pairs as i64 - da).max(0); + let db = (rem - da).min(cap_b); + da = (rem - db).min(cap_a); + let n = 2 + da.max(db) as usize; + let mut w = Landscape::new(n); + w.set_field(0, -a); + w.set_field(1, b); + for j in 0..da.max(db) as usize { + if (j as i64) < da { + w.couple(0, 2 + j, 1); + } + if (j as i64) < db { + w.couple(1, 2 + j, 1); + } + } + w +} + +/// A seed for a generated vector, from its name. +fn seed_of(name: &str) -> u64 { + name.bytes().fold(0xCBF2_9CE4_8422_2325u64, |h, b| (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01B3)) +} + +/// What a capacity lets a workload reach on one side of temperature 0. +#[derive(Clone, Copy, Debug)] +struct Side { + field: i64, + bound: i64, + cost: i64, +} + +/// What a capacity lets a workload reach: each declared number, or what a job +/// can reach where none is declared. +#[derive(Clone, Copy, Debug)] +struct Within { + cells: usize, + pairs: usize, + sweeps: u32, + batch: usize, + zero: Side, + hot: Side, + both: Side, +} + +/// Where generated vectors go: each one the capacity admits, until the sink says stop. +struct Out<'s, 'c> { + cap: &'c Capacity, + want: Want, + sink: &'s mut dyn FnMut(Generated) -> bool, + open: bool, +} + +/// The schedules a workload is generated at: hot at a temperature, cold, and +/// both (hot, then cold, then a sweep at 0, as an annealing job ends). +const COLD: f64 = 0.015625; + +impl Out<'_, '_> { + fn give(&mut self, g: Generated) { + if self.open { + self.open = (self.sink)(g); + } + } + + /// Whether any vector of these kinds is still wanted: a rule builds nothing otherwise. + fn wants(&self, settle: bool, chains: bool) -> bool { + self.open && ((settle && self.want.settle) || (chains && self.want.chains)) + } + + fn settle(&mut self, name: String, l: &Landscape, w: &Workload, bound: u32) { + let bound = bound.max(1); + if self.open && self.want.settle && self.cap.admits_settle(w, bound).is_ok() { + self.give(Generated::Settle(conformance::SettleCase { name, start: vec![0; l.len()], landscape: l.clone(), bound })); + } + } + + fn chain(&mut self, name: String, l: &Landscape, w: &Workload, temperatures: conformance::Temperatures, restarts: u64) { + if self.open && self.want.chains && self.cap.admits(w, &temperatures.each()).is_ok() { + let seed = seed_of(&name); + self.give(Generated::Chains(conformance::ChainCase { name, landscape: l.clone(), temperatures, seed, restarts })); + } + } + + fn law(&mut self, case: conformance::BatteryCase) { + if self.open && self.want.battery && self.cap.admits(&case.landscape.lower(), &case.temperatures).is_ok() { + self.give(Generated::Battery(case)); + } + } + + /// A workload at temperature 0 (a settle of `bound` sweeps and a quench), + /// hot at `t`, cold, and both: whichever of them the capacity admits. + fn every_side(&mut self, name: &str, l: &Landscape, t: f64, bound: u32, restarts: u64) { + use conformance::Temperatures::{Cooling, Held}; + if !self.wants(true, true) { + return; + } + let w = l.lower(); + self.settle(name.to_string(), l, &w, bound); + self.chain(format!("{name}/quench"), l, &w, Cooling { t0: 0.0, sweeps: 3 }, 2); + self.chain(format!("{name}/hot"), l, &w, Held(vec![(t, 3)]), restarts); + self.chain(format!("{name}/cold"), l, &w, Held(vec![(COLD, 3)]), 2); + self.chain(format!("{name}/both"), l, &w, Held(vec![(t, 2), (COLD, 1), (0.0, 1)]), restarts); + } + + /// A workload on one side of 0: at 0 (settles of `bound` sweeps and of one, + /// and a quench), above 0 (hot at `t`, and cold), or both (hot, cold, then 0). + fn on_side(&mut self, side: &str, name: String, l: &Landscape, t: f64, bound: u32) { + use conformance::Temperatures::{Cooling, Held}; + let (settles, chains) = (side == "at-0", true); + if !self.wants(settles, chains) { + return; + } + let w = l.lower(); + match side { + "at-0" => { + self.settle(name.clone(), l, &w, bound); + self.settle(format!("{name}/one-sweep"), l, &w, 1); + self.chain(format!("{name}/quench"), l, &w, Cooling { t0: 0.0, sweeps: 3 }, 2); + } + "above-0" => { + self.chain(format!("{name}/hot"), l, &w, Held(vec![(t, 3)]), 8); + self.chain(format!("{name}/cold"), l, &w, Held(vec![(COLD, 3)]), 2); + } + _ => { + self.chain(format!("{name}/both"), l, &w, Held(vec![(t, 2), (COLD, 1), (0.0, 1)]), 8); + // Cold enough above 0 that a declared span takes it: the sweep at 0 after them all the same. + self.chain(format!("{name}/both-cold"), l, &w, Held(vec![(COLD, 3), (0.0, 1)]), 4); + } + } + } +} + +impl Capacity { + fn within(&self) -> Within { + let cells = self.cells.unwrap_or(reachable::CELLS).clamp(1, reachable::CELLS) as usize; + let pairs = (self.couplings.unwrap_or(reachable::COUPLINGS).min(reachable::COUPLINGS) / 2) as usize; + let sweeps = self.sweeps.unwrap_or(reachable::SWEEPS).clamp(1, reachable::SWEEPS) as u32; + let degree = (cells - 1).min(pairs) as i64; + let side = |l: &Limits| { + let bound = l.field_bound.unwrap_or(reachable::FIELD_BOUND).min(reachable::FIELD_BOUND); + let field = l.field.unwrap_or(reachable::FIELD).min(reachable::FIELD).min(bound); + // Couplings carry a cell's field past its own: a declared field + // alone leaves a field bound of the field and a cell's couplings. + Side { field, bound: bound.min(field + degree), cost: l.cost.unwrap_or(reachable::FIELD).min(reachable::FIELD) } + }; + let (zero, hot) = (side(&self.at_zero), side(&self.above_zero)); + let both = Side { field: zero.field.min(hot.field), bound: zero.bound.min(hot.bound), cost: zero.cost.min(hot.cost) }; + Within { cells, pairs, sweeps, batch: self.batch(super::CHUNK_CHAINS.min(super::CHUNK_CELLS / 2)), zero, hot, both } + } + + /// The vectors certification generates for this capacity, collected + /// ([`Capacity::generate`]). + pub fn edges(&self) -> Edges { + self.edges_of(Want::ALL) + } + + /// The generated vectors of the kinds wanted, collected. + pub fn edges_of(&self, want: Want) -> Edges { + let mut e = Edges::default(); + self.generate(want, &mut |g| { + match g { + Generated::Settle(c) => e.settle.push(c), + Generated::Chains(c) => e.chains.push(c), + Generated::Battery(c) => e.battery.push(c), + } + true + }); + e + } + + /// The vectors certification generates for this capacity, each handed to + /// `sink` as it is made, until `sink` says stop: by rule for every capacity, + /// the reference's none among them, so that a member is held across each + /// number up to what the capacity lets a workload reach (§7.1); and, for each + /// number it declares below what a job can reach ([`reachable`]), workloads + /// at exactly that number within every other it declares, with battery + /// workloads at the same numbers (§5). Only those the capacity admits are + /// given. Deterministic, the reference their oracle. Its cost is bounded + /// whatever the capacity (`§7.1`; `generated_vectors_cost_what_the_spec_says`). + pub fn generate(&self, want: Want, sink: &mut dyn FnMut(Generated) -> bool) { + use conformance::{BatteryCase, Law, Temperatures::Cooling, Temperatures::Held}; + let r = self.within(); + let mut o = Out { cap: self, want, sink, open: true }; + + // Cells: a ladder of every power of two, one less and one more, up to + // what a workload can hold, every cell with a field so that every one moves. + for n in ladder_sizes(r.cells) { + if !o.wants(true, true) { + break; + } + let l = ladder(n, r.pairs); + let w = l.lower(); + o.settle(format!("ladder/cells-{n}"), &l, &w, 4); + o.chain(format!("ladder/cells-{n}/hot"), &l, &w, Held(vec![(1.5, 3)]), 2); + // Where a last, partial warp or workgroup holds more than half of it, the + // chains kernel's sweeps at 0 too. + if n % 64 > 32 || n % 32 > 16 { + o.chain(format!("ladder/cells-{n}/quench"), &l, &w, Cooling { t0: 0.0, sweeps: 3 }, 2); + } + } + // The final sweep: a sweep at 0 straight after sweeps above 0, as an engine + // runs it at the end of a job, over a last, partial warp of 32 or workgroup + // of 64 holding more than half of it, at its edges, its middle and odd and + // even counts (and over the larger groups' two last counts and middle); + // after two hot sweeps, after three (the sweep at 0 at an odd index), and + // after a cold one, as an annealing job ends. Its cells sit at a field equal + // to the cost, a tie between two states above 0 that the sweep at 0 settles + // to 0 however cold the sweep before it, so that the sweep moves cells. + if o.wants(false, true) { + let schedules: [(&str, Vec<(f64, u32)>); 3] = [("hot-then-0", vec![(1.5, 2), (0.0, 1)]), ("three-hot-then-0", vec![(1.5, 3), (0.0, 1)]), ("cold-then-0", vec![(1.5, 2), (COLD, 1), (0.0, 1)])]; + for g in [32usize, 64, 128, 256, 512, 1024] { + let counts: Vec = if g <= 64 { vec![g / 2 + 1, g / 2 + 2, 3 * g / 4, g - 2, g - 1] } else { vec![3 * g / 4, g - 2, g - 1] }; + for n in counts.into_iter().filter(|&n| n <= r.cells) { + let fields: Vec = (0..n).map(|i| if i % 2 == 0 { 1 } else { -1 }).collect(); + let l = singles(&fields, 1); + let w = l.lower(); + for (name, t) in schedules.iter().take(if g <= 64 { 3 } else { 1 }) { + o.chain(format!("final/cells-{n}/{name}"), &l, &w, Held(t.clone()), 4); + } + } + } + } + // A wave settling down as many cells as a workload can hold, a cell a sweep: + // as long as a wave within the capacity can be, coming to rest + // within the sweeps a run may have (of fewer cells where those are fewer); + // and the same stopped at its bound one sweep before it would rest, its last + // sweep moving a cell. + if o.wants(true, false) { + let n = r.cells.min(r.pairs + 1).max(2); + let rest = n.min((r.sweeps as usize).saturating_sub(1)).max(2); + let l = banded(rest, rest - 1, 2); + o.settle(format!("wave/cells-{rest}"), &l, &l.lower(), (2 * rest as u32 + 2).min(r.sweeps)); + let l = banded(n, n - 1, 2); + let b = (n as u32).min(r.sweeps); + o.settle(format!("wave/cells-{n}/to-{b}"), &l, &l.lower(), b); + } + // A settle that keeps moving: units of three cells that move a cell in every + // sweep to sweep 11u − 59 from every cell at 0, nearly twice the wave's, or, + // where couplings are too few for those to move longer, units of two that move + // to sweep 4u − 10. Stopped at its bound while it still moves, just past 2⁹, + // 2¹⁰, 2¹¹, 2¹² and 2¹³ sweeps, where a narrow sweep counter wraps (each on as + // few units as still move there), and at its last moving sweep (or the most a + // run may have) on as many cells as the capacity takes: 14,956 sweeps on 4,095 + // cells with none. No longest settle within the rules is known; past the sweeps + // these reach, a narrow counter or a stale read at a bound is left to the audit (§7.3). + if o.wants(true, false) { + let (two, three) = ((r.cells / 2).min((r.pairs + 2) / 3), (r.cells / 3).min((r.pairs + 16) / 13)); + type Shape = (&'static str, usize, usize, fn(usize) -> u32, fn(usize) -> Landscape); + let (name, per, most, moves_to, build): Shape = if three >= 10 && slower_moves_to(three) >= slow_moves_to(two) { ("slower", 3, three, slower_moves_to, slower) } else { ("slow", 2, two, slow_moves_to, slow) }; + if most >= 10 { + let last = moves_to(most).min(r.sweeps); + for b in [513u32, 1025, 2049, 4097, 8193].into_iter().filter(|&b| b < last) { + let units = (10..=most).find(|&u| moves_to(u) >= b).unwrap_or(most); + let l = build(units); + o.settle(format!("{name}/cells-{}/to-{b}", per * units), &l, &l.lower(), b); + } + let l = build(most); + o.settle(format!("{name}/cells-{}/to-{last}", per * most), &l, &l.lower(), last); + } + } + // Settles that end in a two-cycle late: the same units beside two cells that + // alternate from the start, so that the whole comes to a two-cycle at sweep + // 11u − 57. With 46 units at sweep 449, the first sweep of a later batch of 64, + // where a host that runs a settle in batches must carry the state two sweeps back + // from the batch before; with 378 units at sweep 4,101, past 2¹², where a sweep + // counter of 9 to 12 bits read at the ending gives the other phase of the cycle + // (on as many units as the capacity takes, to 378). + if o.wants(true, false) { + let fit = (r.cells.saturating_sub(2) / 3).min(r.pairs.saturating_sub(1).saturating_add(16) / 13).min((r.sweeps as usize + 57) / 11); + let mut sizes = vec![46, fit.min(378)]; + sizes.retain(|&u| (10..=fit).contains(&u)); + sizes.dedup(); + for units in sizes { + let l = slower_then_a_cycle(units); + let at = slower_moves_to(units) + 2; + o.settle(format!("two-cycle/cells-{}/at-{at}", l.len()), &l, &l.lower(), (at + 2).min(r.sweeps)); + } + } + // Calls: every number of chains from 1 to 32 in one call, so that every + // remainder of a warp is run; past a warp, each band a wavefront of 64 can + // leave; and a whole batch and one less. + let two = landscape(2, &[(0, 1, -1)], &[(1, 1)], 0); + let two_w = two.lower(); + let mut calls: Vec = (1..=32).chain([33, 48, 63, 64, 65, 96, 127, 128]).filter(|&k| k <= r.batch).collect(); + calls.extend([r.batch.saturating_sub(1), r.batch]); + calls.retain(|&k| k >= 1); + calls.sort_unstable(); + calls.dedup(); + for k in calls { + o.chain(format!("calls/{k}"), &two, &two_w, Held(vec![(1.0, 2)]), k as u64); + } + // A row as long as a cell can have, every coupling of it needed: on both + // sides of 0, and at 0 alone where that allows a longer one. + let longest = |side: &Side| (side.bound - 1).clamp(1, (r.cells - 1).min(r.pairs).max(1) as i64) as usize; + let mut done = 0usize; + for (both, side) in [(true, &r.both), (false, &r.zero)] { + if !o.wants(true, both) { + continue; + } + let mut d = longest(side); + while d >= 1 && d != done { + let l = row(d); + let w = l.lower(); + let fits = if both { self.admits(&w, &[0.5, 0.0]).is_ok() } else { self.admits_settle(&w, 4).is_ok() }; + if fits { + if both { + o.chain(format!("row/degree-{d}/hot"), &l, &w, Held(vec![(0.5, 3)]), 8); + o.chain(format!("row/degree-{d}/both"), &l, &w, Held(vec![(0.5, 2), (COLD, 1), (0.0, 1)]), 8); + } + o.settle(format!("row/degree-{d}"), &l, &w, 4); + done = d; + break; + } + d -= (d / 16).max(1); + } + } + // A sum of couplings past what a narrow lane holds: a cell at field 0 whose + // row of cells held at +1 sums to 128, and to as many as a cell can have. + if o.wants(true, true) { + let most = (r.cells - 1).min(r.pairs).min(r.zero.bound.max(1) as usize); + let mut sums = vec![128, most]; + sums.retain(|&d| d >= 2 && d <= most); + sums.dedup(); + for d in sums { + let mut l = Landscape::new(d + 1); + for j in 1..=d { + l.couple(0, j, 1); + l.set_field(j, 2); + } + let w = l.lower(); + o.settle(format!("row/sum-{d}"), &l, &w, 4); + o.chain(format!("row/sum-{d}/hot"), &l, &w, Held(vec![(0.5, 3)]), 4); + o.chain(format!("row/sum-{d}/both"), &l, &w, Held(vec![(0.5, 2), (COLD, 1), (0.0, 1)]), 4); + } + } + // Lanes: on each side of 0, one workload of a cell at each wrap point a + // field can reach, of both signs, and a field reached exactly through two + // couplings at each one a field bound can; and the same beside the largest cost. + let mut wraps: Vec = LANES.iter().flat_map(|&lane| [(1i64 << (lane - 1)) - 1, 1 << (lane - 1), 3 << (lane - 2), 1 << lane]).collect(); + wraps.sort_unstable(); + wraps.dedup(); + for (side, s) in [("at-0", &r.zero), ("above-0", &r.hot), ("both", &r.both)] { + if !o.wants(side == "at-0", true) { + continue; + } + let c = s.cost.min(wraps[0] - 1); + for cost in if c > 0 { vec![0, c] } else { vec![0] } { + // The units: a cell at ±v; two cells whose fields reach ±v through two couplings each. + let mut units: Vec<(bool, i64)> = wraps.iter().copied().filter(|&v| v <= s.field).map(|v| (false, v)).collect(); + units.extend(wraps.iter().copied().filter(|&v| v <= s.bound && v - 2 <= s.field).map(|v| (true, v))); + if units.is_empty() { + continue; + } + // As many as fit a workload the capacity takes, a workload at a time. + let build = |us: &[(bool, i64)]| { + let edge = cost + 1 <= s.field; + let n: usize = us.iter().map(|&(coupled, _)| if coupled { 6 } else { 2 }).sum::() + if edge { 2 } else { 0 }; + let mut l = Landscape::new(n); + let mut at = 0; + for &(coupled, v) in us { + if coupled { + for sign in [1, -1] { + l.couple(at, at + 1, 1).couple(at, at + 2, 1); + l.set_field(at, sign * (v - 2)).set_field(at + 1, sign * 100).set_field(at + 2, sign * 100); + at += 3; + } + } else { + l.set_field(at, v).set_field(at + 1, -v); + at += 2; + } + } + // Each side of the threshold the cost sets. + if edge { + l.set_field(at, cost + 1).set_field(at + 1, -cost - 1); + } + l.set_cost(cost); + l + }; + let takes = |l: &Landscape| { + let w = l.lower(); + match side { + "at-0" => self.admits_settle(&w, 3).is_ok(), + "above-0" => self.admits(&w, &[COLD]).is_ok(), + _ => self.admits(&w, &[COLD, 0.0]).is_ok(), + } + }; + let (mut part, mut k) = (Vec::new(), 0usize); + for u in units.iter().copied().chain(std::iter::once((false, -1))) { + let last = u.1 < 0; + if !last { + part.push(u); + if takes(&build(&part)) { + continue; + } + part.pop(); + } + if !part.is_empty() { + o.on_side(side, format!("lane/{side}/cost-{cost}/{k}"), &build(&part), (1u64 << 15) as f64, 3); + k += 1; + } + part.clear(); + if !last && takes(&build(&[u])) { + part.push(u); + } + } + } + } + // A corner on each side of 0: every number at its most at once. The largest + // field of both signs beside the largest cost under it, so those cells move + // at 0, and every coupling the capacity allows among the other cells; and a + // cell whose field is reached exactly through as many couplings as a cell can + // have, the field bound at its most. + for (side, s) in [("at-0", &r.zero), ("above-0", &r.hot), ("both", &r.both)] { + let n = r.cells.min(4096); + if n < 4 || !o.wants(side == "at-0", true) { + continue; + } + let schedule: Vec = match side { + "at-0" => Vec::new(), + "above-0" => vec![COLD], + _ => vec![COLD, 0.0], + }; + let takes = |l: &Landscape| if schedule.is_empty() { self.admits_settle(&l.lower(), 3).is_ok() } else { self.admits(&l.lower(), &schedule).is_ok() }; + let f0 = s.field.min(s.bound); + let mut degree = (s.bound - 1).clamp(1, 1 << 20) as usize; + let mut cells = n; + let fields = loop { + let fill = banded(cells - 2, r.pairs, degree); + let mut l = Landscape::new(cells); + for i in 0..cells - 2 { + for j in (i + 1)..cells - 2 { + let c = fill.coupling(i, j); + if c != 0 { + l.couple(2 + i, 2 + j, c); + } + } + l.set_field(2 + i, if i % 2 == 0 { 1 } else { -1 }); + } + l.set_field(0, f0).set_field(1, -f0); + l.set_cost(s.cost.min(f0 - 1).max(0)); + if takes(&l) || (degree == 1 && cells <= 4) { + break takes(&l).then_some(l); + } + if degree > 1 { degree /= 2 } else { cells = (cells / 2).max(4) } + }; + if let Some(l) = fields { + o.on_side(side, format!("corner/{side}/fields"), &l, f0.max(1) as f64, 3); + } + let mut d = (r.cells - 1).min(r.pairs).min(s.bound.max(1) as usize); + let bound = loop { + let fh = s.field.min(s.bound - d as i64).max(0); + let p = 2.min(s.field).max(1); + let mut l = Landscape::new(d + 1); + l.set_field(0, fh); + for j in 1..=d { + l.couple(0, j, 1); + l.set_field(j, p); + } + if takes(&l) || d == 1 { + break takes(&l).then_some(l); + } + d -= (d / 8).max(1); + }; + if let Some(l) = bound { + o.on_side(side, format!("corner/{side}/bound"), &l, 1.0, d as u32 + 3); + } + } + // Draw indices: a run carried past each counter width that + // `sweeps · cells` can reach, its sweeps at 0 until the hot ones that cross it. + for bits in DRAW_INDICES { + if !o.wants(false, true) { + break; + } + // Sweep `k − 1` draws up to index `k·n`, at or past the width; sweep `k` past it. + let n = r.cells; + let k = (1u64 << bits).div_ceil(n as u64); + if k > u64::from(r.sweeps) { + continue; + } + // Hot to the end: a last sweep at 0 would put every uncoupled cell back at its sign. + let k = k as u32; + let hot = (r.sweeps - (k - 1)).min(2); + let fields: Vec = (0..n).map(|i| if i % 2 == 0 { 1 } else { -1 }).collect(); + let l = singles(&fields, 0); + o.chain(format!("draws/past-2^{bits}"), &l, &l.lower(), Held(vec![(0.0, k - 1), (1.5, hot)]), 1); + } + + // The edges of each declared number below what a job can reach. + let below_u = |d: Option, max: u64| d.filter(|&d| d < max); + let below_i = |d: Option, max: i64| d.filter(|&d| d < max); + let (fz, bz, fa, ba) = (r.zero.field, r.zero.bound, r.hot.field, r.hot.bound); + let degree = (bz.min(ba) - 3).clamp(1, r.cells.max(2) as i64 - 1) as usize; + if let Some(p) = below_u(self.couplings, reachable::COUPLINGS) { + let pairs = (p / 2) as usize; + if pairs > 0 && o.wants(true, true) { + let n = (pairs + 1).min(r.cells).max(2); + let l = banded(n, pairs, degree); + let w = l.lower(); + o.settle(format!("edge/couplings-{p}"), &l, &w, (2 * n as u32 + 2).min(r.sweeps)); + o.chain(format!("edge/couplings-{p}/hot"), &l, &w, Held(vec![(1.5, 3)]), 4); + o.chain(format!("edge/couplings-{p}/both"), &l, &w, Held(vec![(1.5, 2), (COLD, 1), (0.0, 1)]), 4); + } + } + // At temperature 0: the field bound through couplings and by a field alone, the field, the cost. + if let Some(b) = below_i(self.at_zero.field_bound, reachable::FIELD_BOUND) { + for s in [(b - 2).min(fz).max(0), -(b - 2).min(fz).max(0)] { + let l = star(s); + let w = l.lower(); + o.settle(format!("edge/field-bound-{b}-at-0/{s}"), &l, &w, 20); + o.chain(format!("edge/field-bound-{b}-at-0/{s}/quench"), &l, &w, Cooling { t0: 0.0, sweeps: 5 }, 4); + } + let f = b.min(fz); + let l = singles(&[f, -f, 1], 0); + let w = l.lower(); + o.settle(format!("edge/field-bound-{b}-at-0/alone"), &l, &w, 5); + o.settle(format!("edge/field-bound-{b}-at-0/alone/one-sweep"), &l, &w, 1); + } + if let Some(f) = below_i(self.at_zero.field, reachable::FIELD) { + let f = f.min(bz); + let l = singles(&[f, -f, 1], 0); + let w = l.lower(); + o.settle(format!("edge/field-{f}-at-0"), &l, &w, 5); + o.chain(format!("edge/field-{f}-at-0/quench"), &l, &w, Cooling { t0: 0.0, sweeps: 5 }, 4); + } + if let Some(c) = below_i(self.at_zero.cost, reachable::FIELD) { + let f = fz.min(bz); + let l = singles(&[c.min(f), (c + 1).min(f), -c.min(f), -(c + 1).min(f)], c); + let w = l.lower(); + o.settle(format!("edge/cost-{c}-at-0"), &l, &w, 5); + o.chain(format!("edge/cost-{c}-at-0/quench"), &l, &w, Cooling { t0: 0.0, sweeps: 5 }, 4); + } + // Above temperature 0: each field drawn at a temperature of its own size, + // cold, and hot then at 0. + if let Some(b) = below_i(self.above_zero.field_bound, reachable::FIELD_BOUND) { + let f = b.min(fa).max(0); + for s in [f, -f] { + o.every_side(&format!("edge/field-bound-{b}-above-0/{s}"), &singles(&[s], 0), f.max(1) as f64, 3, 8); + } + let f2 = (b - 2).min(fa).max(0); + o.every_side(&format!("edge/field-bound-{b}-above-0/star"), &star(f2), f2.max(1) as f64, 20, 32); + for sign in [1, -1] { + if let Some(l) = pinned(b, sign, fa, 0) { + o.every_side(&format!("edge/field-bound-{b}-above-0/pinned/{sign}"), &l, 1.0, 3, 4); + } + } + o.law(alike3("edge/field-bound-above-0", f, 0, f.max(1) as f64)); + o.law(BatteryCase::of("edge/field-bound-coupled-above-0", star(f2), vec![f2.max(1) as f64; 32])); + } + if let Some(f) = below_i(self.above_zero.field, reachable::FIELD) { + let f = f.min(ba); + for s in [f, -f] { + o.every_side(&format!("edge/field-{f}-above-0/{s}"), &singles(&[s], 0), f.max(1) as f64, 3, 8); + } + o.law(alike3("edge/field-above-0", f, 0, f.max(1) as f64)); + } + if let Some(c) = below_i(self.above_zero.cost, reachable::FIELD) { + let t = c.max(1) as f64; + let f = fa.min(ba).min(c); + o.every_side(&format!("edge/cost-{c}-above-0"), &singles(&[0], c), t, 3, 8); + o.every_side(&format!("edge/cost-{c}-above-0/at-the-field"), &singles(&[f], c), t, 3, 8); + let g = c.saturating_add(1).min(fa).min(ba); + o.every_side(&format!("edge/cost-{c}-above-0/each-side"), &singles(&[g, g - 1, -g, 1 - g], c), t, 3, 4); + o.law(alike3("edge/cost-above-0", 0, c, t)); + } + // A table of as many entries as it declares, and a span as wide, through couplings where need be. + // Hubs on shared cells, their fields spaced apart; or, where the fields are + // the narrower limit, two hubs whose reaches meet: whichever comes nearer. + if let Some(t) = below_u(self.table_entries, reachable::TABLE_ENTRIES) + && let Some(l) = [table_reaching(t, r.cells, r.pairs, fa.min(ba), ba), Some(span_reaching(t, r.cells, r.pairs, fa.min(ba), ba))] + .into_iter() + .flatten() + .map(|l| (reach::reach(&l.lower()).entries, l)) + .filter(|(e, _)| *e <= t) + .max_by_key(|(e, _)| *e) + .map(|(_, l)| l) + { + let w = l.lower(); + o.chain(format!("edge/table-entries-{t}"), &l, &w, Held(vec![(1.0, 3)]), 4); + o.chain(format!("edge/table-entries-{t}/both"), &l, &w, Held(vec![(1.0, 2), (COLD, 1), (0.0, 1)]), 4); + } + // The uncertain span: as wide as it declares or, where it declares none, as + // wide as two hubs on shared cells reach within the other numbers; and where + // the other declared numbers (a table, the couplings) do not take that, the + // widest under it they do. + if o.open { + let span_of = |s: u64| span_reaching(s, r.cells, r.pairs, fa.min(ba), ba); + let declared = below_u(self.uncertain_span, reachable::UNCERTAIN_SPAN); + let s0 = declared.unwrap_or_else(|| uncertain_span(&span_of(reachable::UNCERTAIN_SPAN).lower(), reachable::UNCERTAIN_SPAN as f64).max(1)); + let admitted = |s: u64| self.admits(&span_of(s).lower(), &[s.max(1) as f64]).is_ok(); + let s = if admitted(s0) { + Some(s0) + } else if admitted(1) { + let (mut lo, mut hi) = (1u64, s0); + while lo + 1 < hi { + let mid = lo + (hi - lo) / 2; + if admitted(mid) { lo = mid } else { hi = mid } + } + Some(lo) + } else { + None + }; + if let Some(s) = s { + let l = span_of(s); + let w = l.lower(); + let t = s.max(1) as f64; + let name = if declared.is_some() { format!("edge/uncertain-span-{s}") } else { format!("span/widest-{s}") }; + o.chain(name.clone(), &l, &w, Held(vec![(t, 3)]), 8); + o.chain(format!("{name}/both"), &l, &w, Held(vec![(t, 2), (COLD, 1), (0.0, 1)]), 8); + if declared.is_some() && l.len() == 2 { + o.law(BatteryCase::of("edge/uncertain-span", l, vec![t; 32])); + } + } + } + // A schedule as long as it declares, across every window a device might cut it into. + if let Some(s) = below_u(self.sweeps, reachable::SWEEPS) { + let l = long_ring(); + let w = l.lower(); + o.chain(format!("edge/sweeps-{s}"), &l, &w, Held(vec![(20.0, s as u32)]), 2); + o.chain(format!("edge/sweeps-{s}/both"), &l, &w, Held(vec![(20.0, s as u32 - 1), (0.0, 1)]), 2); + } + // The battery across the cells: as many alike cells as a workload can hold, + // every one of them read, in blocks of three; and, where that count is not + // one less than a power of two, as many as the largest such count under it to + // 1,023, so that a last, partial warp or workgroup of up to 1,024 holds as + // many cells as it can. At declared couplings and sweeps, alike triangles. + // Every block of each is read (§7.2 holds the false failures of so many). + let every = |c: usize| -> Vec { + let mut out: Vec = (0..c / 3).map(|b| 3 * b).collect(); + if !c.is_multiple_of(3) { + out.push(c - 3); + } + out + }; + if r.cells > 24 { + let c = r.cells; + o.law(BatteryCase { name: "cells", landscape: singles(&vec![1; c], 0), temperatures: vec![1.0], law: Law::Settled, block: 3, read: every(c) }); + let m = (1usize << (usize::BITS - 1 - (c + 1).leading_zeros()).min(10)) - 1; + if !(c + 1).is_power_of_two() && m >= 31 { + o.law(BatteryCase { name: "cells/remainders", landscape: singles(&vec![1; m], 0), temperatures: vec![1.0], law: Law::Settled, block: 3, read: every(m) }); + } + } + if let Some(p) = below_u(self.couplings, reachable::COUPLINGS) { + let blocks = ((p / 6) as usize).min(r.cells / 3).min(341); + if blocks >= 1 { + let mut l = Landscape::new(3 * blocks); + for b in 0..blocks { + let at = 3 * b; + l.couple(at, at + 1, 1).couple(at + 1, at + 2, 1).couple(at, at + 2, 1); + for i in 0..3 { + l.set_field(at + i, 1); + } + } + o.law(BatteryCase { name: "edge/couplings", landscape: l, temperatures: vec![1.0, 1.0], law: Law::Walked, block: 3, read: Vec::new() }); + } + } + if let Some(s) = below_u(self.sweeps, reachable::SWEEPS) { + let s = (s as usize).min(1024); + let mut temps = vec![1.2; s.saturating_sub(6)]; + temps.extend(vec![0.2; s.min(6)]); + let l = landscape(3, &[(0, 1, 1), (1, 2, 1), (0, 2, 1)], &[(0, 1), (1, 1), (2, 1)], 0); + o.law(BatteryCase { name: "edge/sweeps", landscape: l, temperatures: temps, law: Law::Walked, block: 3, read: Vec::new() }); + } + } +} + +#[cfg(test)] +mod tests { + use super::super::energy::Landscape; + use super::*; + + fn ring(n: usize, field: i64, cost: i64) -> Workload { + let mut l = Landscape::new(n); + for i in 0..n { + l.couple(i, (i + 1) % n, 1); + } + l.set_field(0, field).set_cost(cost); + l.lower() + } + + /// The floor is what the core set needs, and no more: each of its numbers + /// is reached by some core case. + #[test] + fn the_floor_is_the_core_set_s_own_reach() { + let f = floor(); + eprintln!("the floor: {}", f.json()); + assert_eq!(f.cells, Some(32), "the recall vector of 32 cells"); + assert_eq!(f.at_zero.field_bound, Some(41), "the wide fields quenched at 0"); + assert_eq!(f.above_zero.field, Some(40)); + assert_eq!(f.above_zero.cost, Some(8)); + assert_eq!(f.chains_per_call, None, "a batch is certified by splitting, so the floor holds none"); + assert_eq!((f.couplings, f.table_entries, f.uncertain_span), (Some(992), Some(31), Some(61)), "the numbers §5 states"); + assert_eq!(f.sweeps, Some(5000), "the core set's long schedule"); + for c in conformance::core_settle_cases() { + assert_eq!(f.admits(&c.landscape.lower(), &[]), Ok(()), "{}", c.name); + } + for c in conformance::core_chain_cases() { + assert_eq!(f.admits(&c.landscape.lower(), &c.temperatures.each()), Ok(()), "{}", c.name); + } + for c in conformance::core_battery() { + assert_eq!(f.admits(&c.landscape.lower(), &c.temperatures), Ok(()), "{}", c.name); + } + assert!(f.under(&f).is_empty()); + } + + /// Each declared number declines exactly what is past it, saying which. + #[test] + fn a_capacity_declines_exactly_what_is_past_it() { + let cap = Capacity { cells: Some(64), above_zero: Limits { field: Some(524_287), field_bound: None, cost: Some(1_048_575) }, uncertain_span: Some(8192), ..Capacity::NONE }; + assert_eq!(cap.admits(&ring(64, 3, 1), &[1.0, 0.0]), Ok(())); + assert!(cap.admits(&ring(65, 3, 1), &[]).unwrap_err().contains("65 cells, and it holds 64")); + // At temperature 0 it takes any field; above it, its registers hold ±524,287. + assert_eq!(cap.admits(&ring(8, 1 << 40, 1), &[0.0; 3]), Ok(())); + assert_eq!(cap.admits(&ring(8, 524_287, 1), &[1.0]), Ok(())); + assert!(cap.admits(&ring(8, 524_288, 1), &[1.0]).unwrap_err().contains("a field of 524288 above temperature 0")); + assert!(cap.admits(&ring(8, 3, 1_048_576), &[1.0]).unwrap_err().contains("a cost of 1048576")); + // A temperature hot enough to leave every reachable field uncertain across a span past the window. + assert!(cap.admits(&ring(8, 20_000, 0), &[1.0e9, 1.0]).unwrap_err().contains("span")); + assert_eq!(cap.admits(&ring(8, 20_000, 0), &[1.0]), Ok(()), "cold, the far field's draw is certain"); + assert_eq!(Capacity::NONE.admits(&ring(4096, 1 << 40, 1 << 40), &[5.0]), Ok(()), "the reference declares none"); + } + + /// A field's draw that is uncertain at a temperature is uncertain at every + /// hotter one, so the hottest sweep's span is the widest. + #[test] + fn the_hottest_sweep_has_the_widest_uncertain_span() { + for (field, cost) in [(0, 0), (40, 8), (20_000, 0), (300, 150), (-7_000, 3_000)] { + let w = ring(12, field, cost); + let mut last = 0; + for t in [0.05, 0.3, 1.0, 4.0, 30.0, 500.0, 1.0e5] { + let s = uncertain_span(&w, t); + assert!(s >= last, "field {field} cost {cost}: {s} at {t} after {last}"); + last = s; + } + } + } + + /// The answer's statement of a capacity, and none for the reference. + #[test] + fn a_capacity_is_stated_in_numbers_and_in_words() { + let cap = Capacity { cells: Some(64), above_zero: Limits { field: Some(524_287), field_bound: None, cost: Some(1_048_575) }, uncertain_span: Some(8192), ..Capacity::NONE }; + assert_eq!(cap.json(), json!({ "cells": 64, "above_zero": { "field": 524_287, "cost": 1_048_575 }, "uncertain_span": 8192 })); + let words = cap.words(true).unwrap(); + assert!(words.contains("at most 64 cells") && words.contains("fields to ±524,287") && words.contains("span of 8,192"), "{words}"); + assert!(words.starts_with("Its parity holds within the capacity it declares, and the host runs it on no job past it"), "{words}"); + assert_eq!(Capacity::NONE.json(), Value::Null); + assert!(Capacity::NONE.words(true).is_none()); + let tiny = Capacity { cells: Some(4), ..Capacity::NONE }; + assert_eq!(tiny.under(&floor()), vec!["cells 4, and the floor is 32".to_string()]); + // A batch is not a limit: no job is declined for it, and the words say so. + let batch = Capacity { chains_per_call: Some(100), ..Capacity::NONE }; + let words = batch.words(true).unwrap(); + assert!(!words.contains("past it") && words.contains("splits a job, as certification split every vector"), "{words}"); + let both = Capacity { cells: Some(64), chains_per_call: Some(100), ..Capacity::NONE }; + let words = both.words(true).unwrap(); + assert!(words.contains("no job past it: at most 64 cells.") && words.contains("at most 100 chains a call"), "{words}"); + // Run by name, with no certification: no grade claimed, and no certification named. + let words = both.words(false).unwrap(); + assert!(words.starts_with("It declares a capacity, and the host runs it on no job past it") && !words.contains("certification") && !words.contains("parity"), "{words}"); + } + + fn names(e: &Edges) -> Vec { + e.settle.iter().map(|c| c.name.clone()).chain(e.chains.iter().map(|c| c.name.clone())).collect() + } + + /// Every number declared below what a job can reach has vectors at it, + /// within every other number declared. + #[test] + fn every_declared_number_has_vectors_at_its_edge() { + let edge = |e: &Edges| -> Vec { names(e).into_iter().filter(|n| n.starts_with("edge/")).collect() }; + assert!(edge(&Capacity::NONE.edges()).is_empty(), "the reference declares none"); + // Cells: a wave down all 299 of them. + let e = Capacity { cells: Some(299), ..Capacity::NONE }.edges(); + let wave = e.settle.iter().find(|c| c.name == "wave/cells-299").expect("a settle at 299 cells"); + assert_eq!(wave.landscape.lower().settle(&wave.start, wave.bound).sweeps, 300, "the wave reaches the last cell"); + // Couplings alone: a path of 497 cells, its 992 couplings. + let e = Capacity { couplings: Some(992), ..Capacity::NONE }.edges(); + let path = e.settle.iter().find(|c| c.name == "edge/couplings-992").expect("a settle at 992 couplings"); + assert_eq!((path.landscape.len(), path.landscape.lower().nonzeros()), (497, 992)); + // A field bound between the vectors' points: at it, on both sides of temperature 0, by a field alone too. + let lanes = Limits { field_bound: Some(39_999), ..Limits::NONE }; + let cap = Capacity { at_zero: lanes, above_zero: lanes, ..Capacity::NONE }; + let e = cap.edges(); + assert!(e.settle.iter().any(|c| c.landscape.lower().field_bound() == 39_999 && c.landscape.lower().nonzeros() > 0)); + assert!(e.settle.iter().any(|c| c.landscape.lower().field_bound() == 39_999 && c.landscape.lower().nonzeros() == 0)); + assert!(e.chains.iter().any(|c| c.landscape.lower().field_bound() == 39_999 && c.temperatures.each().iter().all(|&t| t > 0.0))); + // A schedule length: a schedule that long, hot to the end and ending at 0. + let e = Capacity { sweeps: Some(7000), ..Capacity::NONE }.edges(); + assert_eq!(e.chains.iter().filter(|c| c.temperatures.each().len() == 7000).count(), 2); + // The FPGA reference design's capacity: each of its numbers, and every vector within all of them. + let board = Capacity { cells: Some(64), above_zero: Limits { field: Some(524_287), field_bound: None, cost: Some(1_048_575) }, uncertain_span: Some(8192), ..Capacity::NONE }; + let e = board.edges(); + let n = names(&e); + for want in ["wave/cells-64", "edge/field-524287-above-0/524287", "edge/cost-1048575-above-0/hot", "edge/uncertain-span-8192", "corner/above-0/bound/cold", "corner/at-0/fields"] { + assert!(n.iter().any(|x| x.starts_with(want)), "{want} in {n:?}"); + } + for c in &e.chains { + assert_eq!(board.admits(&c.landscape.lower(), &c.temperatures.each()), Ok(()), "{}", c.name); + } + // A GPU's: only the field bound removes jobs, so it alone has edges. + let gpu_lanes = Limits { field_bound: Some(i64::from(i32::MAX)), ..Limits::NONE }; + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: gpu_lanes, above_zero: gpu_lanes, table_entries: Some(1 << 22), ..Capacity::NONE }; + assert!(edge(&gpu.edges()).iter().all(|x| x.starts_with("edge/field-bound-2147483647")), "{:?}", edge(&gpu.edges())); + } + + /// A table and a span are reached at exactly the declared number, through + /// couplings, where the other numbers leave room for it. + #[test] + fn a_table_and_a_span_are_reached_exactly() { + let hottest = |c: &conformance::ChainCase| c.temperatures.each().iter().copied().fold(0.0, f64::max); + for (cap, entries) in [ + (Capacity { cells: Some(32), table_entries: Some(200), ..Capacity::NONE }, 200), + (Capacity { above_zero: Limits { field: Some(40), ..Limits::NONE }, table_entries: Some(200), ..Capacity::NONE }, 200), + (Capacity { cells: Some(64), table_entries: Some(1000), ..Capacity::NONE }, 1000), + (Capacity { table_entries: Some(77), ..Capacity::NONE }, 77), + ] { + let e = cap.edges(); + let t = e.chains.iter().find(|c| c.name.starts_with("edge/table-entries-") && !c.name.ends_with("/both")).unwrap_or_else(|| panic!("a table edge within {}", cap.json())); + assert_eq!(reach::reach(&t.landscape.lower()).entries, entries, "{}", cap.json()); + } + for (cap, span) in [ + (Capacity { above_zero: Limits { field: Some(40), ..Limits::NONE }, uncertain_span: Some(1000), ..Capacity::NONE }, 1000), + (Capacity { uncertain_span: Some(8192), cells: Some(64), ..Capacity::NONE }, 8192), + (Capacity { above_zero: Limits { field_bound: Some(200), ..Limits::NONE }, uncertain_span: Some(1000), ..Capacity::NONE }, 401), + ] { + let e = cap.edges(); + let c = e.chains.iter().find(|c| c.name.starts_with("edge/uncertain-span-") && !c.name.ends_with("/both")).unwrap_or_else(|| panic!("a span edge within {}", cap.json())); + assert_eq!(uncertain_span(&c.landscape.lower(), hottest(c)), span, "{}", cap.json()); + } + } + + /// The rules a fourth review asked for: a sweep at 0 after hot ones at the + /// remainder bands; a wave that rests within a declared sweep limit, and the + /// widest wave stopped at its bound; the battery's alike cells at a declared + /// count to 4,096; and a span reached past a table limit and with none declared. + #[test] + fn long_settles_bands_cells_and_spans_are_generated() { + let n = names(&Capacity::NONE.edges()); + for size in [17, 18, 24, 30, 31, 33, 34, 48, 62, 63] { + for schedule in ["hot-then-0", "three-hot-then-0", "cold-then-0"] { + assert!(n.contains(&format!("final/cells-{size}/{schedule}")), "{size} {schedule}"); + } + } + for size in [96, 126, 127, 192, 254, 255, 768, 1022, 1023] { + assert!(n.contains(&format!("final/cells-{size}/hot-then-0")), "{size}"); + } + let settles = |cap: Capacity| cap.edges_of(Want::SETTLE).settle.into_iter().map(|c| (c.name, c.bound)).collect::>(); + let none = settles(Capacity::NONE); + for (name, bound) in [ + ("wave/cells-4096", 8194), + ("wave/cells-4096/to-4096", 4096), + ("slower/cells-156/to-513", 513), + ("slower/cells-297/to-1025", 1025), + ("slower/cells-576/to-2049", 2049), + ("slower/cells-1134/to-4097", 4097), + ("slower/cells-2253/to-8193", 8193), + ("slower/cells-4095/to-14956", 14956), + ("two-cycle/cells-140/at-449", 451), + ("two-cycle/cells-1136/at-4101", 4103), + ] { + assert!(none.contains(&(name.to_string(), bound)), "{name} at {bound} in {none:?}"); + } + let s5000 = settles(Capacity { sweeps: Some(5000), ..Capacity::NONE }); + assert!(s5000.contains(&("wave/cells-4096".to_string(), 5000)) && s5000.contains(&("slower/cells-4095/to-5000".to_string(), 5000)), "{s5000:?}"); + let s1000 = settles(Capacity { sweeps: Some(1000), ..Capacity::NONE }); + assert!(s1000.contains(&("wave/cells-999".to_string(), 1000)) && s1000.contains(&("slower/cells-156/to-513".to_string(), 513)) && s1000.contains(&("slower/cells-4095/to-1000".to_string(), 1000)), "{s1000:?}"); + // Where couplings are scarce, units of two move the longer, and are the ones stopped. + let thin = settles(Capacity { couplings: Some(3000), ..Capacity::NONE }); + assert!(thin.contains(&("slow/cells-1000/to-1990".to_string(), 1990)), "{thin:?}"); + // The wave rests where its bound lets it, and stopped one sweep short it still moves. + let l = banded(999, 998, 2); + let o = l.lower().settle(&vec![0; 999], 1000); + assert_eq!((o.sweeps, o.term), (1000, super::super::settle::Term::FixedPoint)); + let l = banded(4096, 4095, 2); + let (w, start) = (l.lower(), vec![0i8; 4096]); + assert_ne!(w.settle(&start, 4095).state, w.settle(&start, 4096).state, "the last sweep of a wave stopped one before its rest moves a cell"); + assert_eq!(w.settle(&start, 4097).term, super::super::settle::Term::FixedPoint); + // Units of two cells move for 2n − 10 sweeps and rest at 2n − 9. + let sizes: &[usize] = if cfg!(target_arch = "wasm32") { &[8, 32, 300] } else { &[8, 32, 300, 2048] }; + for &units in sizes { + let l = slow(units); + let (w, start, n) = (l.lower(), vec![0i8; 2 * units], 2 * units as u32); + let rest = w.settle(&start, 10 * n); + assert_eq!((rest.sweeps, rest.term), (2 * n - 9, super::super::settle::Term::FixedPoint), "{units} units"); + assert_eq!(slow_moves_to(units), 2 * n - 10); + assert_ne!(w.settle(&start, 2 * n - 11).state, w.settle(&start, 2 * n - 10).state, "{units} units: its last moving sweep"); + } + // Units of three move a cell in every sweep to 11u − 59 and rest at 11u − 58, at every + // size generation uses; beside an alternating pair they end in a two-cycle at 11u − 57. + use super::super::settle::Term; + let sizes: Vec = if cfg!(target_arch = "wasm32") { (10..=64).chain([99]).collect() } else { (10..=64).chain([99, 192, 378, 751, 1365]).collect() }; + for units in sizes { + let l = slower(units); + let (w, start) = (l.lower(), vec![0i8; 3 * units]); + let to = slower_moves_to(units); + let rest = w.settle(&start, to + 10); + assert_eq!((rest.sweeps, rest.term), (to + 1, Term::FixedPoint), "{units} units"); + assert_eq!(w.settle(&start, to).term, Term::Bound, "{units} units: still moving at {to}"); + assert_ne!(w.settle(&start, to - 1).state, w.settle(&start, to).state, "{units} units: its last moving sweep"); + } + let cycles: &[usize] = if cfg!(target_arch = "wasm32") { &[10, 20, 46, 110] } else { &[10, 20, 46, 110, 378] }; + for &units in cycles { + let l = slower_then_a_cycle(units); + let at = slower_moves_to(units) + 2; + let o = l.lower().settle(&vec![0i8; l.len()], at + 2); + assert_eq!((o.sweeps, o.term), (at, Term::TwoCycle), "{units} units beside a pair"); + if units == 46 { + assert_eq!(at % 64, 1, "{units} units: the two-cycle at the first sweep of a batch"); + } + if units == 378 { + assert!((4096..4608).contains(&at), "{units} units: past 2¹² and where 9 to 12 bits all read the other phase"); + } + } + // The battery's alike cells: every one read, and a count with a partial group's most where the count has not. + let battery = |cap: Capacity| cap.edges_of(Want::BATTERY).battery; + let wide = Limits { field_bound: Some(i64::from(i32::MAX)), ..Limits::NONE }; + for (cap, want, remainders) in [(Capacity::NONE, 4096, Some(1023)), (Capacity { cells: Some(4096), ..Capacity::NONE }, 4096, Some(1023)), (Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: wide, above_zero: wide, table_entries: Some(1 << 22), ..Capacity::NONE }, 4096, Some(1023)), (Capacity { cells: Some(2000), ..Capacity::NONE }, 2000, Some(1023)), (Capacity { cells: Some(4095), ..Capacity::NONE }, 4095, None), (Capacity { cells: Some(64), ..Capacity::NONE }, 64, Some(63))] { + let b = battery(cap); + let read_all = |c: &conformance::BatteryCase| { + let n = c.landscape.len(); + (0..n).all(|i| c.read.iter().any(|&s| (s..s + 3).contains(&i))) + }; + let cells = b.iter().find(|c| c.name == "cells").unwrap(); + assert_eq!(cells.landscape.len(), want, "{}", cap.json()); + assert!(read_all(cells), "{}", cap.json()); + let rem = b.iter().find(|c| c.name == "cells/remainders"); + assert_eq!(rem.map(|c| c.landscape.len()), remainders, "{}", cap.json()); + assert!(rem.is_none_or(read_all)); + } + // A span past a table limit, and with none declared. + let hottest = |c: &conformance::ChainCase| c.temperatures.each().iter().copied().fold(0.0, f64::max); + let widest = |cap: Capacity, prefix: &str| { + cap.edges().chains.into_iter().filter(|c| c.name.starts_with(prefix) && !c.name.ends_with("/both")).map(|c| uncertain_span(&c.landscape.lower(), hottest(&c))).max() + }; + let lim = |field: Option, bound: Option, cost: Option| Limits { field, field_bound: bound, cost }; + let c1 = Capacity { couplings: Some(3058), at_zero: lim(Some(46), None, Some(8)), above_zero: lim(Some(40), None, Some(996)), table_entries: Some(401), uncertain_span: Some(412), chains_per_call: Some(3788), ..Capacity::NONE }; + let c2 = Capacity { cells: Some(170), couplings: Some(2032), above_zero: lim(Some(40), Some(4192), Some(8)), ..Capacity::NONE }; + let c3 = Capacity { above_zero: lim(Some(40), None, None), table_entries: Some(401), ..Capacity::NONE }; + let s1 = widest(c1, "edge/uncertain-span-").unwrap(); + let s2 = widest(c2, "span/widest-").unwrap(); + let s3 = widest(c3, "span/widest-").unwrap(); + eprintln!("spans reached: table 401 under span 412: {s1}; thin couplings: {s2}; table 401 alone: {s3}"); + assert!(s1 >= 390 && s1 <= 412, "{s1}"); + assert_eq!(s2, 417); + assert!(s3 >= 390, "{s3}"); + } + + /// With no capacity, the generated set reaches what a job can: the most + /// cells, a row of every one of them, every lane's wrap points on each side + /// of 0 (alone, through couplings and beside a cost), every call size to + /// 128 and a whole batch, and draw indices past 2²⁴ and 2³¹. + #[test] + fn with_no_capacity_the_generated_set_reaches_what_a_job_can() { + let e = Capacity::NONE.edges(); + let n = names(&e); + let has = |p: &str| n.iter().any(|x| x.starts_with(p)); + for size in [1, 2, 3, 31, 33, 63, 65, 1023, 1025, 4095, 4096] { + assert!(has(&format!("ladder/cells-{size}/hot")) && has(&format!("ladder/cells-{size}")), "{size}"); + } + assert!(has("row/degree-4095"), "{:?}", n.iter().filter(|x| x.starts_with("row/")).collect::>()); + // Every lane's wrap points, on each side of 0, alone and reached exactly through couplings, beside a cost and not. + for side in ["at-0", "above-0", "both"] { + for cost in [0, 32_766] { + let name = format!("lane/{side}/cost-{cost}/"); + let ws: Vec = e.chains.iter().filter(|c| c.name.starts_with(&name)).map(|c| c.landscape.lower()).collect(); + assert!(!ws.is_empty(), "{name}"); + for v in [32_767i64, 32_768, 49_152, 65_536, 8_388_608, 2_147_483_648, 4_294_967_296] { + let has = |f: &dyn Fn(&Workload, usize) -> bool| ws.iter().any(|w| (0..w.n()).any(|i| f(w, i))); + let zero = |w: &Workload| vec![0i8; w.n()]; + assert!(has(&|w, i| w.field(&zero(w), i) == -v), "{name}: a cell at -{v}"); + assert!(has(&|w, i| w.field(&zero(w), i) == v - 2 && w.degree(i) == 2), "{name}: {v} through couplings"); + } + } + } + let calls: Vec = e.chains.iter().filter(|c| c.name.starts_with("calls/")).map(|c| c.restarts).collect(); + assert!((1..=32).chain([33, 48, 63, 64, 65, 96, 127, 128, 4095, 4096]).all(|k| calls.contains(&k)), "{calls:?}"); + let c = e.chains.iter().find(|c| c.name == "draws/past-2^24").expect("a run past 2^24"); + let t = c.temperatures.each(); + let hot: Vec = (0..t.len()).filter(|&k| t[k] > 0.0).collect(); + let (first, last) = (hot[0] as u64 * c.landscape.len() as u64 + 1, (hot[hot.len() - 1] as u64 + 1) * c.landscape.len() as u64); + assert!(first < 1 << 24 && last > 1 << 24, "draws {first}..={last}"); + assert!(*t.last().unwrap() > 0.0, "hot to the end, so the draws are in the state"); + // Every annealing shape: hot, then cold, then a sweep at 0, at a lane's width. + assert!(e.chains.iter().any(|c| c.name.ends_with("/both") && c.landscape.lower().field_bound() >= 1 << 31)); + } + + /// What certification generates costs a bounded amount, whatever the + /// capacity: the bounds §7.1 states, held over the named capacities and over + /// random ones, each number declared or not. + #[test] + fn generated_vectors_cost_what_the_spec_says() { + let lanes = |b: i64, c: i64| Limits { field: Some(b), field_bound: Some(b), cost: Some(c) }; + let fpga = Capacity { cells: Some(64), above_zero: Limits { field: Some(524_287), field_bound: None, cost: Some(1_048_575) }, uncertain_span: Some(8192), ..Capacity::NONE }; + let biggest = Capacity { cells: Some(4095), couplings: Some(838_858), table_entries: Some(1_681_815), uncertain_span: Some(2_199_023_263_741), sweeps: Some(999_999), chains_per_call: Some(4096), ..Capacity::NONE }; + let below = reachable::FIELD - 1; + let mut caps = vec![ + ("none", Capacity::NONE), + ("floor", floor()), + ("fpga", fpga), + ("biggest", biggest), + ("every number below what a job can reach", Capacity { at_zero: lanes(below, below), above_zero: lanes(below, below), ..biggest }), + ("the same with 4,096 cells", Capacity { cells: Some(4096), at_zero: lanes(below, below), above_zero: lanes(below, below), ..biggest }), + ("the same with 4,096 cells and a schedule of 5,000", Capacity { cells: Some(4096), sweeps: Some(5000), at_zero: lanes(below, below), above_zero: lanes(below, below), ..biggest }), + ("32-bit lanes and a batch of 1,000", Capacity { at_zero: lanes(i64::from(i32::MAX), 1000), above_zero: lanes(i64::from(i32::MAX), 1000), chains_per_call: Some(1000), ..Capacity::NONE }), + ]; + let mut x: u64 = 0x5EED_C057; + let mut next = move |m: u64| { + x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + (x >> 17) % m + }; + // Fewer under wasm, where the module's tests run in a debug build under a runtime. + for _ in 0..if cfg!(target_arch = "wasm32") { 6 } else { 24 } { + let mut pick = |most: u64| if next(3) == 0 { None } else { Some(1 + next(most)) }; + let mut side = || Limits { field: pick(1 << 40).map(|v| v as i64), field_bound: pick((1 << 40) + 4095).map(|v| v as i64), cost: pick(1 << 40).map(|v| v as i64) }; + let (z, a) = (side(), side()); + let c = Capacity { cells: pick(4096), couplings: pick(838_860), at_zero: z, above_zero: a, table_entries: pick(1_681_816), uncertain_span: pick(1 << 41), sweeps: pick(1_000_000), chains_per_call: pick(5000) }; + caps.push(("random", c.join(&floor()).join(&c))); + } + let (mut most_v, mut most_w, mut most_c) = (0u64, 0u64, 0u64); + for (name, cap) in caps { + let (mut vectors, mut cold, mut work, mut battery) = (0u64, 0u64, 0u64, 0u64); + cap.generate(Want::ALL, &mut |g| { + vectors += 1; + match g { + Generated::Settle(c) => work += u64::from(c.bound) * c.landscape.len() as u64, + Generated::Chains(c) => { + let t = c.temperatures.each(); + let zeros = t.iter().filter(|&&t| t <= 0.0).count() as u64; + cold += zeros * c.landscape.len() as u64 * c.restarts; + work += (t.len() as u64 - zeros) * c.landscape.len() as u64 * c.restarts; + } + Generated::Battery(c) => battery += u64::from(conformance::BATTERY_CHAINS) * c.landscape.len() as u64 * c.temperatures.len() as u64, + } + true + }); + let share = battery as f64 / (work + battery).max(1) as f64; + work += battery; + eprintln!("generated for {name}: {vectors} vectors, {work} cell-sweeps hot or settled ({battery} of them battery, {:.0}%), {cold} at temperature 0", 100.0 * share); + (most_v, most_w, most_c) = (most_v.max(vectors), most_w.max(work), most_c.max(cold)); + assert!(vectors <= MOST_GENERATED, "{name} {}: {vectors}", cap.json()); + assert!(work <= MOST_GENERATED_WORK, "{name} {}: {work}", cap.json()); + assert!(cold <= MOST_GENERATED_COLD, "{name} {}: {cold}", cap.json()); + } + eprintln!("most: {most_v} vectors, {most_w} cell-sweeps, {most_c} at 0"); + } +} diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index 237aab132..03f851eaa 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -3,10 +3,10 @@ //! //! - `settle`: whole workloads, the state, the sweeps, the ending and the //! accumulates a settle kernel gives from a start. A member's settle kernel -//! is `bit-exact` when it gives exactly every one it takes, and it takes -//! every one it must ([`MUST_TAKE_FIELD_BOUND`]). They reach past one -//! workgroup and past one batch of sweeps, pin the width of a field at 16, 32 -//! and 40 bits, and sit on both sides of the line a member may decline at. +//! is `bit-exact` within its declared capacity ([`super::capacity`]) when it +//! gives exactly every one within it. They reach past one workgroup and past +//! one batch of sweeps, pin the width of a field at 16, 32 and 40 bits, and +//! sit on both sides of a field bound of 2³¹ − 1. //! - `exp`, `schedule`, `cells`: the reference's exponential, its cooling //! schedule and its cut points, bit for bit, so that a text-only //! implementation that computes any of them another way is caught here. @@ -25,8 +25,17 @@ //! ignores its schedule, keeps only the end of it or mishandles the last sweep //! at 0, one whose table is wrong for large fields or costs, one that is //! wrong past the first four cells of a workload, and one whose fields are -//! held in 16 or 32 bits. A member may decline a workload of it only where -//! it may decline a vector. +//! held in 16 or 32 bits. A member is held to every workload within its +//! declared capacity. +//! +//! The core set is the part of these the set held before it grew to reach +//! large jobs, and a long schedule across windows +//! ([`core_settle_cases`], [`core_chain_cases`], [`core_battery`]): the least +//! capacity that takes all of it is the floor a member must declare +//! ([`super::capacity::floor`]). For each number a member declares below what a +//! job can reach, certification adds vectors at that number +//! ([`super::capacity::Capacity::edges`]), and it splits every vector's chains +//! into calls of as many as the host would give the member. //! - `jobs`: whole requests, what their answers hold, refusals included: the //! worked examples and the rules a reader must follow. @@ -35,6 +44,7 @@ use super::energy::Landscape; use super::job; use super::member::{Chain, Member, check_chain, check_settle}; use super::run::state_text; +use super::capacity::Capacity; use super::sample::{anneal_with, chain_seed, cuts, out, schedule, start}; use super::settle::{Outcome, state_hash}; use serde_json::{Value, json}; @@ -192,29 +202,51 @@ fn width_cases() -> Vec<(String, Landscape)> { ] } -/// The widest field bound a member must take: every vector at or below it. The -/// bound is [`super::settle::Workload::field_bound`], the largest field any -/// state can give a cell, couplings included. -pub const MUST_TAKE_FIELD_BOUND: i64 = i32::MAX as i64; - -/// Whether a member declines a vector it may decline, before running it: one -/// whose field bound is above [`MUST_TAKE_FIELD_BOUND`]. `None` when it takes -/// the vector, `Some(true)` when it may skip it, `Some(false)` when it must not. -fn declines(m: &dyn Member, w: &super::settle::Workload, sampling: bool) -> Option { - m.takes(w, sampling).err().map(|_| w.field_bound() > MUST_TAKE_FIELD_BOUND) +/// The settle vectors of the core set: all but the one with fields at 2⁴⁰, +/// as the set held them before it grew. +pub fn core_settle_cases() -> Vec { + const CORE: [&str; 12] = [ + "recall/n=4", + "recall/n=8", + "recall/n=16", + "recall/n=32", + "two-cycle", + "at-rest", + "bound", + "chain-arrives", + "one-sweep-bound", + "field-and-cost", + "zero-and-minus-at-the-start", + "two-cycle-with-fields", + ]; + settle_cases().into_iter().filter(|c| CORE.contains(&c.name.as_str())).collect() } -/// Whether a member's settle kernel gives every `settle` vector it takes -/// exactly, and takes every one it must. +/// Whether a member's settle kernel gives every `settle` vector within its +/// declared capacity exactly, and every vector at that capacity's edges +/// ([`Capacity::edges`]). One past it is not run. pub fn settle_vectors_hold(m: &dyn Member) -> bool { - settle_cases().iter().all(|c| { + settle_vectors_hold_within(m, &m.capacity()) +} + +/// [`settle_vectors_hold`] against a capacity read once, as certification reads it. +pub fn settle_vectors_hold_within(m: &dyn Member, cap: &Capacity) -> bool { + let holds = |c: &SettleCase| { let w = c.landscape.lower(); - if let Some(may) = declines(m, &w, false) { - return may; - } let want = w.settle(&c.start, c.bound); m.settle(&w, &c.start, c.bound).is_ok_and(|o| check_settle(&o, &w, c.bound).is_ok() && o == want) - }) + }; + if !settle_cases().iter().filter(|c| cap.admits_settle(&c.landscape.lower(), c.bound).is_ok()).all(holds) { + return false; + } + let mut all = true; + cap.generate(super::capacity::Want::SETTLE, &mut |g| { + if let super::capacity::Generated::Settle(c) = g { + all = holds(&c); + } + all + }); + all } fn settle_json() -> Vec { @@ -326,6 +358,17 @@ impl ChainCase { /// 53 bits puts them on the right side of it. pub const DRAW_EDGES: [(&str, u64); 3] = [("the-carry-in-a-product", 1982), ("under-the-first-cut-by-its-low-word", 1_174_725), ("under-the-second-cut-by-its-low-word", 6_472_161)]; +/// The chains vectors of the core set: the first fifteen, three landscapes +/// cooling at five schedules, and [`LONG_SCHEDULE`], 5,000 sweeps on 32 +/// cells, which crosses a device's windows of sweeps, by table and by work, +/// whatever capacity at or above the floor it declares. +pub fn core_chain_cases() -> Vec { + chain_cases().into_iter().enumerate().filter(|(k, c)| *k < 15 || c.name == LONG_SCHEDULE).map(|(_, c)| c).collect() +} + +/// The core set's long schedule. +pub const LONG_SCHEDULE: &str = "a-long-schedule-across-windows"; + pub fn chain_cases() -> Vec { let mixed = landscape(8, &[(0, 1, -1), (1, 2, -1), (0, 2, -1), (3, 4, 1), (4, 5, 1), (3, 5, 1), (5, 6, -1), (3, 6, -1), (6, 7, 1)], &[], 1); let dense = stored(&pattern(0xBEEF, 16)); @@ -357,6 +400,22 @@ pub fn chain_cases() -> Vec { out.push(ChainCase { name: "windows-by-their-table".into(), landscape: spread, temperatures: held(&[(400.0, 70), (0.0, 2), (250.0, 70), (150.0, 60)]), seed: 9, restarts: 4 }); // Windows of sweeps, by work: about 170 sweeps a window, couplings of both signs. out.push(ChainCase { name: "windows-by-their-work".into(), landscape: random(1024, 4, 0x71D0), temperatures: held(&[(3.0, 200), (1.5, 200)]), seed: 10, restarts: 2 }); + // Windows of sweeps within the floor, in the core set, so that no capacity a member may + // declare leaves it untested across its windows: 32 cells each coupled to 15 others + // (at offsets 1 to 7, and 16), 31 entries a sweep, held hot for 5,000 + // sweeps. About 530 sweeps a window by table, and 1,930 by work. + let mut circle = Landscape::new(32); + for i in 0..32 { + for k in 1..=7usize { + circle.couple(i, (i + k) % 32, if k % 2 == 1 { 1 } else { -1 }); + } + if i < 16 { + circle.couple(i, i + 16, -1); + } + } + out.push(ChainCase { name: LONG_SCHEDULE.into(), landscape: circle, temperatures: held(&[(20.0, 5000)]), seed: 13, restarts: 4 }); + // A window by work on the smallest workload: 200,000 sweeps of two cells. + out.push(ChainCase { name: "a-schedule-across-a-work-window".into(), landscape: landscape(2, &[(0, 1, 1)], &[(0, 1)], 0), temperatures: held(&[(1.5, 200_000)]), seed: 14, restarts: 2 }); // Fields only their full width tells apart: at temperature 0, and at a temperature // of their own size, so that every cell's field is in its draw. for (name, l) in width_cases() { @@ -370,16 +429,46 @@ pub fn chain_cases() -> Vec { /// Whether a member's sampling kernel gives every `chains` vector it takes /// exactly, every case in one call, and takes every one it must. pub fn chain_vectors_hold(m: &dyn Member) -> bool { - chain_cases().iter().all(|c| { + chain_vectors_hold_within(m, &m.capacity()) +} + +/// [`chain_vectors_hold`] against a capacity read once: every `chains` vector +/// within it and every vector at its edges, each case's chains in calls of as +/// many as the host would give the member at run time, so that a vector is +/// split as a job is and never left out for it. +pub fn chain_vectors_hold_within(m: &dyn Member, cap: &Capacity) -> bool { + let fixed = chain_cases().into_iter().filter(|c| cap.admits(&c.landscape.lower(), &c.temperatures.each()).is_ok()); + if !fixed.into_iter().all(|c| chain_holds(m, cap, &c)) { + return false; + } + let mut all = true; + cap.generate(super::capacity::Want::CHAINS, &mut |g| { + if let super::capacity::Generated::Chains(c) = g { + all = chain_holds(m, cap, &c); + } + all + }); + all +} + +/// Whether a member gives one `chains` case's every chain exactly, in calls of +/// as many as the host would give it. +fn chain_holds(m: &dyn Member, cap: &Capacity, c: &ChainCase) -> bool { + { let w = c.landscape.lower(); let temperatures = c.temperatures.each(); - if let Some(may) = declines(m, &w, temperatures.iter().any(|&t| t > 0.0)) { - return may; - } let chains = c.chains(); + let batch = cap.batch((super::CHUNK_CELLS / w.n().max(1)).clamp(1, super::CHUNK_CHAINS)); let want: Vec = chains.iter().map(|ch| anneal_with(&w, &ch.start, &temperatures, ch.seed)).collect(); - m.chains(&w, &chains, &temperatures).is_ok_and(|got| got.len() == want.len() && got.iter().all(|o| check_chain(o, &w, &temperatures).is_ok()) && got == want) - }) + let mut got = Vec::with_capacity(chains.len()); + for call in chains.chunks(batch) { + match m.chains(&w, call, &temperatures) { + Ok(o) if o.len() == call.len() && o.iter().all(|o| check_chain(o, &w, &temperatures).is_ok()) => got.extend(o), + _ => return false, + } + } + got == want + } } fn chains_json() -> Vec { @@ -451,12 +540,18 @@ pub struct BatteryCase { pub temperatures: Vec, pub law: Law, pub block: usize, + /// The first cell of each block whose final states are counted; empty, every + /// block at a multiple of `block`. A workload of many blocks reads a few, so + /// that so many tests at once do not fail a correct sampler by chance; of + /// alike cells, none coupled, a block may start at any cell. + pub read: Vec, } impl BatteryCase { - fn of(name: &'static str, landscape: Landscape, temperatures: Vec) -> BatteryCase { + /// A workload held to its Boltzmann law at the schedule's last temperature, one block. + pub fn of(name: &'static str, landscape: Landscape, temperatures: Vec) -> BatteryCase { let block = landscape.len(); - BatteryCase { name, landscape, temperatures, law: Law::Settled, block } + BatteryCase { name, landscape, temperatures, law: Law::Settled, block, read: Vec::new() } } /// The temperature the schedule settles at: its last sweep above 0. @@ -537,7 +632,12 @@ impl BatteryCase { } pub fn chains(&self, k: usize) -> Vec { - (0..u64::from(BATTERY_CHAINS)) + self.chains_from(k, 0, u64::from(BATTERY_CHAINS)) + } + + /// Chains `from` up to `to` of workload `k`: a call's worth at a time. + fn chains_from(&self, k: usize, from: u64, to: u64) -> Vec { + (from..to) .map(|r| { let c = chain_seed(BATTERY_SEED + k as u64, r); Chain { start: start(c, self.landscape.len()), seed: c } @@ -560,6 +660,11 @@ fn index_of(s: &[i8]) -> usize { s.iter().rev().fold(0, |acc, &v| acc * 3 + (v + 1) as usize) } +/// The battery's core: its first eight workloads, of two to four cells. +pub fn core_battery() -> Vec { + battery().into_iter().take(8).collect() +} + pub fn battery() -> Vec { let constant = |t: f64, sweeps: usize| vec![t; sweeps]; let mut cooling = vec![6.0; 8]; @@ -595,8 +700,8 @@ pub fn battery() -> Vec { BatteryCase::of("ending-at-zero", landscape(4, &[(0, 1, 1), (1, 2, 1), (2, 3, -1)], &[(0, 1)], 1), quench), BatteryCase::of("cooling", landscape(3, &[(0, 1, -1), (1, 2, -1), (0, 2, -1)], &[(2, -2)], 0), cooling), BatteryCase::of("a-ring-of-six", ring, constant(1.1, 32)), - BatteryCase { name: "triangles-side-by-side", landscape: triangles, temperatures: constant(1.2, 32), law: Law::Settled, block: side }, - BatteryCase { name: "a-schedule-that-matters", landscape: alike, temperatures: two_stages, law: Law::Walked, block: 3 }, + BatteryCase { name: "triangles-side-by-side", landscape: triangles, temperatures: constant(1.2, 32), law: Law::Settled, block: side, read: Vec::new() }, + BatteryCase { name: "a-schedule-that-matters", landscape: alike, temperatures: two_stages, law: Law::Walked, block: 3, read: Vec::new() }, // Fields that only their full width tells apart, at a temperature of their own size: // past 16 bits, which every member takes, and past 32, which a member may decline. BatteryCase::of("fields-past-16-bits", landscape(3, &[(0, 1, 1), (1, 2, -1)], &[(0, 70_000), (1, -65_536), (2, 40_000)], 30_000), constant(40_000.0, 32)), @@ -609,12 +714,13 @@ pub fn battery() -> Vec { #[derive(Clone, Debug, PartialEq)] pub struct BatteryResult { pub name: &'static str, + /// The statistic of the block furthest from its law: Pearson's for the fixed + /// battery, the likelihood ratio for a workload a capacity adds. pub chi_square: f64, pub degrees: usize, pub critical: f64, pub passed: bool, - /// The member declined the workload, which it may where the field bound is above - /// [`MUST_TAKE_FIELD_BOUND`], as for a vector; it then passes without running. + /// The workload is past the member's declared capacity: it passes without running. pub declined: bool, } @@ -663,37 +769,100 @@ fn binned(law: Law, n: usize, by_state: &[f64]) -> Vec { /// each block against the exact distribution. A member that fails to answer, or /// whose answers fail [`check_chain`], fails that workload. pub fn run_battery(m: &dyn Member) -> Vec { - battery() + run_battery_within(m, &m.capacity()) +} + +/// The most chance that a correct sampler fails one generated battery workload, +/// whatever its draws (§7.2): each block is held at the likelihood-ratio +/// statistic this chance, shared among the workload's blocks, bounds by the +/// method of types ([`likelihood_ratio_critical`]); so the few workloads a +/// capacity adds fail a correct sampler less than once in a million between them. +pub const GENERATED_FALSE: f64 = 1e-8; + +/// The likelihood-ratio statistic `G = 2 Σ O ln(O / E)` of `counts` against the +/// law `expected` over `total` draws, no bin pooled: `2·total` times the +/// divergence of the counts from the law. Infinite where a count falls in a bin +/// the law gives no chance. +pub fn likelihood_ratio(counts: &[u64], expected: &[f64], total: u64) -> f64 { + let n = total as f64; + counts .iter() + .zip(expected) + .map(|(&o, &p)| match (o, p > 0.0) { + (0, _) => 0.0, + (_, false) => f64::INFINITY, + (o, true) => 2.0 * o as f64 * (o as f64 / (n * p)).ln(), + }) + .sum() +} + +/// The value of [`likelihood_ratio`] over `bins` bins and `total` draws that a +/// correct sampler's reaches with chance at most `p`, whatever the law. By the +/// method of types, the chance that the counts lie at a divergence `t` or more +/// from the law is at most the number of ways `total` draws fall in `bins` bins, +/// `C(total + bins − 1, bins − 1)`, times `e^(−total·t)`. +pub fn likelihood_ratio_critical(bins: usize, total: u64, p: f64) -> f64 { + let n = total as f64; + let types: f64 = (1..bins).map(|i| ((n + i as f64) / i as f64).ln()).sum(); + 2.0 * (types - p.ln()) +} + +/// [`run_battery`] against a capacity read once: every workload within it, and +/// the workloads at its edges ([`Capacity::edges`]), each in calls of as many +/// chains as the host would give the member. +pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec { + let fixed = battery().len(); + // The fixed workloads in calls of as many chains as the host gives; the ones a + // capacity adds in calls of 63, 62 and on down to 17 chains in turn (no more than + // the host gives), so that their calls end in every partial warp of more than 16 + // chains and every partial workgroup of more than 32. + let whole = cap.batch(4096); + let sizes: Vec = (17..=63).rev().map(|k| cap.batch(k).max(1)).collect(); + battery() + .into_iter() + .chain(cap.edges_of(super::capacity::Want::BATTERY).battery) .enumerate() .map(|(k, case)| { + let case = &case; let w = case.landscape.lower(); - if let Some(may) = declines(m, &w, true) { - return BatteryResult { name: case.name, chi_square: 0.0, degrees: 0, critical: 0.0, passed: may, declined: true }; + if cap.admits(&w, &case.temperatures).is_err() { + return BatteryResult { name: case.name, chi_square: 0.0, degrees: 0, critical: 0.0, passed: true, declined: true }; } let exact = case.exact(); - let blocks = case.landscape.len() / case.block; - let mut counts = vec![vec![0u64; exact.len()]; blocks]; + let blocks: Vec = if case.read.is_empty() { (0..case.landscape.len() / case.block).map(|b| b * case.block).collect() } else { case.read.clone() }; + let mut counts = vec![vec![0u64; exact.len()]; blocks.len()]; let mut ok = true; - for chunk in case.chains(k).chunks(4096) { - match m.chains(&w, chunk, &case.temperatures) { + let mut at = 0u64; + let mut call = 0usize; + while at < u64::from(BATTERY_CHAINS) { + let per_call = if k < fixed { whole } else { sizes[call % sizes.len()] }; + call += 1; + let to = (at + per_call as u64).min(u64::from(BATTERY_CHAINS)); + let chunk = case.chains_from(k, at, to); + match m.chains(&w, &chunk, &case.temperatures) { Ok(got) if got.len() == chunk.len() && got.iter().all(|o| check_chain(o, &w, &case.temperatures).is_ok()) => { for o in got { - for (b, c) in counts.iter_mut().enumerate() { - c[index_of(&o.state[b * case.block..(b + 1) * case.block])] += 1; + for (c, &b) in counts.iter_mut().zip(&blocks) { + c[index_of(&o.state[b..b + case.block])] += 1; } } } _ => ok = false, } + at = to; } let expected = binned(case.law, case.block, &exact); + let generated = likelihood_ratio_critical(expected.len(), u64::from(BATTERY_CHAINS), GENERATED_FALSE / blocks.len().max(1) as f64); let worst = counts .iter() .map(|c| { let as_real: Vec = c.iter().map(|&v| v as f64).collect(); let got: Vec = binned(case.law, case.block, &as_real).iter().map(|&v| v as u64).collect(); - chi_square(&got, &expected, u64::from(BATTERY_CHAINS)) + if k < fixed { + chi_square(&got, &expected, u64::from(BATTERY_CHAINS)) + } else { + (likelihood_ratio(&got, &expected, u64::from(BATTERY_CHAINS)), expected.len() - 1, generated) + } }) .fold((0.0f64, 1usize, 0.0f64), |w, r| if r.0 - r.2 > w.0 - w.2 || w.2 == 0.0 { r } else { w }); let (chi, df, critical) = worst; @@ -795,6 +964,41 @@ mod tests { } } + /// The bound a generated workload's blocks are held to holds exactly: over + /// every way 1,000 draws fall in three bins, under laws with bins a pooled + /// chi-square mishandles, a correct sampler's statistic reaches it with less + /// chance than it is set at. And its values at the battery's size. + #[test] + fn the_likelihood_ratio_bound_holds_exactly() { + let n = 1000u64; + let ln_fact: Vec = (0..=n).scan(0.0f64, |acc, i| { + if i > 0 { + *acc += (i as f64).ln(); + } + Some(*acc) + }).collect(); + for law in [[0.665, 0.245, 0.090], [0.9897, 0.0101, 0.0002], [1.0 / 3.0; 3]] { + for p in [1e-2, 1e-4, 1e-8] { + let critical = likelihood_ratio_critical(3, n, p); + let mut tail = 0.0f64; + for a in 0..=n { + for b in 0..=(n - a) { + let c = n - a - b; + if likelihood_ratio(&[a, b, c], &law, n) >= critical { + let ln = ln_fact[n as usize] - ln_fact[a as usize] - ln_fact[b as usize] - ln_fact[c as usize] + a as f64 * law[0].ln() + b as f64 * law[1].ln() + c as f64 * law[2].ln(); + tail += ln.exp(); + } + } + } + assert!(tail <= p, "{law:?} at {p}: {tail}"); + } + } + let at = |bins: usize, p: f64| likelihood_ratio_critical(bins, u64::from(BATTERY_CHAINS), p); + assert!((at(3, 1e-8) - 75.0).abs() < 1.0 && (at(10, 1e-8) - 189.5).abs() < 1.0 && (at(27, 1e-8) - 429.3).abs() < 1.0, "{} {} {}", at(3, 1e-8), at(10, 1e-8), at(27, 1e-8)); + assert_eq!(likelihood_ratio(&[3, 0], &[1.0, 0.0], 3), 0.0); + assert_eq!(likelihood_ratio(&[2, 1], &[1.0, 0.0], 3), f64::INFINITY); + } + #[test] fn a_state_s_index_is_its_digits() { for k in 0..81 { diff --git a/wai/wai-quantum/src/quantum_landscape/cuda.rs b/wai/wai-quantum/src/quantum_landscape/cuda.rs index 894eab9b9..93f1ae52c 100644 --- a/wai/wai-quantum/src/quantum_landscape/cuda.rs +++ b/wai/wai-quantum/src/quantum_landscape/cuda.rs @@ -286,8 +286,9 @@ impl Member for CudaLandscape { device::label("gpu", &self.name, "CUDA", "its cells in the reference's order") } - fn takes(&self, w: &Workload, sampling: bool) -> Result<(), String> { - if sampling { device::table_fits(&device::reach(w)) } else { Ok(()) } + /// 64-bit fields: no limit but one sweep's cut points, which no job reaches. + fn capacity(&self) -> super::capacity::Capacity { + super::capacity::Capacity { table_entries: Some(device::MAX_TABLE_ENTRIES), ..super::capacity::Capacity::NONE } } fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { diff --git a/wai/wai-quantum/src/quantum_landscape/job.rs b/wai/wai-quantum/src/quantum_landscape/job.rs index ad5c2d8a4..33ea73512 100644 --- a/wai/wai-quantum/src/quantum_landscape/job.rs +++ b/wai/wai-quantum/src/quantum_landscape/job.rs @@ -101,10 +101,17 @@ pub struct Prepared { temperature: Option, seeds: u32, temperatures: Vec, - /// Whether any sweep of the job is above temperature 0. - pub sampling: bool, + /// Every schedule of chains the job runs a member on, a temperature a + /// sweep. A member's capacity is held to each. + pub schedules: Vec>, + /// The bound of every settle the job runs a member on. + pub settles: Vec, /// The kernels the job runs. pub kernels: Vec<&'static str>, + /// Whether the job takes a kernel graded `statistical` (`"parity": + /// "statistical"`): placed, a job is otherwise run only where a kernel gives + /// the reference's bits (§5.1). + pub statistical: bool, } /// Read a request and check every bound of its job before any of it runs: @@ -122,6 +129,12 @@ pub fn prepare(op: &str, request: &Value, max_bytes: u64) -> Result false, + Some(Some("statistical")) => true, + Some(_) => return Err(Refusal::general("A job's `parity` is `bit-exact`, the default, or `statistical`, which also takes a kernel graded statistical.")), + }; let p = read(source)?; let seed = seed_of(request)?; let temperature = temperature_of(request)?; @@ -161,15 +174,46 @@ pub fn prepare(op: &str, request: &Value, max_bytes: u64) -> Result most { return Err(Refusal::general(format!("This job could hold up to {} bytes, and this host allows {}.", super::price::group(bytes), super::price::group(most)))); } - let sampling = op == "compare" || runs.iter().any(|(plan, _)| plan.mode() == super::price::Mode::Sample); + let (mut schedules, mut settles): (Vec>, Vec) = (Vec::new(), Vec::new()); + for (plan, _) in &runs { + let mut each = match plan { + Plan::Settle { sweeps } => { + if !settles.contains(sweeps) { + settles.push(*sweeps); + } + Vec::new() + } + _ => vec![plan.temperatures()], + }; + // A comparison also runs the emulator at temperature 0, with the sampling run's starts and sweeps. + if op == "compare" { + each.push(vec![0.0; plan.sweeps() as usize]); + } + for s in each { + if !schedules.iter().any(|t| t.len() == s.len() && t.iter().zip(&s).all(|(a, b)| a.to_bits() == b.to_bits())) { + schedules.push(s); + } + } + } let mut kernels: Vec<&'static str> = runs.iter().map(|(plan, _)| plan.kernel()).collect(); kernels.sort_unstable(); kernels.dedup(); - Ok(Some(Prepared { job: Job::new(p), op: op.into(), seed, temperature, seeds, temperatures, sampling, kernels })) + Ok(Some(Prepared { job: Job::new(p), op: op.into(), seed, temperature, seeds, temperatures, schedules, settles, kernels, statistical })) } -/// Answer a prepared job on a certified member. +/// Answer a prepared job on a certified member, with what the host's audit +/// checked of it (`audit`, §7.3) where it checked anything. pub fn answer(c: &Certified<'_>, prepared: &Prepared) -> Result { + c.take_audit(); + let mut a = answer_unaudited(c, prepared)?; + let checked = c.take_audit(); + if !checked.is_empty() { + a["audit"] = json!({ "checked": checked }); + } + Ok(a) +} + +fn answer_unaudited(c: &Certified<'_>, prepared: &Prepared) -> Result { let Prepared { job, op, seed, temperature, seeds, temperatures, .. } = prepared; let (seed, temperature, seeds) = (*seed, *temperature, *seeds); match op.as_str() { @@ -198,7 +242,7 @@ pub fn answer(c: &Certified<'_>, prepared: &Prepared) -> Result )); } } - Ok(json!({ "protocol": PROTOCOL, "label": c.member().label(), "member": super::member::describe(c, plan.kernel()), "seeds": seeds, "lines": lines })) + Ok(json!({ "protocol": PROTOCOL, "label": c.label(), "member": super::member::describe(c, plan.kernel()), "seeds": seeds, "lines": lines })) } } } @@ -209,14 +253,29 @@ pub fn answer(c: &Certified<'_>, prepared: &Prepared) -> Result pub fn call_certified(c: &Certified<'_>, op: &str, request: &Value, max_bytes: u64) -> Result { match prepare(op, request, max_bytes)? { None => Ok(examples()), - Some(p) => answer(c, &p), + Some(p) => { + let before = c.demoted().is_some(); + match answer(c, &p) { + Err(r) if !before && c.demoted().is_some() => from_the_reference(&p, vec![json!({ "member": c.member().id(), "why": r.plain() })]), + a => a, + } + } } } +/// The job again on the reference, after the audit demoted the member that ran +/// it (§7.3): the reference's answer, naming each member passed over and why. +fn from_the_reference(p: &Prepared, refusals: Vec) -> Result { + let mut a = answer(&Certified::reference(), p)?; + a["refusals"] = Value::Array(refusals); + Ok(a) +} + /// Answer a request on the first of `members` that can: one certified for /// every kernel the job runs (an uncertified kernel is never placed; a member -/// asked for by name goes through [`call_certified`]), that takes the workload -/// ([`super::member::Member::takes`]), and that gives an answer. Each member +/// asked for by name goes through [`call_certified`]), within whose declared +/// capacity the workload fits at every schedule the job runs +/// ([`super::capacity::Capacity::admits`]), and that gives an answer. Each member /// passed over, for any of those, is named in the answer's `refusals` with why, /// and the next is asked. Put the CPU reference last, and a job that a member /// could not finish is still answered. @@ -227,9 +286,12 @@ pub fn call_placed(members: &[Certified<'_>], op: &str, request: &Value, max_byt let m = c.member(); let why = if let Some(k) = p.kernels.iter().find(|k| c.parity(k) == Parity::Uncertified) { format!("its kernel {k} is not certified") - } else if let Err(why) = m.takes(&p.job.workload, p.sampling) { + } else if let Some(k) = p.kernels.iter().find(|k| c.parity(k) == Parity::Statistical && !p.statistical) { + format!("its kernel {k} is graded statistical, which the host does not audit, and the job asks for the reference's bits (\"parity\": \"statistical\" takes it)") + } else if let Some(why) = p.settles.iter().find_map(|&b| c.capacity().admits_settle(&p.job.workload, b).err()).or_else(|| p.schedules.iter().find_map(|t| c.capacity().admits(&p.job.workload, t).err())) { why } else { + let before = c.demoted().is_some(); match answer(c, &p) { Ok(mut a) => { if !refusals.is_empty() { @@ -237,6 +299,10 @@ pub fn call_placed(members: &[Certified<'_>], op: &str, request: &Value, max_byt } return Ok(a); } + Err(r) if !before && c.demoted().is_some() => { + refusals.push(json!({ "member": m.id(), "why": r.plain() })); + return from_the_reference(&p, refusals); + } Err(r) => r.plain(), } }; @@ -543,8 +609,8 @@ mod tests { assert_eq!(a["rows"][0]["mean_energy"].as_f64(), Some(want), "{}", a["rows"][0]); } - /// A stand-in member with narrow lanes: it declines a workload whose field - /// bound is above `widest`, and runs the reference's kernels otherwise. + /// A stand-in member with narrow lanes: it declares field bounds to `.0` on + /// both sides of temperature 0, and runs the reference's kernels. struct Narrow(i64); impl super::super::member::Member for Narrow { @@ -563,8 +629,34 @@ mod tests { fn chains(&self, w: &super::super::settle::Workload, chains: &[super::super::member::Chain], temperatures: &[f64]) -> Result, String> { super::super::member::CpuReference.chains(w, chains, temperatures) } - fn takes(&self, w: &super::super::settle::Workload, _: bool) -> Result<(), String> { - if w.field_bound() > self.0 { Err(format!("its fields reach {}, beyond {}", w.field_bound(), self.0)) } else { Ok(()) } + fn capacity(&self) -> super::super::capacity::Capacity { + let lanes = super::super::capacity::Limits { field_bound: Some(self.0), ..super::super::capacity::Limits::NONE }; + super::super::capacity::Capacity { at_zero: lanes, above_zero: lanes, ..super::super::capacity::Capacity::NONE } + } + } + + /// A stand-in that declares wide lanes and then refuses, at run time, a + /// workload within them: a member choosing what to decline. + struct Choosy; + + impl super::super::member::Member for Choosy { + fn id(&self) -> String { + "fpga:choosy".into() + } + fn class(&self) -> &'static str { + "fpga" + } + fn label(&self) -> String { + "A board that picks its jobs.".into() + } + fn settle(&self, w: &super::super::settle::Workload, start: &[i8], bound: u32) -> Result { + if w.field_bound() > 1000 { Err("not this one".into()) } else { Ok(w.settle(start, bound)) } + } + fn chains(&self, w: &super::super::settle::Workload, chains: &[super::super::member::Chain], temperatures: &[f64]) -> Result, String> { + if w.field_bound() > 1000 { Err("not this one".into()) } else { super::super::member::CpuReference.chains(w, chains, temperatures) } + } + fn capacity(&self) -> super::super::capacity::Capacity { + Narrow(i64::from(i32::MAX)).capacity() } } @@ -576,7 +668,7 @@ mod tests { let placed = [certify(&lanes32).unwrap(), Certified::reference()]; let wide = call_placed(&placed, "run", &json!({ "program": "vars 2\nfield 0 1099511627776\nagree 0 1\nminimize\n" }), MAX_JOB_BYTES).unwrap(); assert_eq!(wide["member"]["id"], "cpu:reference"); - assert_eq!(wide["refusals"], json!([{ "member": "fpga:narrow-2147483647", "why": "its fields reach 1099511627777, beyond 2147483647" }])); + assert_eq!(wide["refusals"], json!([{ "member": "fpga:narrow-2147483647", "why": "past its declared capacity: its field bound is 1099511627777 at temperature 0, and it holds 2147483647" }])); let narrow = call_placed(&placed, "run", &json!({ "program": example("talks"), "seed": "3" }), MAX_JOB_BYTES).unwrap(); assert_eq!(narrow["member"]["id"], "fpga:narrow-2147483647"); assert!(narrow.get("refusals").is_none()); @@ -585,17 +677,32 @@ mod tests { assert!(none.message.starts_with("No member offered answered this job"), "{}", none.message); } - /// A member may decline only the vectors whose fields do not fit 32 bits: - /// declining one that does is failing it. + /// A member is held to every vector within the capacity it declares, and + /// its grade holds within that capacity, which every answer states. One + /// that refuses a vector within its capacity loses the grade; one that + /// declares a capacity under the floor is not certified at all. #[test] - fn a_member_may_decline_only_what_does_not_fit_32_bits() { + fn a_member_is_held_to_every_vector_within_its_declared_capacity() { use super::super::member::{KERNEL_SAMPLE, KERNEL_SETTLE, Parity, certify}; let lanes32 = Narrow(i64::from(i32::MAX)); let c = certify(&lanes32).unwrap(); assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact)); - let lanes8 = Narrow(10); - let c = certify(&lanes8).unwrap(); - assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::Uncertified, Parity::Uncertified), "it declines vectors whose fields fit 32 bits"); + // At the floor itself: held to the core set and to whatever else fits, and certified. + let at_floor = Narrow(41); + let c = certify(&at_floor).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact)); + let a = call_certified(&c, "run", &json!({ "program": example("talks"), "seed": "3" }), MAX_JOB_BYTES).unwrap(); + assert_eq!(a["member"]["capacity"], json!({ "at_zero": { "field_bound": 41 }, "above_zero": { "field_bound": 41 } })); + assert!(a["label"].as_str().unwrap().ends_with("Its parity holds within the capacity it declares, and the host runs it on no job past it: at temperature 0, a field bound to 41; above temperature 0, a field bound to 41."), "{}", a["label"]); + // Under the floor: not certified, saying which number. + let under = Narrow(40); + let e = certify(&under).err().unwrap(); + assert!(e.contains("under the floor") && e.contains("a field bound at temperature 0 40, and the floor is 41"), "{e}"); + // Declaring 2³¹ − 1 and refusing what it would rather not run: the vectors within it are failed. + let c = certify(&Choosy).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::Uncertified, Parity::Uncertified)); + // The reference declares none, and its answers carry no capacity: the same bytes as before. + assert!(call("run", &json!({ "program": example("talks") })).unwrap()["member"].get("capacity").is_none()); } /// A member whose chains come back where they started: its settle kernel is the @@ -682,7 +789,7 @@ mod tests { for op in ["run", "replay", "sweep", "compare"] { for program in ["vars 3\nfield 0 4294967295\nagree 0 1\nagree 0 2\nminimize steps=1\n", "vars 3\nfield 0 4294967295\nagree 0 1\nsolve\n"] { let r = call_certified(&c, op, &json!({ "program": program, "seeds": 2 }), MAX_JOB_BYTES).unwrap_err(); - assert!(r.message.contains("fpga:narrow-2147483647") && r.message.contains("beyond 2147483647"), "{op}: {}", r.message); + assert!(r.message.contains("fpga:narrow-2147483647") && r.message.contains("past its declared capacity"), "{op}: {}", r.message); } } } diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index a83ba3690..04fdfeeb2 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -17,22 +17,43 @@ //! conformance set and grants what they pass: //! - [`Parity::Reference`]: only this crate's own CPU kernels, through //! [`Certified::reference`]; no member can be granted it. -//! - [`Parity::BitExact`]: its settle kernel gives every `settle` vector it -//! takes bit for bit, or its sampling kernel every `chains` vector it takes, -//! and it takes every one it must ([`conformance::MUST_TAKE_FIELD_BOUND`]). +//! - [`Parity::BitExact`]: its settle kernel gives every `settle` vector +//! within its declared capacity bit for bit, or its sampling kernel every +//! `chains` vector within it. //! - [`Parity::Statistical`]: a sampling kernel with draws of its own whose //! final states, over the battery of `statistical` workloads, follow the //! exact distribution by the test that battery states. //! - [`Parity::Uncertified`]: what passed neither. An answer can still come //! from it when it is asked for by name ([`named`]), and says so. //! +//! A member declares its capacity, the fixed numbers its kernels hold +//! ([`Member::capacity`], [`super::capacity`]). The host runs it on no job past +//! those numbers, holds it to every vector within them, and states them in +//! every answer: a parity holds within the capacity it was found in. A +//! capacity under the floor ([`super::capacity::floor`]) is not certified. +//! //! A member says of itself what kind of run it is ([`Member::label`]) and may //! read a meter ([`Member::meter`]); a reading stands beside the priced figures //! and does not replace them. +use super::capacity::{Capacity, floor}; use super::conformance; use super::settle::{Outcome, Term, Workload}; use serde_json::{Value, json}; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::sync::{Mutex, OnceLock}; + +/// The host's audit of a kernel graded `bit-exact` (§7.3): a settle whose work, +/// the sweeps the host asked for times its couplings and cells, is at most this +/// is run again on the reference whole, every time; past it, the reference's +/// settle as far as this much work goes. +pub const AUDIT_SETTLE_WORK: u64 = 1 << 24; + +/// Past [`AUDIT_SETTLE_WORK`], one settle in this many is run again whole, +/// drawn afresh with the host's audit key on every job. +pub const AUDIT_SETTLE_ONE_IN: u64 = 8; /// The deterministic kernel's name. pub const KERNEL_SETTLE: &str = "wai.quantum.landscape/settle"; @@ -127,16 +148,622 @@ pub trait Member { fn meter(&self) -> Option { None } - /// Whether it takes a workload, said before anything runs: the reason - /// where it does not, and the host then gives the job to the next member it - /// was offered. `sampling` is whether any sweep is above temperature 0. - /// A member declines only what `extensions/quantum-landscape.md` §5 lets it, - /// and the host never runs it on what it declines. - fn takes(&self, _w: &Workload, _sampling: bool) -> Result<(), String> { + /// The fixed numbers its kernels hold, declared up front and the same for + /// every job (`extensions/quantum-landscape.md` §5). The host declines for + /// it exactly the jobs past them ([`Capacity::admits`]) and runs it on no + /// other; it holds the member to every vector within them. None declared is + /// no limit, the reference's. + fn capacity(&self) -> Capacity { + Capacity::NONE + } + /// What the member runs, beyond its id: a bitstream's or a firmware's digest, + /// a driver's version. With its id it is the identity a demotion is kept + /// under (§7.3), so a member that runs something new is certified afresh, and + /// one that runs the same is not. None by default. + fn fingerprint(&self) -> String { + String::new() + } +} + +/// How long the store waits for its file's lock. Past it, the certification +/// that waited grades no kernel bit-exact, and says why; the store is not broken +/// for good, and the next certification waits again. A demotion does not wait on +/// it to be kept: it is kept at once in a file of its own, and only merging it into +/// the store's file waits this long ([`Demotions::record`]). A board job waits no +/// longer than this on the store while it holds the board. +pub const STORE_LOCK_WAIT: std::time::Duration = std::time::Duration::from_secs(5); + +/// The most bytes of a demotion's reason the store keeps on file; a longer one is +/// cut at a character's edge. With it bounded, a certification knows how large the +/// store's next rewrite can be before it grades a member. +pub const STORE_REASON_BYTES: usize = 1024; + +/// How long one of the store's temporary files must have gone unchanged, with no +/// writer holding it, before the store takes it for a killed writer's and removes it. +pub const STORE_STALE_AFTER: std::time::Duration = std::time::Duration::from_secs(10); + +/// What the host remembers of the members its audit demoted (§7.3), keyed by +/// identity (the member's id and fingerprint), and kept in a file where one is +/// named, so that a demotion outlives the process that made it. A demoted +/// identity is certified `uncertified` for both kernels whatever capacity it +/// declares; only another identity is certified afresh. +/// +/// A store with a file `` keeps its demotions there and in files of their +/// own beside it, `..demotion.<32 hex digits>.json`, one a demotion, which +/// the next demotion merges into the file. At every certification it reads them +/// all afresh under a shared lock (on the file `.lock`), and then rewrites +/// the file as it is, the way a demotion rewrites it: a file of this process's own +/// as large as the largest rewrite a demotion can make, synced, and renamed over +/// the file. So a folder that no longer takes a file, a full disk, a limit on a +/// file's size, a file that cannot be replaced (another user's in a sticky folder, +/// one marked immutable or append-only) are each found before a member is graded +/// on the store. A demotion is written at once to a file of its own, with no lock +/// to wait for, and then merged into the file under the exclusive lock, waited for +/// at most [`STORE_LOCK_WAIT`]; where that wait runs out it stays in its own file, +/// which every certification reads, until a later demotion merges it. Its temporary +/// files are named for the store alone (`.....tmp`), +/// each held locked by its writer while it lives, so that one a killed writer left +/// is removed at the next certification or demotion and no other store's, or a +/// live writer's, ever is. +/// +/// Where the path is a link, the store is its target's, followed link by link; a +/// link in a folder anyone may write that has the sticky bit (`/tmp`) is not +/// followed, as the kernel follows another user's there only where +/// `fs.protected_symlinks` is off, and no file of the store's is opened through a +/// link. It fails closed: where the path names no file or a link to nothing, a link +/// in such a folder, a file with more than one name (a hard link, which a rewrite +/// would split), where the file or a demotion of its own cannot be read or does not +/// parse, the file cannot be rewritten as a demotion rewrites it, a write fails, or +/// a host has no file to name, the store is broken, says so on standard error, and +/// the host grades no kernel bit-exact on it ([`certify_with`]); where its lock is +/// held past [`STORE_LOCK_WAIT`], that certification grades none. +pub struct Demotions { + path: Option, + /// Every demotion this process has read or made: none is forgotten here. + records: Mutex>, + /// Why the store cannot keep demotions, once it cannot. + broken: Mutex>, +} + +/// Why the store's lock could not be had: for good, or for now. +enum Unlocked { + Fault(String), + Busy(String), +} + +/// What a file beside a store is to it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Beside { + /// One of its temporary files: a rewrite, a probe or a demotion being written. + Temporary, + /// A demotion kept in a file of its own. + Demotion, +} + +/// What the file `file` in a store's folder is to the store whose file is `name`: +/// `.....tmp`, or +/// `..demotion.<32 lower-case hex digits>.json`, matched exactly. No file is +/// both one store's and another's, whatever their names: a file of a store named +/// `.` has more dots after `..` than either form allows. +fn beside(name: &str, file: &str) -> Option { + let rest = file.strip_prefix('.')?.strip_prefix(name)?.strip_prefix('.')?; + if let Some(hex) = rest.strip_prefix("demotion.").and_then(|r| r.strip_suffix(".json")) { + return (hex.len() == 32 && hex.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))).then_some(Beside::Demotion); + } + let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()); + match rest.strip_suffix(".tmp")?.split('.').collect::>()[..] { + ["write" | "probe" | "demotion", process, count] if digits(process) && digits(count) => Some(Beside::Temporary), + _ => None, + } +} + +/// The open(2) flags that refuse a link at the last step of a path and never wait +/// on a FIFO, where this target's values are known; elsewhere the opened file is +/// still checked against the path ([`open_plain`]). +#[cfg(any(target_vendor = "apple", target_os = "freebsd"))] +const NOFOLLOW_NONBLOCK: i32 = 0x0100 | 0x0004; +#[cfg(all(target_os = "linux", any(target_arch = "x86_64", target_arch = "x86", target_arch = "riscv64", target_arch = "s390x", target_arch = "loongarch64")))] +const NOFOLLOW_NONBLOCK: i32 = 0o400000 | 0o4000; +#[cfg(all(target_os = "linux", any(target_arch = "aarch64", target_arch = "arm", target_arch = "powerpc", target_arch = "powerpc64")))] +const NOFOLLOW_NONBLOCK: i32 = 0o100000 | 0o4000; +#[cfg(all(unix, not(any(target_vendor = "apple", target_os = "freebsd", all(target_os = "linux", any(target_arch = "x86_64", target_arch = "x86", target_arch = "riscv64", target_arch = "s390x", target_arch = "loongarch64", target_arch = "aarch64", target_arch = "arm", target_arch = "powerpc", target_arch = "powerpc64"))))))] +const NOFOLLOW_NONBLOCK: i32 = 0; + +/// A file of the store's opened as it is: never through a link at its last step, +/// and the file opened the plain file the path names, its device and inode checked +/// against the path's own. +fn open_plain(p: &std::path::Path, o: &mut std::fs::OpenOptions) -> std::io::Result { + #[cfg(unix)] + std::os::unix::fs::OpenOptionsExt::custom_flags(o, NOFOLLOW_NONBLOCK); + let f = o.open(p)?; + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + let (opened, named) = (f.metadata()?, std::fs::symlink_metadata(p)?); + if !named.file_type().is_file() || (opened.dev(), opened.ino()) != (named.dev(), named.ino()) { + return Err(std::io::Error::other("it is not a plain file, or it changed as it was opened")); + } + } + Ok(f) +} + +/// Whether a folder is one anyone may write that has the sticky bit. +fn sticky_and_open(dir: &std::path::Path) -> bool { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::metadata(dir).is_ok_and(|m| m.permissions().mode() & 0o1002 == 0o1002) + } + #[cfg(not(unix))] + { + let _ = dir; + false + } +} + +/// How many names a file has (more than one is a hard link); one where that is not known. +fn names(m: &std::fs::Metadata) -> u64 { + #[cfg(unix)] + { + std::os::unix::fs::MetadataExt::nlink(m) + } + #[cfg(not(unix))] + { + let _ = m; + 1 + } +} + +/// A writer that only counts what is written to it. +struct Count(usize); + +impl std::io::Write for Count { + fn write(&mut self, b: &[u8]) -> std::io::Result { + self.0 += b.len(); + Ok(b.len()) + } + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } +/// A reason as the store keeps it on file: its first [`STORE_REASON_BYTES`], cut +/// at a character's edge. +fn on_file_reason(why: &str) -> &str { + let mut end = why.len().min(STORE_REASON_BYTES); + while !why.is_char_boundary(end) { + end -= 1; + } + &why[..end] +} + +/// The largest entry a demotion can add: a reason of [`STORE_REASON_BYTES`] that +/// JSON writes six bytes a byte, and a capacity with every number at its widest. +fn largest_entry() -> Value { + use super::capacity::Limits; + let wide = Limits { field: Some(i64::MIN), field_bound: Some(i64::MIN), cost: Some(i64::MIN) }; + let every = Capacity { cells: Some(u64::MAX), couplings: Some(u64::MAX), at_zero: wide, above_zero: wide, table_entries: Some(u64::MAX), uncertain_span: Some(u64::MAX), sweeps: Some(u64::MAX), chains_per_call: Some(u64::MAX) }; + json!({ "why": "\u{1}".repeat(STORE_REASON_BYTES), "capacity": every.json() }) +} + +/// What a store's file holds, as read: its bytes, its records and its mode; where +/// it does not exist yet, an empty object and no mode. +struct OnFile { + bytes: Vec, + records: BTreeMap, + mode: Option, +} + +impl Demotions { + /// A store kept in `path`, or in memory alone with none (for this process). A + /// path that names no file is a broken store. + pub fn open(path: Option) -> Demotions { + if let Some(p) = &path + && (p.as_os_str().is_empty() || p.file_name().is_none()) + { + return Demotions::broken(format!("{:?} names no file to keep demotions in", p.display().to_string())); + } + Demotions { path, records: Mutex::new(BTreeMap::new()), broken: Mutex::new(None) } + } + + /// A store that cannot keep demotions, saying why: for a host that keeps them + /// in a file and cannot name one. + pub fn broken(why: String) -> Demotions { + let d = Demotions { path: None, records: Mutex::new(BTreeMap::new()), broken: Mutex::new(None) }; + d.fail(why); + d + } + + /// Why the store cannot keep demotions, if it cannot. + pub fn fault(&self) -> Option { + self.broken.lock().map_or_else(|_| Some("the store's lock was poisoned".into()), |b| b.clone()) + } + + fn fail(&self, why: String) -> String { + if let Ok(mut b) = self.broken.lock() + && b.is_none() + { + eprintln!("wai-quantum landscape: the host cannot keep its demotions, and grades no kernel bit-exact: {why}"); + *b = Some(why.clone()); + } + why + } + + fn unlocked(&self, e: Unlocked) -> String { + match e { + Unlocked::Fault(e) => self.fail(e), + Unlocked::Busy(e) => { + eprintln!("wai-quantum landscape: {e}, so this certification grades no kernel bit-exact"); + e + } + } + } + + /// The file the store's path names: a link's target, followed link by link, + /// never the link, so that its lock, its own files and its rename are the + /// target's. A link to nothing is an error, not an empty store, and so is a + /// link in a folder anyone may write that has the sticky bit. + fn target(p: &std::path::Path) -> Result { + let mut at = p.to_path_buf(); + for _ in 0..40 { + match std::fs::symlink_metadata(&at) { + Ok(m) if m.file_type().is_symlink() => { + let dir = at.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new(".")); + if sticky_and_open(dir) { + return Err(format!("{} is a link in {}, a folder anyone may write that has the sticky bit, where the store follows no link", at.display(), dir.display())); + } + let to = std::fs::read_link(&at).map_err(|e| format!("{} is a link that cannot be read ({e})", at.display()))?; + at = dir.join(to); + } + Ok(_) => return Ok(at), + Err(e) if e.kind() == std::io::ErrorKind::NotFound && at == p => return Ok(at), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(format!("{} is a link to nothing that can be read ({} is missing)", p.display(), at.display())), + Err(e) => return Err(format!("{} cannot be read: {e}", at.display())), + } + } + Err(format!("{} is a link through more than 40 links", p.display())) + } + + /// The store's file name, and its folder. + fn place(target: &std::path::Path) -> (String, &std::path::Path) { + let name = target.file_name().map_or_else(|| "demotions".into(), |n| n.to_string_lossy().into_owned()); + (name, target.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new("."))) + } + + /// A new temporary file beside the store's file, named for it, this process and + /// a count, made where no file of that name is and held locked by this process + /// for as long as it is open, so that no other writer takes it for a killed one's. + fn temporary(target: &std::path::Path, what: &str) -> std::io::Result<(PathBuf, std::fs::File)> { + static MADE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let (name, _) = Demotions::place(target); + let mut last = None; + for _ in 0..64 { + let p = target.with_file_name(format!(".{name}.{what}.{}.{}.tmp", std::process::id(), MADE.fetch_add(1, std::sync::atomic::Ordering::Relaxed))); + match std::fs::OpenOptions::new().write(true).create_new(true).open(&p) { + Ok(f) => { + if let Err(e) = f.lock() { + let _ = std::fs::remove_file(&p); + return Err(e); + } + return Ok((p, f)); + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => last = Some(e), + Err(e) => return Err(e), + } + } + Err(last.unwrap_or_else(|| std::io::Error::other("no free name"))) + } + + /// The lock beside the file, held shared or exclusive until it is dropped, its + /// folder made where it is missing; waited for at most [`STORE_LOCK_WAIT`]. + fn lock(path: &std::path::Path, exclusive: bool) -> Result { + if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) { + std::fs::create_dir_all(dir).map_err(|e| Unlocked::Fault(format!("its folder {} cannot be made: {e}", dir.display())))?; + } + let mut name = path.as_os_str().to_owned(); + name.push(".lock"); + let at = PathBuf::from(name); + let f = open_plain(&at, std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false)).map_err(|e| Unlocked::Fault(format!("its lock {} cannot be opened: {e}", at.display())))?; + let until = std::time::Instant::now() + STORE_LOCK_WAIT; + loop { + match if exclusive { f.try_lock() } else { f.try_lock_shared() } { + Ok(()) => return Ok(f), + Err(std::fs::TryLockError::WouldBlock) if std::time::Instant::now() < until => std::thread::sleep(std::time::Duration::from_millis(10)), + Err(std::fs::TryLockError::WouldBlock) => return Err(Unlocked::Busy(format!("the lock {} of the host's demotions was held past {} s", at.display(), STORE_LOCK_WAIT.as_secs()))), + Err(std::fs::TryLockError::Error(e)) => return Err(Unlocked::Fault(format!("its lock {} cannot be taken: {e}", at.display()))), + } + } + } + + /// The file as it is: none where it does not exist yet. A file with more than + /// one name is refused: a rewrite renames a new file into place under one name + /// alone, and would split it from the others for good. + fn read(path: &std::path::Path) -> Result { + use std::io::Read; + let mut f = match open_plain(path, std::fs::OpenOptions::new().read(true)) { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(OnFile { bytes: b"{}".to_vec(), records: BTreeMap::new(), mode: None }), + Err(e) => return Err(format!("{} cannot be read: {e}", path.display())), + }; + let m = f.metadata().map_err(|e| format!("{} cannot be read: {e}", path.display()))?; + if names(&m) > 1 { + return Err(format!("{} has {} names (a hard link): a rewrite would give one of them a file of its own, so the store keeps no demotions in it; share a store by naming one path, or by a symbolic link", path.display(), names(&m))); + } + let mut bytes = Vec::new(); + f.read_to_end(&mut bytes).map_err(|e| format!("{} cannot be read: {e}", path.display()))?; + let records = serde_json::from_slice(&bytes).map_err(|e| format!("{} does not parse as demotions: {e}", path.display()))?; + Ok(OnFile { bytes, records, mode: Some(m.permissions()) }) + } + + /// The demotions kept in files of their own beside the store's file, and those files. + fn read_own(target: &std::path::Path) -> Result<(BTreeMap, Vec), String> { + use std::io::Read; + let (name, dir) = Demotions::place(target); + let mut records = BTreeMap::new(); + let mut files = Vec::new(); + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok((records, files)), + Err(e) => return Err(format!("the folder {} cannot be read: {e}", dir.display())), + }; + for e in entries { + let e = e.map_err(|e| format!("the folder {} cannot be read: {e}", dir.display()))?; + if e.file_name().to_str().and_then(|f| beside(&name, f)) != Some(Beside::Demotion) { + continue; + } + let p = e.path(); + let mut bytes = Vec::new(); + match open_plain(&p, std::fs::OpenOptions::new().read(true)).and_then(|mut f| f.read_to_end(&mut bytes)) { + Ok(_) => {} + // Merged and removed since the folder was listed. + Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, + Err(e) => return Err(format!("{} cannot be read: {e}", p.display())), + } + let one: BTreeMap = serde_json::from_slice(&bytes).map_err(|e| format!("{} does not parse as demotions: {e}", p.display()))?; + for (k, v) in one { + records.entry(k).or_insert(v); + } + files.push(p); + } + Ok((records, files)) + } + + /// Remove what killed writers left beside the store: each of its temporary files + /// unchanged for [`STORE_STALE_AFTER`] whose lock no writer holds. + fn sweep(target: &std::path::Path) { + let (name, dir) = Demotions::place(target); + let Ok(entries) = std::fs::read_dir(dir) else { return }; + for e in entries.flatten() { + if e.file_name().to_str().and_then(|f| beside(&name, f)) != Some(Beside::Temporary) { + continue; + } + let p = e.path(); + let old = std::fs::symlink_metadata(&p).is_ok_and(|m| m.file_type().is_file() && m.modified().ok().and_then(|t| t.elapsed().ok()).is_some_and(|age| age >= STORE_STALE_AFTER)); + if old && let Ok(f) = open_plain(&p, std::fs::OpenOptions::new().write(true)) && f.try_lock().is_ok() { + let _ = std::fs::remove_file(&p); + } + } + } + + /// How large the store's file can be once a demotion of `identity` is merged + /// into what it holds: as a demotion writes it, with the largest entry it can add. + fn largest_rewrite(records: &mut BTreeMap, identity: &str) -> usize { + let added = !records.contains_key(identity); + if added { + records.insert(identity.to_string(), largest_entry()); + } + let mut n = Count(0); + let size = serde_json::to_writer_pretty(&mut n, &*records).map_or(usize::MAX, |()| n.0); + if added { + records.remove(identity); + } + size + } + + /// Rewrite the store's file as it is, the way a demotion rewrites it: a file of + /// this process's own beside it, its bytes padded with line ends to `size`, the + /// most a demotion can write, with the file's mode, synced, cut back to its bytes + /// and renamed over the file. A folder that no longer takes a file, a full disk, + /// a limit on a file's size and a file that cannot be replaced each fail here, + /// before a member is graded on the store. + fn takes_a_rewrite(target: &std::path::Path, f: &OnFile, size: usize) -> Result<(), String> { + use std::io::Write; + let wrote = Demotions::temporary(target, "probe").and_then(|(tmp, mut file)| { + let put = (|| { + file.write_all(&f.bytes)?; + let pad = [b'\n'; 1 << 16]; + let mut left = size.saturating_sub(f.bytes.len()); + while left > 0 { + let k = left.min(pad.len()); + file.write_all(&pad[..k])?; + left -= k; + } + if let Some(m) = &f.mode { + file.set_permissions(m.clone())?; + } + file.sync_all()?; + file.set_len(f.bytes.len() as u64)?; + std::fs::rename(&tmp, target) + })(); + if put.is_err() { + let _ = std::fs::remove_file(&tmp); + } + put + }); + wrote.map_err(|e| format!("{} does not take a rewrite of it as a demotion makes one ({size} bytes, renamed over it): {e}", target.display())) + } + + /// Why an identity was demoted, if it was, its file and its own demotions read + /// afresh and the file shown to take a rewrite; an error where the store is + /// broken, and then nothing it holds can be trusted to be all. + pub fn get(&self, identity: &str) -> Result, String> { + if let Some(why) = self.fault() { + return Err(why); + } + let on_file = match &self.path { + None => BTreeMap::new(), + Some(p) => { + let t = Demotions::target(p).map_err(|e| self.fail(e))?; + let held = Demotions::lock(&t, false).map_err(|e| self.unlocked(e))?; + let mut f = Demotions::read(&t).map_err(|e| self.fail(e))?; + let (own, _) = Demotions::read_own(&t).map_err(|e| self.fail(e))?; + let mut all = std::mem::take(&mut f.records); + for (k, v) in own { + all.entry(k).or_insert(v); + } + Demotions::sweep(&t); + let size = Demotions::largest_rewrite(&mut all, identity); + Demotions::takes_a_rewrite(&t, &f, size).map_err(|e| self.fail(e))?; + drop(held); + all + } + }; + let mut records = self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?; + for (k, v) in on_file { + records.entry(k).or_insert(v); + } + Ok(records.get(identity).map(|r| r["why"].as_str().map_or_else(|| r.to_string(), String::from))) + } + + /// Keep a demotion in a file of its own beside the store's, with the store's + /// mode: written to a temporary file, synced, and renamed to a name no other + /// file has (128 bits drawn afresh). No lock is taken: every reader reads it. + fn keep_alone(target: &std::path::Path, identity: &str, entry: &Value, mode: Option) -> Result { + use std::hash::{BuildHasher, Hasher}; + use std::io::Write; + let (name, _) = Demotions::place(target); + let draw = |k: u64| { + let mut h = std::collections::hash_map::RandomState::new().build_hasher(); + h.write_u64(k); + h.write_u32(std::process::id()); + h.write_u128(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos())); + h.finish() + }; + let own = target.with_file_name(format!(".{name}.demotion.{:016x}{:016x}.json", draw(1), draw(2))); + let mut one = serde_json::Map::new(); + one.insert(identity.to_string(), entry.clone()); + let text = serde_json::to_string_pretty(&one).map_err(|e| e.to_string())?; + let (tmp, mut f) = Demotions::temporary(target, "demotion").map_err(|e| format!("a demotion cannot be written beside {}: {e}", target.display()))?; + let put = (|| { + f.write_all(text.as_bytes())?; + if let Some(m) = mode { + f.set_permissions(m)?; + } + f.sync_all()?; + std::fs::rename(&tmp, &own) + })(); + if let Err(e) = put { + let _ = std::fs::remove_file(&tmp); + return Err(format!("a demotion cannot be written beside {}: {e}", target.display())); + } + Ok(own) + } + + /// Under the store's exclusive lock: merge into its file every demotion kept in a + /// file of its own and `identity`'s, written whole to a file of this process's own + /// with the file's mode, synced and renamed into place; then remove the files of + /// their own it merged, and any temporary file a killed writer left. + fn merge(&self, t: &std::path::Path, identity: &str, entry: &Value) -> Result<(), String> { + use std::io::Write; + let f = Demotions::read(t)?; + let mut on_file = f.records; + let (own, merged) = Demotions::read_own(t)?; + for (k, v) in own { + on_file.entry(k).or_insert(v); + } + on_file.entry(identity.to_string()).or_insert_with(|| entry.clone()); + if let Ok(mut records) = self.records.lock() { + for (k, v) in on_file.iter() { + records.entry(k.clone()).or_insert_with(|| v.clone()); + } + } + Demotions::sweep(t); + let text = serde_json::to_string_pretty(&on_file).map_err(|e| e.to_string())?; + let (tmp, mut file) = Demotions::temporary(t, "write").map_err(|e| format!("{} cannot be written: {e}", t.display()))?; + let put = (|| { + file.write_all(text.as_bytes())?; + if let Some(m) = &f.mode { + file.set_permissions(m.clone())?; + } + file.sync_all()?; + std::fs::rename(&tmp, t) + })(); + if let Err(e) = put { + let _ = std::fs::remove_file(&tmp); + return Err(format!("{} cannot be written: {e}", t.display())); + } + for p in merged { + let _ = std::fs::remove_file(p); + } + Ok(()) + } + + /// Remember a demotion, and where the store has a file, keep it there: at once + /// in a file of its own beside the store's, which every certification reads, and + /// then merged into the store's file under its exclusive lock. The merge waits for + /// the lock at most [`STORE_LOCK_WAIT`]; where the wait runs out, the demotion + /// stays in its own file until a later demotion merges it, and is kept all the + /// same. The first reason stands, its first [`STORE_REASON_BYTES`] on file. An + /// error where it could not be kept past this process: neither its own file nor + /// the merge could be written. + pub fn record(&self, identity: &str, why: &str, capacity: &Capacity) -> Result<(), String> { + let entry = json!({ "why": on_file_reason(why), "capacity": capacity.json() }); + self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?.entry(identity.to_string()).or_insert_with(|| entry.clone()); + if let Some(why) = self.fault() { + return Err(why); + } + let Some(p) = &self.path else { return Ok(()) }; + let t = Demotions::target(p).map_err(|e| self.fail(e))?; + let mode = std::fs::metadata(&t).ok().map(|m| m.permissions()); + let alone = Demotions::keep_alone(&t, identity, &entry, mode); + let merged = match Demotions::lock(&t, true) { + Ok(held) => { + let m = self.merge(&t, identity, &entry); + drop(held); + m.map_err(|e| (self.fail(e.clone()), false)) + } + Err(Unlocked::Busy(e)) => Err((e, true)), + Err(Unlocked::Fault(e)) => Err((self.fail(e.clone()), false)), + }; + match (alone, merged) { + (_, Ok(())) => Ok(()), + (Ok(own), Err((e, busy))) => { + if busy { + eprintln!("wai-quantum landscape: {e}, so this demotion is kept in {}, which every certification reads, until a later demotion merges it", own.display()); + } + Ok(()) + } + (Err(a), Err((e, _))) => Err(self.fail(format!("{a}; and {e}"))), + } + } +} + +static DEMOTIONS: OnceLock = OnceLock::new(); + +/// The host's store of demotions: the file `WAI_LANDSCAPE_DEMOTIONS` names (an +/// empty value names none), or +/// the store a host chose with [`keep_demotions`] before its first use; in +/// memory alone otherwise, for this process. +pub fn demotions() -> &'static Demotions { + DEMOTIONS.get_or_init(|| Demotions::open(std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").filter(|v| !v.is_empty()).map(PathBuf::from))) +} + +/// Keep the host's demotions in `store` (a host that runs members does so at +/// start). False where the store was already in use. +pub fn keep_demotions(store: Demotions) -> bool { + DEMOTIONS.set(store).is_ok() +} + +/// Why a member is held at `uncertified` on a broken store. +fn cannot_keep(e: &str) -> String { + format!("the host cannot keep its demotions, so it grades no kernel bit-exact: {e}") +} + +/// A member's identity, as demotions are kept under it. +pub fn identity_of(m: &dyn Member) -> String { + format!("{} {}", m.id(), m.fingerprint()) +} + /// The reference: this crate's kernels, on this machine's CPU. #[derive(Clone, Copy, Debug, Default)] pub struct CpuReference; @@ -163,18 +790,35 @@ impl Member for CpuReference { } } +/// What an answer from a sampling kernel graded `statistical` says of the grade +/// (§7.2): what it rests on and what it misses. +pub const STATISTICAL_GRADE: &str = "Its sampling kernel is graded statistical: its final states passed the battery's test (§7.2) on the battery's workloads alone. A fault at a count of cells the battery does not run, or a law near the exact one (blocks of three drawn at a temperature 10% off pass the test), can keep the grade, and the host does not audit it."; + /// A member with the parity the host found for each kernel. pub struct Certified<'a> { member: &'a dyn Member, settle: Parity, sample: Parity, + capacity: Capacity, + /// Whether the host certified it, or runs it by name ([`named`]). + certified: bool, + /// Where a demotion is remembered. + store: &'a Demotions, + /// Why the host's audit demoted it, once it has: a kernel graded + /// `bit-exact` gave other bits than the reference's on a job (§7.3); or why + /// the host holds it there, where its store of demotions is broken. From + /// then on both its kernels are `uncertified`, and its label says why. + demoted: RefCell>, + /// What the audit checked since the last answer took it: each run's seed + /// and the chains of it run again, or how much of a settle. + audit_log: RefCell>, } impl Certified<'static> { /// The CPU reference, at parity `reference` for both kernels. pub fn reference() -> Certified<'static> { static REFERENCE: CpuReference = CpuReference; - Certified { member: &REFERENCE, settle: Parity::Reference, sample: Parity::Reference } + Certified { member: &REFERENCE, settle: Parity::Reference, sample: Parity::Reference, capacity: Capacity::NONE, certified: true, store: demotions(), demoted: RefCell::new(None), audit_log: RefCell::new(Vec::new()) } } } @@ -183,9 +827,63 @@ impl<'a> Certified<'a> { self.member } - /// The parity the host found for a kernel. + /// The parity the host found for a kernel, which holds within + /// [`Certified::capacity`]: `uncertified` once the audit has demoted it. pub fn parity(&self, kernel: &str) -> Parity { - if kernel == KERNEL_SETTLE { self.settle } else { self.sample } + let p = if kernel == KERNEL_SETTLE { self.settle } else { self.sample }; + if p != Parity::Reference && self.demoted.borrow().is_some() { Parity::Uncertified } else { p } + } + + /// Note what the audit checked, for the answer it is part of. + pub(super) fn log_audit(&self, v: Value) { + self.audit_log.borrow_mut().push(v); + } + + /// What the audit checked since the last call: each entry a run's seed and + /// the chains of it run again on the reference, or how much of a settle. + pub fn take_audit(&self) -> Vec { + std::mem::take(&mut *self.audit_log.borrow_mut()) + } + + /// Why the host's audit demoted it, if it has. + pub fn demoted(&self) -> Option { + self.demoted.borrow().clone() + } + + /// Demote it to `uncertified` for both kernels, saying why: what the host + /// does when a kernel graded `bit-exact` gives other bits than the + /// reference's (§7.3). The first reason stands. + pub fn demote(&self, why: String) { + let mut d = self.demoted.borrow_mut(); + if d.is_none() { + let kept = self.store.record(&identity_of(self.member), &why, &self.capacity); + *d = Some(match kept { + Ok(()) => why, + Err(e) => format!("{why} (the host could not keep this demotion past this process: {e})"), + }); + } + } + + /// The capacity the member declared when the host certified it. + pub fn capacity(&self) -> Capacity { + self.capacity + } + + /// What every answer from it says of itself: its own label, the capacity its + /// parity holds within where it declares one, and what a `statistical` grade + /// rests on where its sampling kernel has one ([`STATISTICAL_GRADE`]). + pub fn label(&self) -> String { + let mut label = self.member.label(); + if let Some(w) = self.capacity.words(self.certified) { + label = format!("{label} {w}"); + } + if self.parity(KERNEL_SAMPLE) == Parity::Statistical { + label = format!("{label} {STATISTICAL_GRADE}"); + } + if let Some(d) = self.demoted() { + label = format!("{label} The host demoted it to uncertified: {d}."); + } + label } } @@ -214,23 +912,45 @@ pub fn check_id(m: &dyn Member) -> Result<(), String> { /// it, and says so. pub fn named(m: &dyn Member) -> Result, String> { check_id(m)?; - Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified }) + let demoted = demotions().get(&identity_of(m)).unwrap_or_else(|e| Some(cannot_keep(&e))); + Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified, capacity: m.capacity(), certified: false, store: demotions(), demoted: RefCell::new(demoted), audit_log: RefCell::new(Vec::new()) }) } -/// Certify a member: its settle kernel on every `settle` vector, its sampling -/// kernel on every `chains` vector and, where those differ, on the battery of -/// `statistical` workloads. +/// Certify a member within its declared capacity: its settle kernel on every +/// `settle` vector within it, its sampling kernel on every `chains` vector +/// within it and, where those differ, on the battery of `statistical` +/// workloads within it. A capacity under the floor is refused: a member cannot +/// be certified by declaring itself too small to be held to the core set. pub fn certify(m: &dyn Member) -> Result, String> { + certify_with(m, demotions()) +} + +/// [`certify`], remembering demotions in `store`: an identity it holds as +/// demoted is `uncertified` for both kernels, whatever it declares, and is not +/// certified again. +pub fn certify_with<'a>(m: &'a dyn Member, store: &'a Demotions) -> Result, String> { check_id(m)?; - let settle = if conformance::settle_vectors_hold(m) { Parity::BitExact } else { Parity::Uncertified }; - let sample = if conformance::chain_vectors_hold(m) { + let capacity = m.capacity(); + let under = capacity.under(&floor()); + if !under.is_empty() { + return Err(format!("{} declares a capacity under the floor every member is held to: {}", m.id(), under.join("; "))); + } + // A demoted identity, or one the host cannot tell is not, or whose demotion it + // could not keep: both kernels uncertified, and certified no further. + if let Some(why) = store.get(&identity_of(m)).unwrap_or_else(|e| Some(cannot_keep(&e))) { + return Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified, capacity, certified: true, store, demoted: RefCell::new(Some(why)), audit_log: RefCell::new(Vec::new()) }); + } + // One reading of the capacity: what the floor checked, what the vectors are held to, + // and what every answer states. + let settle = if conformance::settle_vectors_hold_within(m, &capacity) { Parity::BitExact } else { Parity::Uncertified }; + let sample = if conformance::chain_vectors_hold_within(m, &capacity) { Parity::BitExact - } else if conformance::run_battery(m).iter().all(|r| r.passed) { + } else if conformance::run_battery_within(m, &capacity).iter().all(|r| r.passed) { Parity::Statistical } else { Parity::Uncertified }; - Ok(Certified { member: m, settle, sample }) + Ok(Certified { member: m, settle, sample, capacity, certified: true, store, demoted: RefCell::new(None), audit_log: RefCell::new(Vec::new()) }) } fn check_cells(o: &Outcome, n: usize) -> Result<(), String> { @@ -291,9 +1011,22 @@ pub fn check_chain(o: &Outcome, w: &Workload, temperatures: &[f64]) -> Result<() } /// What an answer says of the member that ran it. +/// What an answer says of the member that ran it, with the capacity its parity +/// holds within where it declares one. With none declared, as the reference's, +/// there is no `capacity`, and the answer is the same bytes it always was. pub(super) fn describe(c: &Certified<'_>, kernel: &str) -> Value { let m = c.member(); - json!({ "id": m.id(), "class": m.class(), "kernel": kernel, "parity": c.parity(kernel).label() }) + with_capacity(c, json!({ "id": m.id(), "class": m.class(), "kernel": kernel, "parity": c.parity(kernel).label() })) +} + +fn with_capacity(c: &Certified<'_>, mut v: Value) -> Value { + if c.capacity().is_declared() { + v["capacity"] = c.capacity().json(); + } + if let Some(d) = c.demoted() { + v["demoted"] = json!(d); + } + v } /// What an answer that may run both kernels says of the member that ran it: @@ -306,7 +1039,7 @@ pub(super) fn describe_all(c: &Certified<'_>, kernels: &[&str]) -> Value { used.push(k); } } - json!({ "id": m.id(), "class": m.class(), "kernels": used.iter().map(|k| json!({ "kernel": k, "parity": c.parity(k).label() })).collect::>() }) + with_capacity(c, json!({ "id": m.id(), "class": m.class(), "kernels": used.iter().map(|k| json!({ "kernel": k, "parity": c.parity(k).label() })).collect::>() })) } #[cfg(test)] diff --git a/wai/wai-quantum/src/quantum_landscape/metal.rs b/wai/wai-quantum/src/quantum_landscape/metal.rs index cab9a608f..1a812fb50 100644 --- a/wai/wai-quantum/src/quantum_landscape/metal.rs +++ b/wai/wai-quantum/src/quantum_landscape/metal.rs @@ -185,8 +185,9 @@ impl Member for MetalLandscape { device::label("gpu", &self.name, "Metal", "its cells in the reference's order") } - fn takes(&self, w: &Workload, sampling: bool) -> Result<(), String> { - if sampling { device::table_fits(&device::reach(w)) } else { Ok(()) } + /// 64-bit fields: no limit but one sweep's cut points, which no job reaches. + fn capacity(&self) -> super::capacity::Capacity { + super::capacity::Capacity { table_entries: Some(device::MAX_TABLE_ENTRIES), ..super::capacity::Capacity::NONE } } fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { diff --git a/wai/wai-quantum/src/quantum_landscape/reach.rs b/wai/wai-quantum/src/quantum_landscape/reach.rs index 202b46e0c..ab652351e 100644 --- a/wai/wai-quantum/src/quantum_landscape/reach.rs +++ b/wai/wai-quantum/src/quantum_landscape/reach.rs @@ -38,6 +38,18 @@ pub struct Reach { pub entries: u64, } +impl Reach { + /// Every field the cells can reach, lowest first. + pub fn fields(&self) -> impl Iterator + '_ { + self.spans.iter().flat_map(|&(lo, hi)| lo..=hi) + } + + /// Every field the cells can reach, highest first. + pub fn fields_back(&self) -> impl Iterator + '_ { + self.spans.iter().rev().flat_map(|&(lo, hi)| (lo..=hi).rev()) + } +} + /// What the cells of a workload can reach. pub fn reach(w: &Workload) -> Reach { let n = w.n(); diff --git a/wai/wai-quantum/src/quantum_landscape/run.rs b/wai/wai-quantum/src/quantum_landscape/run.rs index 488739659..7896db086 100644 --- a/wai/wai-quantum/src/quantum_landscape/run.rs +++ b/wai/wai-quantum/src/quantum_landscape/run.rs @@ -12,7 +12,7 @@ use super::member::{self, Certified, Chain, KERNEL_SAMPLE, KERNEL_SETTLE}; use super::price::{self, Mode, group, joules}; use super::program::{Check, Graded as CheckGraded, Program, Refusal, RunLine, check_items, check_temperature, grade}; use super::sample; -use super::settle::{Term, Workload, state_hash}; +use super::settle::{Outcome, Term, Workload, state_hash}; use super::truth::{Truth, exhaustive_checked, exhaustive_declared}; use super::{CHUNK_CELLS, CHUNK_CHAINS, EXHAUSTIVE_MAX_VARS, PROTOCOL}; use serde_json::{Value, json}; @@ -317,17 +317,94 @@ fn add_reading(total: &mut Option>, reading: Option &'static [u8; 32] { + static KEY: std::sync::OnceLock<[u8; 32]> = std::sync::OnceLock::new(); + KEY.get_or_init(|| { + use std::hash::{BuildHasher, Hasher}; + let mut h = blake3::Hasher::new(); + for k in 0..8u64 { + let mut x = std::collections::hash_map::RandomState::new().build_hasher(); + x.write_u64(k); + h.update(&x.finish().to_le_bytes()); + } + let here = 0u8; + h.update(&(std::ptr::addr_of!(here) as usize).to_le_bytes()); + *h.finalize().as_bytes() + }) +} + +/// A number for the audit's choices: keyed with the host's secret, over the +/// job's program, the seed, a purpose and a count that every draw moves on, so +/// that the same job drawn again draws afresh. +fn audit_draw(job: &Job, seed: u64, purpose: u64) -> u64 { + static DRAWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let mut h = blake3::Hasher::new_keyed(audit_key()); + h.update(job.program.source.as_bytes()); + h.update(&seed.to_le_bytes()); + h.update(&purpose.to_le_bytes()); + h.update(&DRAWS.fetch_add(1, std::sync::atomic::Ordering::Relaxed).to_le_bytes()); + let b = h.finalize(); + u64::from_le_bytes(b.as_bytes()[..8].try_into().expect("eight bytes")) +} + +/// The audit found a kernel graded `bit-exact` giving other bits than the +/// reference's: the member is demoted at once, and the job goes to the reference (§7.3). +fn demote(c: &Certified<'_>, kernel: &str, what: String) -> Refusal { + let m = c.member(); + c.demote(format!("its {kernel} kernel, graded bit-exact, {what}")); + Refusal::general(format!("The host's audit found that the member {} gave other bits than the reference's, and demoted it to uncertified: {}.", m.id(), c.demoted().unwrap_or_default())) +} + fn execute(job: &Job, plan: Plan, seed: u64, c: &Certified<'_>) -> Result { let w = &job.workload; let n = w.n(); let m = c.member(); let refused = |e: String| Refusal::general(format!("The member {} refused or failed: {e}.", m.id())); + let declined = |e: String| Refusal::general(format!("The host declined this job for {}: {e}.", m.id())); match plan { Plan::Settle { sweeps } => { // The host gives a member only what it takes, however it was reached. - m.takes(w, false).map_err(refused)?; - let o = m.settle(w, &vec![0i8; n], sweeps).map_err(refused)?; - member::check_settle(&o, w, sweeps).map_err(refused)?; + c.capacity().admits_settle(w, sweeps).map_err(declined)?; + let start = vec![0i8; n]; + let o = m.settle(w, &start, sweeps).map_err(refused)?; + let audited = c.parity(KERNEL_SETTLE) == member::Parity::BitExact; + if let Err(e) = member::check_settle(&o, w, sweeps) { + return Err(if audited { demote(c, KERNEL_SETTLE, format!("gave an answer the host's checks refuse ({e})")) } else { refused(e) }); + } + // The audit, from what the host computes alone: the reference's settle, as + // far as a budget of work goes (the cells and couplings times the sweeps the + // host asked for, never the sweeps the member reports). Where the reference + // comes to rest within it, or the whole bound fits it, that is the whole + // answer, and it is compared; past it, the whole settle one job in so many, + // drawn with the host's key, and otherwise a member that stops sooner than + // the reference did is wrong. + if audited { + let per = (w.nonzeros() + n as u64).max(1); + let most = (member::AUDIT_SETTLE_WORK / per).clamp(1, u64::from(u32::MAX)); + let reach = u64::from(sweeps).min(most) as u32; + let prefix = w.settle(&start, reach); + let whole = u64::from(sweeps) <= most || prefix.term != Term::Bound; + let (want, how) = if whole { + (Some(prefix), "whole".to_string()) + } else if audit_draw(job, seed, 0).is_multiple_of(member::AUDIT_SETTLE_ONE_IN) { + (Some(w.settle(&start, sweeps)), "whole, drawn".to_string()) + } else { + (None, format!("its first {reach} sweeps")) + }; + c.log_audit(json!({ "seed": seed.to_string(), "settle": how })); + let wrong = match want { + Some(want) => o != want, + None => o.sweeps <= reach, + }; + if wrong { + return Err(demote(c, KERNEL_SETTLE, format!("settled a workload of {n} cells to another state than the reference's"))); + } + } let energy = job.program.landscape.energy(&o.state); Ok(Found { energy, accumulates: o.accumulates, draws: o.draws, sweeps: u64::from(o.sweeps), term: Some(o.term), state: o.state, metered: m.meter() }) } @@ -353,11 +430,21 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 let n = w.n(); let m = c.member(); let refused = |e: String| Refusal::general(format!("The member {} refused or failed: {e}.", m.id())); - m.takes(w, temperatures.iter().any(|&t| t > 0.0)).map_err(refused)?; - let batch = (CHUNK_CELLS / n.max(1)).clamp(1, CHUNK_CHAINS); + let declined = |e: String| Refusal::general(format!("The host declined this job for {}: {e}.", m.id())); + let cap = c.capacity(); + cap.admits(w, temperatures).map_err(declined)?; + // The audit of a sampling kernel graded `bit-exact`: in every call, the last + // chain, and one chain of each seed's run among those the call holds, drawn + // afresh with the host's key, so that what the host checks in one call says + // nothing of the next; run again on the reference, on the audit's threads, + // after the call and before the next. + let audited = c.parity(KERNEL_SAMPLE) == member::Parity::BitExact; + // No more chains in a call than the member declares it takes. + let batch = cap.batch((CHUNK_CELLS / n.max(1)).clamp(1, CHUNK_CHAINS)); let r = u64::from(restarts); // Every (seed, start) in order: seed after seed, start after start. let total = seeds.len() as u64 * r; + let mut checked: Vec> = vec![Vec::new(); seeds.len()]; let mut found: Vec = vec![(None, 0, 0, None); seeds.len()]; let mut at = 0u64; while at < total { @@ -373,9 +460,32 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 return Err(refused(format!("it returned {} chains for {}", outcomes.len(), chains.len()))); } let reading = m.meter(); + if audited { + let mut these: Vec = ((at / r)..=((end - 1) / r)) + .map(|s| { + let (from, to) = ((s * r).max(at), ((s + 1) * r).min(end)); + from + audit_draw(job, seeds[s as usize], 1) % (to - from) + }) + .collect(); + these.push(end - 1); + these.sort_unstable(); + these.dedup(); + for &k in &these { + checked[(k / r) as usize].push(k % r); + if let Err(e) = member::check_chain(&outcomes[(k - at) as usize], w, temperatures) { + return Err(demote(c, KERNEL_SAMPLE, format!("gave a chain the host's checks refuse ({e})"))); + } + } + let places: Vec = these.iter().map(|&k| (k - at) as usize).collect(); + if let Some(i) = differs(w, temperatures, &chains, &outcomes, &places) { + return Err(demote(c, KERNEL_SAMPLE, format!("gave chain {} of a call of {} other bits than the reference's", i + 1, chains.len()))); + } + } let one_run = alone || (at / r == (end - 1) / r && at.is_multiple_of(r) && end.is_multiple_of(r)); for (k, o) in (at..end).zip(outcomes) { - member::check_chain(&o, w, temperatures).map_err(refused)?; + if let Err(e) = member::check_chain(&o, w, temperatures) { + return Err(if audited { demote(c, KERNEL_SAMPLE, format!("gave a chain the host's checks refuse ({e})")) } else { refused(e) }); + } let f = &mut found[(k / r) as usize]; f.1 += o.accumulates; f.2 += o.draws; @@ -389,6 +499,11 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 } at = end; } + if audited { + for (s, ks) in checked.iter().enumerate() { + c.log_audit(json!({ "seed": seeds[s].to_string(), "chains": ks })); + } + } Ok(found .into_iter() .map(|(best, accumulates, draws, metered)| { @@ -399,6 +514,154 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 .collect()) } +/// The threads the audit's re-runs may take beside the jobs' own, over the whole +/// process at once: a quarter of the host's cores, at most 4 (§7.3); none on a host +/// of fewer than 4. Each runs at a low priority ([`lower_this_thread`]). The job's +/// own thread runs its share of the re-runs as well, at the job's priority, so an +/// audited job takes at most its own thread and these. +pub fn audit_threads() -> usize { + (std::thread::available_parallelism().map_or(1, |p| p.get()) / 4).min(4) +} + +static AUDIT_THREADS_TAKEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + +/// As many of the audit's threads as are free, up to `want`; given back by the caller. +fn take_audit_threads(want: usize) -> usize { + use std::sync::atomic::Ordering; + let most = audit_threads(); + let mut taken = AUDIT_THREADS_TAKEN.load(Ordering::Relaxed); + loop { + let k = want.min(most.saturating_sub(taken)); + if k == 0 { + return 0; + } + match AUDIT_THREADS_TAKEN.compare_exchange_weak(taken, taken + k, Ordering::AcqRel, Ordering::Relaxed) { + Ok(_) => return k, + Err(now) => taken = now, + } + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +unsafe extern "C" { + fn getpriority(which: i32, who: u32) -> i32; + fn setpriority(which: i32, who: u32, prio: i32) -> i32; +} + +/// This thread at a low priority: on Linux, where a nice value is a thread's, 10 +/// more, to 19 at most; on macOS, the background band; elsewhere as it is. Its +/// priority before and after, as `getpriority` reads it for this thread (the nice +/// value on Linux; on macOS nonzero in the background band), or the error +/// `setpriority` gave. +fn lower_this_thread() -> std::io::Result<(i32, i32)> { + #[cfg(target_os = "linux")] + // SAFETY: getpriority and setpriority take plain values; `PRIO_PROCESS` (0) and + // `who` 0 name the calling thread on Linux. + unsafe { + let before = getpriority(0, 0); + if setpriority(0, 0, (before + 10).min(19)) != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok((before, getpriority(0, 0))) + } + #[cfg(target_os = "macos")] + // SAFETY: plain values; `PRIO_DARWIN_THREAD` (3) with `who` 0 names the calling + // thread, and `PRIO_DARWIN_BG` (0x1000) puts it in the background band. + unsafe { + let before = getpriority(3, 0); + if setpriority(3, 0, 0x1000) != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok((before, getpriority(3, 0))) + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + Ok((0, 0)) +} + +/// An audit thread that could not be lowered says so, once a process, and runs its +/// share at the job's priority. +fn lowered_or_said(r: &std::io::Result<(i32, i32)>) { + static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if let Err(e) = r + && !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) + { + eprintln!("wai-quantum landscape: an audit thread's priority could not be lowered ({e}), so the audit's threads run at the job's priority"); + } +} + +/// What the audit's threads did, for the test that holds their number and priority. +#[cfg(test)] +mod census { + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + pub static LIVE: AtomicUsize = AtomicUsize::new(0); + pub static MOST: AtomicUsize = AtomicUsize::new(0); + pub static LOWERED: Mutex>> = Mutex::new(Vec::new()); + + /// One audit thread running, until dropped. + pub struct Running; + + pub fn enter(lowered: &std::io::Result<(i32, i32)>) -> Running { + let now = LIVE.fetch_add(1, Ordering::AcqRel) + 1; + MOST.fetch_max(now, Ordering::AcqRel); + if let Ok(mut l) = LOWERED.lock() { + l.push(lowered.as_ref().map(|p| *p).map_err(|e| e.to_string())); + } + Running + } + + impl Drop for Running { + fn drop(&mut self) { + LIVE.fetch_sub(1, Ordering::AcqRel); + } + } +} + +/// The first of a call's chains at `places` whose outcome is not the +/// reference's, each run again on the reference: on this thread, at its own +/// priority, and on as many of the audit's threads as are free +/// ([`audit_threads`]), each at a low priority. +fn differs(w: &Workload, temperatures: &[f64], chains: &[Chain], outcomes: &[Outcome], places: &[usize]) -> Option { + let wrong = |part: &[usize]| part.iter().copied().find(|&i| outcomes[i] != sample::anneal_with(w, &chains[i].start, temperatures, chains[i].seed)); + // The threads taken are given back however the re-runs end. + struct Taken(usize); + impl Drop for Taken { + fn drop(&mut self) { + AUDIT_THREADS_TAKEN.fetch_sub(self.0, std::sync::atomic::Ordering::AcqRel); + } + } + let extra = Taken(take_audit_threads(places.len().saturating_sub(1))); + let parts: Vec<&[usize]> = places.chunks(places.len().div_ceil(extra.0 + 1).max(1)).collect(); + std::thread::scope(|s| { + let (first, rest) = parts.split_first()?; + let others: Vec<_> = rest + .iter() + .map(|&part| { + let h = std::thread::Builder::new().name("wai-audit".into()).spawn_scoped(s, move || { + let lowered = lower_this_thread(); + lowered_or_said(&lowered); + #[cfg(test)] + let _running = census::enter(&lowered); + wrong(part) + }); + (part, h) + }) + .collect(); + let mut found = wrong(first); + for (part, h) in others { + let r = match h { + Ok(h) => h.join().unwrap_or_else(|p| std::panic::resume_unwind(p)), + Err(_) => wrong(part), + }; + found = match (found, r) { + (Some(a), Some(b)) => Some(a.min(b)), + (a, b) => a.or(b), + }; + } + found + }) +} + /// Every seed's run of a plan, the starts of several seeds handed to the member /// together. A settle has no randomness: it runs once, and every seed has its answer. pub(crate) fn execute_seeds(job: &Job, plan: Plan, seeds: &[u64], c: &Certified<'_>) -> Result, Refusal> { @@ -535,7 +798,6 @@ pub fn run_on(c: &Certified<'_>, job: &Job, seed: u64, temperature: Option) /// The answer of `extensions/quantum-landscape.md` §3, from what a run's kernels left. pub(crate) fn answer_of(c: &Certified<'_>, job: &Job, plan: Plan, seed: u64, found: &Found) -> Value { let (p, exact) = (&job.program, &job.exact); - let m = c.member(); let n = p.landscape.len(); let reached = exact.truth.as_ref().map(|t| found.energy as f64 == t.lowest); let (declared, declared_reached, declared_energy) = match &p.declared { @@ -569,8 +831,8 @@ pub(crate) fn answer_of(c: &Certified<'_>, job: &Job, plan: Plan, seed: u64, fou }; // The label claims a reading only for a run that has one, and says what it covers. let label = match &found.metered { - Some(r) => format!("{} Beside the priced figures is a reading of {}.", m.label(), r.scope.as_deref().unwrap_or("this run alone")), - None => m.label(), + Some(r) => format!("{} Beside the priced figures is a reading of {}.", c.label(), r.scope.as_deref().unwrap_or("this run alone")), + None => c.label(), }; json!({ "protocol": PROTOCOL, @@ -644,7 +906,6 @@ pub fn sweep_plans(p: &Program, temperatures: &[f64], seeds: u32) -> Result, job: &Job, temperatures: &[f64], seeds: u32, first_seed: u64) -> Result { let (p, exact) = (&job.program, &job.exact); - let m = c.member(); let plans = sweep_plans(p, temperatures, seeds)?; let default = p.landscape.default_temperature(); let mut rows = Vec::with_capacity(plans.len()); @@ -685,7 +946,7 @@ pub fn sweep_on(c: &Certified<'_>, job: &Job, temperatures: &[f64], seeds: u32, } Ok(json!({ "protocol": PROTOCOL, - "label": m.label(), + "label": c.label(), "member": member::describe_all(c, &kernels), "lowest": exact.truth.as_ref().map(|t| t.lowest), "default_temperature": default, @@ -758,7 +1019,7 @@ pub fn compare_on(c: &Certified<'_>, job: &Job, seeds: u32, first_seed: u64, tem .collect(); Ok(json!({ "protocol": PROTOCOL, - "label": c.member().label(), + "label": c.label(), "member": member::describe_all(c, &[member::KERNEL_SAMPLE]), "target": target, "target_is_exact": exact_target, @@ -999,4 +1260,64 @@ mod tests { assert_eq!(c["target_is_exact"], true); assert_eq!(rows[2]["classical"], true); } + + /// The audit's threads (§7.3): a quarter of the host's cores, 4 at most, over the + /// whole process at once however many audited calls run together; each lowered, + /// `setpriority`'s result checked and the priority read back; every one given back. + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn the_audit_s_threads_are_capped_over_the_process_and_run_low() { + use std::sync::atomic::Ordering; + let cores = std::thread::available_parallelism().map_or(1, |p| p.get()); + let cap = (cores / 4).min(4); + assert_eq!(audit_threads(), cap, "a quarter of {cores} cores, 4 at most"); + let lowered_well = |(before, after): (i32, i32)| if cfg!(target_os = "linux") { after == (before + 10).min(19) } else { after != 0 }; + // A thread lowered on its own: setpriority succeeded, and the priority reads lowered. + let alone = std::thread::spawn(lower_this_thread).join().unwrap(); + println!("PROBE audit threads: {cores} cores, a cap of {cap}; a thread lowered alone: {alone:?}"); + let alone = alone.expect("setpriority lowers a thread"); + assert!(lowered_well(alone), "a thread lowered alone reads {alone:?}"); + // Eight audited calls at once, each running 24 chains again. + let mut l = crate::quantum_landscape::energy::Landscape::new(64); + for i in 0..64 { + l.couple(i, (i + 1) % 64, if i % 2 == 0 { 1 } else { -1 }); + } + let w = l.lower(); + let temperatures = vec![1.0; 300]; + let chains: Vec = (0..24) + .map(|k| { + let s = sample::chain_seed(7, k); + Chain { start: sample::start(s, 64), seed: s } + }) + .collect(); + let outcomes: Vec = chains.iter().map(|c| sample::anneal_with(&w, &c.start, &temperatures, c.seed)).collect(); + let places: Vec = (0..chains.len()).collect(); + std::thread::scope(|s| { + for _ in 0..8 { + s.spawn(|| { + for _ in 0..3 { + assert_eq!(differs(&w, &temperatures, &chains, &outcomes, &places), None); + } + }); + } + }); + let most = census::MOST.load(Ordering::Acquire); + let lowered = census::LOWERED.lock().unwrap().clone(); + println!("PROBE audit threads: at most {most} at once; {} lowered, the first {:?}", lowered.len(), lowered.first()); + assert!(most <= cap, "{most} of the audit's threads at once, past {cap}"); + if cap > 0 { + assert!(most >= 1 && !lowered.is_empty(), "the audit's threads ran"); + for r in &lowered { + let r = r.clone().unwrap_or_else(|e| panic!("an audit thread was not lowered: {e}")); + assert!(lowered_well(r), "an audit thread reads {r:?}"); + } + } + // Every thread taken is given back: none is left taken once the audits end + // (another test's audit may hold some a while). + let until = std::time::Instant::now() + std::time::Duration::from_secs(120); + while AUDIT_THREADS_TAKEN.load(Ordering::Acquire) != 0 { + assert!(std::time::Instant::now() < until, "{} of the audit's threads never given back", AUDIT_THREADS_TAKEN.load(Ordering::Acquire)); + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } } diff --git a/wai/wai-quantum/src/quantum_landscape/sample.rs b/wai/wai-quantum/src/quantum_landscape/sample.rs index 3990b42e3..7d66c6908 100644 --- a/wai/wai-quantum/src/quantum_landscape/sample.rs +++ b/wai/wai-quantum/src/quantum_landscape/sample.rs @@ -166,10 +166,18 @@ pub fn anneal_with(w: &Workload, start: &[i8], temperatures: &[f64], chain: u64) let mut s = start.to_vec(); let (mut accumulates, mut draws) = (0u64, 0u64); let mut table = Table::new(w.field_bound()); + // A sweep at 0 from a state that rests leaves it where it is: a run of them + // after a fixed point is skipped, its work counted all the same. + let mut rests = false; for (k, &t) in temperatures.iter().enumerate() { if t <= 0.0 { - s = w.sweep(&s); + if !rests { + let next = w.sweep(&s); + rests = next == s; + s = next; + } } else { + rests = false; let beta = 1.0 / t; table.stamp += 1; let base = (k as u64).wrapping_mul(n as u64); diff --git a/wai/wai-quantum/src/quantum_landscape/webgpu.rs b/wai/wai-quantum/src/quantum_landscape/webgpu.rs index 6bce8eeea..c60c54dd8 100644 --- a/wai/wai-quantum/src/quantum_landscape/webgpu.rs +++ b/wai/wai-quantum/src/quantum_landscape/webgpu.rs @@ -17,15 +17,17 @@ //! class are coupled, but the order is not the reference's, so its parity //! can only be statistical, and it has to pass the battery to get that. //! -//! A workload whose field bound does not fit a 32-bit lane is declined before -//! anything runs, with the reason ([`Member::takes`]): the host then runs it on -//! the next member it was offered, and the answer says so. A cost above every -//! field is held at the lane's top, where it does what it does at its own size. +//! It declares its capacity ([`Member::capacity`]): a field bound of at most +//! 2³¹ − 1, which its 32-bit lanes hold, on both sides of temperature 0. The +//! host runs it on no workload past that, gives such a job to the next member +//! it was offered, and the answer says so. A cost above every field is held at +//! the lane's top, where it does what it does at its own size. //! //! The member says nothing of its own parity: the host certifies it //! ([`super::member::certify`]). It is native only: a page's WebGPU is //! asynchronous, and the member's calls block. +use super::capacity::{Capacity, Limits}; use super::device::{self, MeterSlot, Reach, SETTLE_BATCH}; use super::member::{Chain, Member, Metered}; use super::settle::{Outcome, Term, Workload}; @@ -389,25 +391,27 @@ impl WebGpuLandscape { words(&s) } - /// Whether the workload fits the kernels: every field in a 32-bit lane, - /// the workload within a binding, and, for sampling, a table of one sweep's - /// cut points within [`device::MAX_TABLE_ENTRIES`]. Only the first can - /// decline a workload a job gives (`extensions/quantum-landscape.md` §2.0). - fn fits(&self, w: &Workload, sampling: bool) -> Result<(), String> { - let bound = w.field_bound(); - if bound > i64::from(i32::MAX) { - return Err(format!("its field bound is {bound}, and this member's 32-bit lanes hold at most {}", i32::MAX)); + /// What its kernels hold: every field in a 32-bit lane, so a field bound + /// of at most 2³¹ − 1 on both sides of temperature 0; cells and couplings + /// that fit a storage binding (each at most a 32nd of it, which leaves the + /// workload's words within one); and one sweep's cut points within + /// [`device::MAX_TABLE_ENTRIES`]. Only the first is reached by a job + /// (`extensions/quantum-landscape.md` §2.0). + fn declared(&self) -> Capacity { + let lanes = Limits { field_bound: Some(i64::from(i32::MAX)), ..Limits::NONE }; + Capacity { + cells: Some(self.max_binding / 32), + couplings: Some(self.max_binding / 32), + at_zero: lanes, + above_zero: lanes, + table_entries: Some(device::MAX_TABLE_ENTRIES), + ..Capacity::NONE } - let n = w.n() as u64; - if 3 * n * 4 > self.max_binding || (w.nonzeros() * 2 + 3 * n + 1) * 4 > self.max_binding { - return Err("the workload is larger than a storage binding on this device".into()); - } - if sampling { device::table_fits(&device::reach(w)) } else { Ok(()) } } fn settle_on_device(&self, w: &Workload, start: &[i8], bound: u32) -> Result { self.settled()?; - self.fits(w, false)?; + self.declared().admits(w, &[])?; let n = w.n(); let (g, at) = graph(w, &device::reach(w)); let graph = self.buffer("graph", &words(&g), wgpu::BufferUsages::STORAGE); @@ -445,7 +449,7 @@ impl WebGpuLandscape { fn chains_on_device(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { let sampling = temperatures.iter().any(|&t| t > 0.0); self.settled()?; - self.fits(w, sampling)?; + self.declared().admits(w, temperatures)?; let n = w.n(); let count = chains.len(); if count == 0 { @@ -570,8 +574,8 @@ impl Member for WebGpuLandscape { device::label(self.class, &self.adapter, &format!("WebGPU ({})", self.backend), order) } - fn takes(&self, w: &Workload, sampling: bool) -> Result<(), String> { - self.fits(w, sampling) + fn capacity(&self) -> Capacity { + self.declared() } fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { diff --git a/wai/wai-quantum/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index 738a63b38..5b704916a 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -1,11 +1,14 @@ //! The conformance set in `quantum-conformance/landscape/vectors.json`, held to //! the code and read as a member's checker reads it: from the file alone. -use serde_json::Value; -use wai_quantum::quantum_landscape::conformance::{chi_square, run_battery, vectors}; +use serde_json::{Value, json}; +use std::cell::Cell; +use wai_quantum::quantum_landscape::capacity::{Capacity, Limits, floor}; +use wai_quantum::quantum_landscape::conformance::{ChainCase, Temperatures, chain_cases, chain_vectors_hold, chi_square, run_battery, settle_vectors_hold, vectors}; use wai_quantum::quantum_landscape::energy::Landscape; use wai_quantum::quantum_landscape::job; -use wai_quantum::quantum_landscape::member::{Chain, CpuReference, Member, Parity, certify, KERNEL_SAMPLE, KERNEL_SETTLE}; +use wai_quantum::quantum_landscape::member::{Certified, Chain, CpuReference, Member, Parity, certify, KERNEL_SAMPLE, KERNEL_SETTLE}; +use wai_quantum::quantum_landscape::{CHUNK_CELLS, CHUNK_CHAINS, MAX_JOB_BYTES}; use wai_quantum::quantum_landscape::reach; use wai_quantum::quantum_landscape::run::state_text; use wai_quantum::quantum_landscape::sample::{anneal_with, cuts, out, pick, schedule}; @@ -270,6 +273,12 @@ impl Member for Mutant { fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { Ok(chains.iter().map(|c| self.chain(w, c, temperatures)).collect()) } + /// 32 cells and no other limit: the battery it is held to is the whole fixed + /// one (§7.2's statements about it) and the alike cells of 32, not 4,096 cells a + /// mutant would take minutes over. + fn capacity(&self) -> Capacity { + Capacity { cells: Some(32), ..Capacity::NONE } + } } impl Mutant { @@ -299,6 +308,7 @@ impl Mutant { }; let groups = colour.iter().copied().max().map_or(0, |m| m + 1); let last = temperatures.len().saturating_sub(1); + let mut rests = false; for (k, &t) in temperatures.iter().enumerate() { let t = match self { Mutant::IgnoresSchedule => temperatures[0], @@ -307,9 +317,14 @@ impl Mutant { _ => t, }; if t <= 0.0 { - s = w.sweep(&s); + if !rests { + let next = w.sweep(&s); + rests = next == s; + s = next; + } continue; } + rests = false; for g in 0..groups { let old = s.clone(); for i in (0..n).filter(|&i| colour[i] == g) { @@ -379,12 +394,15 @@ enum Fault { Fields16, /// Fields held in 32 bits, and nothing declined. Fields32, - /// Fields held in 32 bits, and a check of `|b_i|` alone that forgets the couplings. + /// Fields held in 32 bits, and a capacity declared on `|b_i|` alone, forgetting the couplings. Fields32CheckedWithoutCouplings, /// Each chain's cells last to first. CellsBackwards, /// The draw counter starting again at each window of this many sweeps. CounterRestartsEachWindow(usize), + /// The draw counter starting again at each window a device cuts the + /// workload's schedule into ([`reach::window`]), whatever the workload. + DeviceWindowRestart, /// The sweep index stopping after this many sweeps. StopsAfterOneWindow(usize), /// Only this many chains run. @@ -393,8 +411,76 @@ enum Fault { OnlyTheFirstCells(usize), /// A settle that stops looking for its end after this many sweeps. SettleEndsLate(u32), - /// Declines everything wider than this. - DeclinesAbove(i64), + /// In a last, partial workgroup of 64 cells holding more than 32, lanes 32 on do not move. + TileRemainder, + /// Each row read to its first so many couplings only. + RowCap(usize), + /// In a call whose last warp of 32 holds more than 16 chains, lanes 16 on return their starts. + WarpRemainder, + /// The draw index held in 24 bits. + Counter24, + /// At temperature 0, a field and the cost compared in wrapping 32-bit lanes: `h − c > 0`, `h + c < 0`. + FieldCostWrap, + /// A sweep at 0 in a schedule with a sweep above 0 compares in 16-bit lanes. + MixedFinal16, + /// A sweep at 0 in a schedule with a sweep above 0 compares `h − c` and `h + c` in wrapping 32-bit lanes. + MixedFieldCostWrap, + /// The chains kernel's sweeps at 0 leave lanes 32 on of a partial last workgroup of 64 unmoved. + ColdTileRemainder, + /// The first chain of every call of two or more is another seed's chain. + FirstOfCallWrong, + /// A settle with a bound past 65,536 never reports a fixed point: it says it ran to the bound. + RestBlind, + /// A coupling sum held in 8 bits. + CouplingSum8, + /// Cells from this one on never drawn. + SampleOnlyTheFirstCells(usize), + /// Cell 0 flipped above 0 where a sweep's table holds more than this many entries. + TableUpTo(u64), + /// Cell 0 flipped where the uncertain fields at the hottest sweep span more than this. + SpanUpTo(u64), + /// The chains kernel's sweep at 0 straight after a sweep above 0 (an engine's + /// final sweep) leaves lanes 32 on of a partial last workgroup of 64 unmoved. + Final64, + /// The same in a partial last warp of 32 holding more than 16: lanes 16 on. + Final32, + /// A settle that keeps its last three states in a ring and reads the one at a + /// 9-bit sweep count, `(sweeps & 511) % 3`, where it stops at its bound. + Ring9, + /// The same at a 12-bit sweep count, `(sweeps & 4095) % 3`. + Ring12, + /// [`Fault::Final64`] only where the partial group holds an even count. + Final64Even, + /// [`Fault::Final64`] only where the partial group holds 34 to 62. + Final64Mid, + /// [`Fault::Final64`] only where the sweep before the sweep at 0 was below 1. + Final64AfterCold, + /// [`Fault::Final64`] only after three or more sweeps above 0. + Final64AfterThreeHot, + /// [`Fault::Final64`] only where the sweep at 0 falls at an odd index. + Final64OddIndex, + /// [`Fault::Final32`] only where the partial warp holds an even count. + Final32Even, + /// A settle stopped at its bound past this many sweeps answers the state one sweep back. + Stale(u32), + /// A settle that would run past this many sweeps stops moving there and says it ran to its bound. + Truncate(u32), + /// The same at a 13-bit sweep count, `(sweeps & 8191) % 3`. + Ring13, + /// A settle that keeps its last three states in a ring and reads the one at a + /// sweep count of these bits, `(sweeps & mask) % 3`, where it ends in a two-cycle. + CycleRing(u32), + /// A settle that ends in a two-cycle past this many sweeps answers the state one sweep back. + CycleStale(u32), + /// A settle that stops looking for a two-cycle after this many sweeps. + MissCycleAfter(u32), + /// A settle run in batches of 64 sweeps that keeps no state two sweeps back from + /// the batch before: no two-cycle is seen at the first sweep of a later batch. + BatchCycle, + /// No fault: it declares field bounds to this, on both sides of temperature 0. + DeclaresFieldBound(i64), + /// Declares field bounds to 2³¹ − 1 and refuses, at run time, any above this. + RefusesAbove(i64), } fn mul32(a: u32, b: u32) -> (u32, u32) { @@ -421,9 +507,25 @@ fn out_no_carry(s: u64, j: u64) -> u64 { x ^ (x >> 31) } +/// Whether cell `i` of `n` lies in the lanes a partial last workgroup of 64 leaves unmoved. +fn in_the_tile_gap(n: usize, i: usize) -> bool { + let (full, rest) = (n - n % 64, n % 64); + rest > 32 && i >= full + 32 +} + +/// Whether cell `i` of `n` lies in the lanes a partial last warp of 32 leaves unmoved. +fn in_the_warp_gap(n: usize, i: usize) -> bool { + let (full, rest) = (n - n % 32, n % 32); + rest > 16 && i >= full + 16 +} + impl Fault { fn field(self, w: &Workload, s: &[i8], i: usize) -> i64 { - let h = w.field(s, i); + let h = match self { + Fault::RowCap(most) => w.field(&vec![0; w.n()], i) + w.row(i).take(most).map(|(j, c)| c * i64::from(s[j])).sum::(), + Fault::CouplingSum8 => w.field(&vec![0; w.n()], i) + i64::from(w.row(i).map(|(j, c)| c * i64::from(s[j])).sum::() as i8), + _ => w.field(s, i), + }; match self { Fault::Fields16 => i64::from(h as i16), Fault::Fields32 | Fault::Fields32CheckedWithoutCouplings => i64::from(h as i32), @@ -432,28 +534,88 @@ impl Fault { } fn sweep(self, w: &Workload, s: &[i8]) -> Vec { + self.sweep_in(w, s, false, false) + } + + /// A sweep at 0; `mixed` where the schedule has a sweep above 0 as well, and + /// `mixed_or_chain` where it is the chains kernel's. + fn sweep_in(self, w: &Workload, s: &[i8], mixed: bool, mixed_or_chain: bool) -> Vec { let c = w.cost(); - (0..w.n()).map(|i| { + let n = w.n(); + (0..n).map(|i| { + if (self == Fault::TileRemainder || (self == Fault::ColdTileRemainder && mixed_or_chain)) && in_the_tile_gap(n, i) { + return s[i]; + } let h = self.field(w, s, i); - if h > c { 1 } else if h < -c { -1 } else { 0 } + match self { + Fault::MixedFieldCostWrap if mixed => { + let (h, c) = (h as i32, c as i32); + if h.wrapping_sub(c) > 0 { 1 } else if h.wrapping_add(c) < 0 { -1 } else { 0 } + } + Fault::FieldCostWrap => { + let (h, c) = (h as i32, c as i32); + if h.wrapping_sub(c) > 0 { 1 } else if h.wrapping_add(c) < 0 { -1 } else { 0 } + } + Fault::MixedFinal16 if mixed => { + let (h, c) = (i64::from(h as i16), i64::from(c as i16)); + if h > c { 1 } else if h < -c { -1 } else { 0 } + } + _ => if h > c { 1 } else if h < -c { -1 } else { 0 }, + } }).collect() } fn chain(self, w: &Workload, c: &Chain, temperatures: &[f64]) -> Outcome { let n = w.n(); let mut s = c.start.clone(); + let mixed = temperatures.iter().any(|&t| t > 0.0); + // A sweep at 0 from a state that rests leaves it there: a run of them is skipped. + let mut rests = false; for (k, &t) in temperatures.iter().enumerate() { if matches!(self, Fault::StopsAfterOneWindow(at) if k >= at) { break; } if t <= 0.0 { - s = self.sweep(w, &s); + if !rests { + let mut next = self.sweep_in(w, &s, mixed, true); + if k > 0 && temperatures[k - 1] > 0.0 { + let hot_before = temperatures[..k].iter().filter(|&&t| t > 0.0).count(); + let fires64 = match self { + Fault::Final64 => true, + Fault::Final64Even => (n % 64).is_multiple_of(2), + Fault::Final64Mid => (34..=62).contains(&(n % 64)), + Fault::Final64AfterCold => temperatures[k - 1] < 1.0, + Fault::Final64AfterThreeHot => hot_before >= 3, + Fault::Final64OddIndex => k % 2 == 1, + _ => false, + }; + let fires32 = self == Fault::Final32 || (self == Fault::Final32Even && (n % 32).is_multiple_of(2)); + for i in 0..n { + if (fires64 && in_the_tile_gap(n, i)) || (fires32 && in_the_warp_gap(n, i)) { + next[i] = s[i]; + } + } + } + rests = next == s; + s = next; + } continue; } + rests = false; let cells: Vec = if self == Fault::CellsBackwards { (0..n).rev().collect() } else { (0..n).collect() }; for i in cells { - let counter = if let Fault::CounterRestartsEachWindow(at) = self { k % at } else { k }; - let j = (counter * n + i + 1) as u64; + if (self == Fault::TileRemainder && in_the_tile_gap(n, i)) || matches!(self, Fault::SampleOnlyTheFirstCells(at) if i >= at) { + continue; + } + let counter = match self { + Fault::CounterRestartsEachWindow(at) => k % at, + Fault::DeviceWindowRestart => k % reach::window(w, &reach::reach(w), true), + _ => k, + }; + let j = match self { + Fault::Counter24 => ((counter * n + i + 1) as u64) & 0xFF_FFFF, + _ => (counter * n + i + 1) as u64, + }; let u = if self == Fault::DroppedCarry { out_no_carry(c.seed, j) } else { out(c.seed, j) } >> 11; let (a, b) = cuts(self.field(w, &s, i), w.cost(), 1.0 / t); s[i] = if self == Fault::HighWordCompare { @@ -463,6 +625,15 @@ impl Fault { }; } } + let hottest = temperatures.iter().copied().fold(0.0, f64::max); + let past = match self { + Fault::TableUpTo(most) => mixed && reach::reach(w).entries > most, + Fault::SpanUpTo(most) => mixed && wai_quantum::quantum_landscape::capacity::uncertain_span(w, hottest) > most, + _ => false, + }; + if past { + s[0] = if s[0] == 0 { 1 } else { -s[0] }; + } let draws = (n as u64) * temperatures.iter().filter(|&&t| t > 0.0).count() as u64; let term = if w.rests(&s) { Term::FixedPoint } else { Term::Bound }; Outcome { state: s, sweeps: temperatures.len() as u32, term, accumulates: w.nonzeros() * temperatures.len() as u64, draws } @@ -480,10 +651,45 @@ impl Member for Fault { "A kernel under test.".into() } fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + if let Fault::RefusesAbove(most) = *self + && w.field_bound() > most + { + return Err("not this one".into()); + } + if *self == Fault::RestBlind && bound > 65_536 { + let o = w.settle(start, bound); + return Ok(Outcome { term: Term::Bound, sweeps: bound, accumulates: w.nonzeros() * u64::from(bound), ..o }); + } if let Fault::SettleEndsLate(at) = *self { let o = w.settle(start, bound); return Ok(if o.sweeps > at { Outcome { sweeps: o.sweeps - 1, accumulates: o.accumulates - w.nonzeros(), ..o } } else { o }); } + if matches!(self, Fault::Ring9 | Fault::Ring12 | Fault::Ring13 | Fault::Stale(_)) { + let o = w.settle(start, bound); + let d = o.sweeps; + let back = match *self { + Fault::Ring9 => (d % 3 + 3 - (d & 511) % 3) % 3, + Fault::Ring12 => (d % 3 + 3 - (d & 4095) % 3) % 3, + Fault::Ring13 => (d % 3 + 3 - (d & 8191) % 3) % 3, + Fault::Stale(at) => u32::from(d > at), + _ => 0, + }; + return Ok(if o.term == Term::Bound && back > 0 { Outcome { state: w.settle(start, d - back).state, ..o } } else { o }); + } + if matches!(self, Fault::CycleRing(_) | Fault::CycleStale(_)) { + let o = w.settle(start, bound); + let d = o.sweeps; + let back = match *self { + Fault::CycleRing(mask) => (d % 3 + 3 - (d & mask) % 3) % 3, + Fault::CycleStale(at) => u32::from(d > at), + _ => 0, + }; + return Ok(if o.term == Term::TwoCycle && back > 0 { Outcome { state: w.settle(start, d - back).state, ..o } } else { o }); + } + if let Fault::Truncate(at) = *self { + let o = w.settle(start, bound); + return Ok(if o.sweeps > at { Outcome { state: w.settle(start, at).state, sweeps: bound, term: Term::Bound, accumulates: w.nonzeros() * u64::from(bound), draws: 0 } } else { o }); + } let mut s = start.to_vec(); let mut back: Option> = None; for k in 1..=bound { @@ -491,7 +697,12 @@ impl Member for Fault { if let Fault::OnlyTheFirstCells(at) = *self { next[at.min(s.len())..].copy_from_slice(&s[at.min(s.len())..]); } - let term = if next == s { Some(Term::FixedPoint) } else if back.as_ref() == Some(&next) { Some(Term::TwoCycle) } else if k == bound { Some(Term::Bound) } else { None }; + let sees_a_cycle = match *self { + Fault::MissCycleAfter(at) => k <= at, + Fault::BatchCycle => !(k > 64 && k % 64 == 1), + _ => true, + }; + let term = if next == s { Some(Term::FixedPoint) } else if sees_a_cycle && back.as_ref() == Some(&next) { Some(Term::TwoCycle) } else if k == bound { Some(Term::Bound) } else { None }; back = Some(std::mem::replace(&mut s, next)); if let Some(term) = term { return Ok(Outcome { state: s, sweeps: k, term, accumulates: w.nonzeros() * u64::from(k), draws: 0 }); @@ -500,7 +711,19 @@ impl Member for Fault { Err("no sweeps".into()) } fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + if let Fault::RefusesAbove(most) = *self + && w.field_bound() > most + { + return Err("not this one".into()); + } + let warp_start = chains.len() - (chains.len() - 1) % 32 - 1; + let partial = (17..32).contains(&(chains.len() % 32)); Ok(chains.iter().enumerate().map(|(k, c)| match *self { + Fault::FirstOfCallWrong if k == 0 && chains.len() >= 2 => anneal_with(w, &c.start, temperatures, c.seed.wrapping_add(1)), + Fault::WarpRemainder if partial && k >= warp_start + 16 => { + let term = if w.rests(&c.start) { Term::FixedPoint } else { Term::Bound }; + Outcome { state: c.start.clone(), sweeps: temperatures.len() as u32, term, accumulates: w.nonzeros() * temperatures.len() as u64, draws: (w.n() as u64) * temperatures.iter().filter(|&&t| t > 0.0).count() as u64 } + } Fault::OnlyTheFirstChains(at) if k >= at => { let term = if w.rests(&c.start) { Term::FixedPoint } else { Term::Bound }; Outcome { state: c.start.clone(), sweeps: temperatures.len() as u32, term, accumulates: w.nonzeros() * temperatures.len() as u64, draws: (w.n() as u64) * temperatures.iter().filter(|&&t| t > 0.0).count() as u64 } @@ -508,65 +731,323 @@ impl Member for Fault { f => f.chain(w, c, temperatures), }).collect()) } - fn takes(&self, w: &Workload, _sampling: bool) -> Result<(), String> { - let zero = vec![0i8; w.n()]; + fn capacity(&self) -> Capacity { + let lanes = |l: Limits| Capacity { at_zero: l, above_zero: l, ..Capacity::NONE }; match *self { - Fault::DeclinesAbove(most) if w.field_bound() > most => Err(format!("its field bound is {}, above {most}", w.field_bound())), - Fault::Fields32CheckedWithoutCouplings if (0..w.n()).any(|i| w.field(&zero, i).abs() > i64::from(i32::MAX)) => Err("its fields are beyond 32 bits".into()), - _ => Ok(()), + Fault::DeclaresFieldBound(most) => lanes(Limits { field_bound: Some(most), ..Limits::NONE }), + Fault::RefusesAbove(_) => lanes(Limits { field_bound: Some(i64::from(i32::MAX)), ..Limits::NONE }), + Fault::Fields32CheckedWithoutCouplings => lanes(Limits { field: Some(i64::from(i32::MAX)), ..Limits::NONE }), + _ => Capacity::NONE, } } } -/// With no fault the stand-in is the reference, so each fault below is the only difference. -#[test] -fn every_fault_loses_the_grade_of_the_kernel_it_is_in() { - let clean = Fault::DeclinesAbove(i64::MAX); - let c = certify(&clean).unwrap(); - assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact)); - let settle = [Fault::Fields16, Fault::Fields32, Fault::Fields32CheckedWithoutCouplings, Fault::OnlyTheFirstCells(64), Fault::OnlyTheFirstCells(256), Fault::SettleEndsLate(64), Fault::SettleEndsLate(256)]; - let sample = [ - Fault::DroppedCarry, - Fault::HighWordCompare, - Fault::Fields16, - Fault::Fields32, - Fault::Fields32CheckedWithoutCouplings, - Fault::CellsBackwards, - Fault::OnlyTheFirstChains(64), - ]; - for f in settle { - assert_ne!(certify(&f).unwrap().parity(KERNEL_SETTLE), Parity::BitExact, "{f:?}"); +/// A faulty kernel that declares a capacity of its choosing. +struct Declared(Fault, Capacity); + +impl Member for Declared { + fn id(&self) -> String { + self.0.id() } - // A window's faults, at every window a vector crosses on a device that takes its table from the host. - let mut windows = Vec::new(); - for c in wai_quantum::quantum_landscape::conformance::chain_cases() { + fn class(&self) -> &'static str { + self.0.class() + } + fn label(&self) -> String { + self.0.label() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + self.0.settle(w, start, bound) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + self.0.chains(w, chains, temperatures) + } + fn capacity(&self) -> Capacity { + self.1 + } +} + +/// A job a fault gets wrong: a settle from every cell at 0 for at most so many +/// sweeps, or a `chains` case. +enum Witness { + Settle(Landscape, u32), + Chains(ChainCase), +} + +impl Witness { + /// The most hostile capacity a member with the fault may declare and still be + /// certified that takes this job: the least that does, at or above the floor, + /// in calls of exactly as many chains as the job has. + fn hostile(&self) -> Capacity { + let (least, batch) = match self { + Witness::Settle(l, bound) => (Capacity::least(&l.lower(), &[], u64::from(*bound)), 1), + Witness::Chains(c) => { + let t = c.temperatures.each(); + (Capacity::least(&c.landscape.lower(), &t, t.len() as u64), c.restarts) + } + }; + Capacity { chains_per_call: Some(batch), ..least.join(&floor()) } + } + + /// The least capacity that takes the job, at or above the floor, with no batch. + fn least(&self) -> Capacity { + let c = match self { + Witness::Settle(l, bound) => Capacity::least(&l.lower(), &[], u64::from(*bound)), + Witness::Chains(c) => { + let t = c.temperatures.each(); + Capacity::least(&c.landscape.lower(), &t, t.len() as u64) + } + }; + c.join(&floor()) + } + + /// Whether a capacity takes the job. + fn within(&self, cap: &Capacity) -> bool { + match self { + Witness::Settle(l, bound) => cap.admits_settle(&l.lower(), *bound).is_ok(), + Witness::Chains(c) => cap.admits(&c.landscape.lower(), &c.temperatures.each()).is_ok(), + } + } + + /// Capacities a member with the fault might declare that take the job: the + /// most hostile ([`Witness::hostile`]), the same with its cells and its + /// couplings rounded up as hardware rounds them, with no cell count at all, + /// and the numbers the in-tree members declare. A fault that is not monotone + /// can hide from the least capacity's edges and not from the others'. + fn realistic(&self) -> Vec { + let up = |v: Option| v.map(|v| v.next_power_of_two().max(64)); + let least = self.least(); + let lanes = Limits { field_bound: Some(i64::from(i32::MAX)), ..Limits::NONE }; + let candidates = [ + self.hostile(), + Capacity { cells: up(least.cells), couplings: up(least.couplings), ..least }, + Capacity { cells: None, ..least }, + Capacity { cells: Some(64), above_zero: Limits { field: Some(524_287), field_bound: None, cost: Some(1_048_575) }, uncertain_span: Some(8192), ..Capacity::NONE }, + Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes, above_zero: lanes, table_entries: Some(1 << 22), ..Capacity::NONE }, + Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }, + ]; + candidates.into_iter().filter(|c| c.under(&floor()).is_empty() && self.within(c)).collect() + } + + /// Whether `m` gets the job wrong, run as the host runs it within `cap`. + fn wrong(&self, m: &dyn Member, cap: &Capacity) -> bool { + match self { + Witness::Settle(l, bound) => { + let w = l.lower(); + let start = vec![0i8; l.len()]; + assert_eq!(cap.admits_settle(&w, *bound), Ok(()), "the job is within the capacity"); + m.settle(&w, &start, *bound).ok() != Some(w.settle(&start, *bound)) + } + Witness::Chains(c) => { + let (w, t, chains) = (c.landscape.lower(), c.temperatures.each(), c.chains()); + assert_eq!(cap.admits(&w, &t), Ok(()), "the job is within the capacity"); + let batch = cap.batch((CHUNK_CELLS / w.n()).clamp(1, CHUNK_CHAINS)); + let mut got = Vec::new(); + for call in chains.chunks(batch) { + match m.chains(&w, call, &t) { + Ok(o) => got.extend(o), + Err(_) => return true, + } + } + got != chains.iter().map(|ch| anneal_with(&w, &ch.start, &t, ch.seed)).collect::>() + } + } + } +} + +fn chain_case(name: &str) -> ChainCase { + chain_cases().into_iter().find(|c| c.name == name).unwrap_or_else(|| panic!("a chains vector {name}")) +} + +fn case(name: &str, l: Landscape, temperatures: Temperatures, seed: u64, restarts: u64) -> ChainCase { + ChainCase { name: name.into(), landscape: l, temperatures, seed, restarts } +} + +/// `n` cells in a line, each agreeing with the next, a field of 3 on the first: +/// a settle from 0 is a wave down it, one cell a sweep. +fn path(n: usize) -> Landscape { + let mut l = Landscape::new(n); + for i in 0..n - 1 { + l.couple(i, i + 1, 1); + } + l.set_field(0, 3); + l +} + +fn small(n: usize, couplings: &[(usize, usize, i8)], fields: &[(usize, i64)], cost: i64) -> Landscape { + let mut l = Landscape::new(n); + for &(a, b, w) in couplings { + l.couple(a, b, w); + } + for &(i, b) in fields { + l.set_field(i, b); + } + l.set_cost(cost); + l +} + +/// Eight cells of both signs and a cost, the review's job for a window fault. +fn eight() -> Landscape { + small(8, &[(0, 1, 1), (1, 2, -1), (2, 3, 1), (3, 4, 1), (4, 5, -1), (5, 6, 1), (6, 7, 1)], &[(0, 1)], 1) +} + +/// `n` uncoupled cells at field 1: every one moves in a sweep. +fn ones(n: usize) -> Landscape { + let mut l = Landscape::new(n); + for i in 0..n { + l.set_field(i, 1); + } + l +} + +/// `n` uncoupled cells at fields of 1 and −1 in turn. +fn alternating(n: usize) -> Landscape { + let mut l = Landscape::new(n); + for i in 0..n { + l.set_field(i, if i % 2 == 0 { 1 } else { -1 }); + } + l +} + +/// `n` cells in opposed pairs, a cell and the next, no fields. +fn opposed_pairs(n: usize) -> Landscape { + let mut l = Landscape::new(n); + for i in (0..n - 1).step_by(2) { + l.couple(i, i + 1, -1); + } + l +} + +/// A cell coupled to `d` others held at their sign by fields of 2, the first +/// half at +1 and the rest at −1: its whole row gives it −1, any shorter part 0 or more. +fn row(d: usize) -> Landscape { + let mut l = Landscape::new(d + 1); + l.set_field(0, if d % 2 == 1 { 0 } else { -1 }); + for j in 1..=d { + l.couple(0, j, 1); + l.set_field(j, if j <= d / 2 { 2 } else { -2 }); + } + l +} + +/// A cell at field `b` coupled to `d` cells at field 0. +fn hub(d: usize, b: i64) -> Landscape { + let mut l = Landscape::new(d + 1); + l.set_field(0, b); + for j in 1..=d { + l.couple(0, j, 1); + } + l +} + +/// Two cells at fields `−f` and `f`, each coupled to the same `d` cells at +/// field 0: fields from `−f − d` to `f + d`. +fn two_hubs(f: i64, d: usize) -> Landscape { + let mut l = Landscape::new(d + 2); + l.set_field(0, -f).set_field(1, f); + for j in 0..d { + l.couple(0, 2 + j, 1).couple(1, 2 + j, 1); + } + l +} + +/// Every fault, the kernel it is in, and a job it gets wrong; and for each +/// window a vector crosses on a device that takes its table from the host, +/// the faults of that window and the vector. +fn faults() -> Vec<(Fault, &'static str, Witness)> { + let held = |runs: &[(f64, u32)]| Temperatures::Held(runs.to_vec()); + let lane = i64::from(i32::MAX); + let mut out = vec![ + (Fault::Fields16, KERNEL_SETTLE, Witness::Settle(small(3, &[(0, 1, 1), (1, 2, 1)], &[(0, 35_000)], 0), 1)), + (Fault::Fields32, KERNEL_SETTLE, Witness::Settle(small(2, &[(0, 1, 1)], &[(0, 1 << 31)], 0), 2)), + (Fault::Fields32CheckedWithoutCouplings, KERNEL_SETTLE, Witness::Settle(small(2, &[(0, 1, 1)], &[(0, lane), (1, 1)], 0), 5)), + (Fault::OnlyTheFirstCells(64), KERNEL_SETTLE, Witness::Settle(path(65), 140)), + (Fault::OnlyTheFirstCells(256), KERNEL_SETTLE, Witness::Settle(path(257), 520)), + (Fault::SettleEndsLate(64), KERNEL_SETTLE, Witness::Settle(path(74), 160)), + (Fault::SettleEndsLate(256), KERNEL_SETTLE, Witness::Settle(path(266), 540)), + (Fault::DroppedCarry, KERNEL_SAMPLE, Witness::Chains(chain_case("a-draw/the-carry-in-a-product"))), + (Fault::HighWordCompare, KERNEL_SAMPLE, Witness::Chains(chain_case("a-draw/under-the-first-cut-by-its-low-word"))), + (Fault::Fields16, KERNEL_SAMPLE, Witness::Chains(case("fields-16", small(3, &[(0, 1, 1), (1, 2, -1)], &[(0, 35_000), (2, -33_000)], 0), held(&[(20_000.0, 6)]), 31, 8))), + (Fault::Fields32, KERNEL_SAMPLE, Witness::Chains(case("fields-32", small(2, &[(0, 1, 1)], &[(0, 1 << 31)], 0), held(&[(2_147_483_648.0, 4)]), 32, 8))), + (Fault::Fields32CheckedWithoutCouplings, KERNEL_SAMPLE, Witness::Chains(case("fields-32-and-a-coupling", small(2, &[(0, 1, 1)], &[(0, lane), (1, 1)], 0), held(&[(2_147_483_648.0, 4)]), 33, 8))), + (Fault::CellsBackwards, KERNEL_SAMPLE, Witness::Chains(chain_case("the-order-of-the-cells"))), + (Fault::OnlyTheFirstChains(64), KERNEL_SAMPLE, Witness::Chains(case("a-call-of-100", small(2, &[(0, 1, -1)], &[(1, 1)], 0), held(&[(1.0, 2)]), 15, 100))), + (Fault::DeviceWindowRestart, KERNEL_SAMPLE, Witness::Chains(case("eight-cells-5000-sweeps", eight(), Temperatures::Cooling { t0: 3.0, sweeps: 5000 }, 21, 4))), + // The fault classes of the second review: each by a job of its own. + (Fault::TileRemainder, KERNEL_SETTLE, Witness::Settle(ones(40), 1)), + (Fault::TileRemainder, KERNEL_SAMPLE, Witness::Chains(case("forty-cells", ones(40), held(&[(1.0, 2)]), 40, 4))), + (Fault::RowCap(32), KERNEL_SETTLE, Witness::Settle(row(40), 4)), + (Fault::RowCap(32), KERNEL_SAMPLE, Witness::Chains(case("a-row-of-forty", row(40), held(&[(0.5, 3)]), 41, 8))), + (Fault::RowCap(15), KERNEL_SAMPLE, Witness::Chains(case("a-row-of-twenty", row(20), held(&[(0.5, 3)]), 42, 8))), + (Fault::WarpRemainder, KERNEL_SAMPLE, Witness::Chains(case("a-call-of-20", small(2, &[(0, 1, -1)], &[(1, 1)], 0), held(&[(1.0, 2)]), 43, 20))), + (Fault::Counter24, KERNEL_SAMPLE, Witness::Chains(case("past-2^24", alternating(64), held(&[(0.0, 262_143), (1.5, 2)]), 44, 1))), + (Fault::FieldCostWrap, KERNEL_SETTLE, Witness::Settle(small(1, &[], &[(0, -lane)], 1000), 1)), + (Fault::MixedFinal16, KERNEL_SAMPLE, Witness::Chains(case("annealed-at-40000", small(1, &[], &[(0, 40_000)], 0), Temperatures::Cooling { t0: 2.0, sweeps: 5 }, 46, 2))), + (Fault::OnlyTheFirstCells(600), KERNEL_SETTLE, Witness::Settle(ones(1000), 1)), + (Fault::OnlyTheFirstCells(1024), KERNEL_SETTLE, Witness::Settle(ones(2000), 1)), + (Fault::SampleOnlyTheFirstCells(600), KERNEL_SAMPLE, Witness::Chains(case("a-thousand-cells", ones(1000), held(&[(1.0, 2)]), 47, 2))), + (Fault::TableUpTo(32), KERNEL_SAMPLE, Witness::Chains(case("a-hub-of-31", hub(31, 0), held(&[(1.0, 3)]), 48, 4))), + (Fault::SpanUpTo(81), KERNEL_SAMPLE, Witness::Chains(case("a-span-of-401", two_hubs(40, 160), held(&[(401.0, 3)]), 49, 4))), + ]; + let mut windows: Vec<(usize, String)> = Vec::new(); + for c in chain_cases() { let w = c.landscape.lower(); let at = reach::window(&w, &reach::reach(&w), true); - if at < c.temperatures.each().len() { - windows.push(at); + if at < c.temperatures.each().len() && !windows.iter().any(|(a, _)| *a == at) { + windows.push((at, c.name)); } } - windows.sort_unstable(); - windows.dedup(); - assert!(windows.len() >= 2, "windows by table and by work: {windows:?}"); - let sample = sample.into_iter().chain(windows.iter().flat_map(|&at| [Fault::CounterRestartsEachWindow(at), Fault::StopsAfterOneWindow(at)])); - for f in sample { - assert!(!wai_quantum::quantum_landscape::conformance::chain_vectors_hold(&f), "{f:?}"); + assert!(windows.len() >= 3, "windows by table, by work, and the core set's: {windows:?}"); + for (at, name) in windows { + for f in [Fault::CounterRestartsEachWindow(at), Fault::StopsAfterOneWindow(at)] { + out.push((f, KERNEL_SAMPLE, Witness::Chains(chain_case(&name)))); + } + } + out +} + +fn holds(m: &dyn Member, kernel: &str) -> bool { + if kernel == KERNEL_SETTLE { settle_vectors_hold(m) } else { chain_vectors_hold(m) } +} + +/// With no fault the stand-in is the reference, so each fault below is the only +/// difference. Each loses its kernel's grade declaring no capacity, and declaring +/// each capacity it might that still takes a job it gets wrong: the most hostile, +/// the least at or above the floor; that one rounded up as hardware rounds; with +/// no cell count; and the numbers the in-tree members declare. +#[test] +fn every_fault_loses_the_grade_of_the_kernel_it_is_in() { + let clean = Fault::DeclaresFieldBound(i64::MAX); + let c = certify(&clean).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact)); + for (f, kernel, witness) in faults() { + assert!(!holds(&f, kernel), "{f:?}"); + let caps = witness.realistic(); + assert!(!caps.is_empty(), "{f:?}: the most hostile capacity takes its job"); + for cap in caps { + let declared = Declared(f, cap); + assert!(witness.wrong(&declared, &cap), "{f:?}: positive control, a job within {} it gets wrong", cap.json()); + assert!(!holds(&declared, kernel), "{f:?} declaring {}", cap.json()); + } } } -/// A member takes every vector at or under the line, and only there may it decline. +/// A member is graded within the capacity it declares: a correct one earns +/// `bit-exact` within any capacity at or above the floor, one that refuses a +/// vector within its capacity loses the grade, and one under the floor is not +/// certified. #[test] -fn a_member_that_declines_below_the_line_earns_nothing_and_one_at_it_keeps_its_grade() { - for most in [41, 70_000, i64::from(i32::MAX) - 1] { - let narrow = Fault::DeclinesAbove(most); +fn a_member_is_graded_within_the_capacity_it_declares() { + for most in [41, 70_000, i64::from(i32::MAX)] { + let narrow = Fault::DeclaresFieldBound(most); let c = certify(&narrow).unwrap(); - assert_eq!(c.parity(KERNEL_SETTLE), Parity::Uncertified, "declines above {most}"); - assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact, "declines above {most}"); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact), "declares {most}"); + assert_eq!(c.capacity(), narrow.capacity()); } - let lanes32 = Fault::DeclinesAbove(i64::from(i32::MAX)); - let c = certify(&lanes32).unwrap(); - assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact)); + let under = Fault::DeclaresFieldBound(40); + assert!(certify(&under).err().unwrap().contains("under the floor")); + let choosy = Fault::RefusesAbove(1000); + let c = certify(&choosy).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::Uncertified, Parity::Uncertified), "it refused vectors within its capacity"); } /// The set reaches what large jobs reach: a settle past a workgroup and past a @@ -595,3 +1076,1798 @@ fn the_statistic_pools_small_bins() { let (chi, _, _) = chi_square(&[50, 49, 1], &[0.5, 0.5, 0.0], 100); assert!(chi.is_infinite(), "a state that cannot occur, occurring, fails"); } + +// ── the adversarial review's probes (#678), kept ─────────────────────────────────────────── +// Each faulty member below was certified bit-exact at 75edfe063 by declaring a +// capacity that left its fault untested while it admitted jobs that show it. + +fn grades(c: &Certified<'_>) -> (Parity, Parity) { + (c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)) +} + +/// A job served by a certified member: the parity its answer states, and +/// whether its bits are the reference's (a refusal states none, and is honest). +fn served(c: &Certified<'_>, op: &str, req: &Value) -> (String, bool) { + let want = job::call(op, req).expect("the reference answers"); + match job::call_certified(c, op, req, MAX_JOB_BYTES) { + Ok(a) => { + // A replay's lines, by state, energy and digest. + let lines = |v: &Value| -> Vec { + v["lines"].as_str().unwrap_or("").lines().map(|l| { + let f: Vec<&str> = l.split(' ').collect(); + format!("{} {} {}", f.get(2).unwrap_or(&""), f.get(3).unwrap_or(&""), f.get(6).unwrap_or(&"")) + }).collect() + }; + let same = if op == "replay" { lines(&a) == lines(&want) } else { ["state", "energy", "replay", "work"].iter().all(|k| a[k] == want[k]) }; + (a["member"]["parity"].as_str().unwrap_or("").to_string(), same) + } + Err(e) => (format!("refused: {}", e.message), true), + } +} + +/// An answer says `bit-exact` only where its bits are the reference's. +fn honest(s: &(String, bool)) -> bool { + s.0 != "bit-exact" || s.1 +} + +fn path_program(n: usize, steps: u32) -> String { + let mut p = format!("vars {n}\n"); + for i in 0..n - 1 { + p += &format!("agree {i} {}\n", i + 1); + } + p + &format!("field 0 3\nminimize steps={steps}\n") +} + +const EIGHT: &str = "vars 8\nagree 0 1\noppose 1 2\nagree 2 3\nagree 3 4\noppose 4 5\nagree 5 6\nagree 6 7\nfield 0 1\ncost 1\nsolve t0=3 restarts=1 steps=5000\n"; + +/// p02: a batch is not a limit. Certification splits every vector into calls +/// of it, as the host splits a job, and runs one call of exactly that many. +#[test] +fn review_p02_a_batch_is_split_never_skipped() { + let c0 = certify(&Fault::OnlyTheFirstChains(64)).unwrap(); + let m = Declared(Fault::OnlyTheFirstChains(64), Capacity { chains_per_call: Some(100), ..Capacity::NONE }); + let c = certify(&m).unwrap(); + let program = "vars 6\nagree 0 1\noppose 1 2\nagree 2 3\nagree 3 4\noppose 4 5\nfield 0 3\ncost 1\nsolve t0=2 restarts=1 steps=20\n"; + let s = served(&c, "replay", &json!({ "program": program, "seeds": 100, "seed": "1" })); + println!("PROBE p02 OnlyTheFirstChains(64): no capacity {:?}; chains_per_call 100 {:?}; replay of 100 seeds {s:?}", grades(&c0), grades(&c)); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact); + assert!(honest(&s), "{s:?}"); +} + +/// p03, p04, p10: a limit between the vectors' points is held at its edge. +#[test] +fn review_p03_p04_p10_a_limit_between_the_vectors_is_held_at_its_edge() { + let cells = |n| Capacity { cells: Some(n), ..Capacity::NONE }; + let couplings = Capacity { couplings: Some(992), ..Capacity::NONE }; + for (what, f, cap, n, steps) in [ + ("p03 OnlyTheFirstCells(256), cells 299", Fault::OnlyTheFirstCells(256), cells(299), 280, 1000), + ("p03 OnlyTheFirstCells(300), couplings 992", Fault::OnlyTheFirstCells(300), couplings, 497, 1200), + ("p03 SettleEndsLate(256), cells 299", Fault::SettleEndsLate(256), cells(299), 290, 1000), + ("p10 OnlyTheFirstCells(256), cells 299, ending at its bound", Fault::OnlyTheFirstCells(256), cells(299), 280, 270), + ("p10 OnlyTheFirstCells(300), couplings 992, ending at its bound", Fault::OnlyTheFirstCells(300), couplings, 450, 400), + ] { + let m = Declared(f, cap); + let c = certify(&m).unwrap(); + let s = served(&c, "run", &json!({ "program": path_program(n, steps) })); + println!("PROBE {what}: {:?}; a path of {n} cells, steps={steps}: {s:?}", grades(&c)); + assert_ne!(c.parity(KERNEL_SETTLE), Parity::BitExact, "{what}"); + assert!(honest(&s), "{what}: {s:?}"); + } + let lanes = Limits { field_bound: Some(39_999), ..Limits::NONE }; + let m = Declared(Fault::Fields16, Capacity { at_zero: lanes, above_zero: lanes, ..Capacity::NONE }); + let c = certify(&m).unwrap(); + let settle = served(&c, "run", &json!({ "program": "vars 3\nfield 0 35000\nagree 0 1\nagree 1 2\nminimize steps=1\n" })); + let sample = served(&c, "replay", &json!({ "program": "vars 3\nfield 0 35000\nfield 2 -33000\nagree 0 1\noppose 1 2\nsolve t0=20000 restarts=1 steps=6\n", "seeds": 50 })); + println!("PROBE p04/p10 Fields16, field bound 39,999 on both sides: {:?}; settle at 35,000 {settle:?}; sampled at 20,000 {sample:?}", grades(&c)); + assert_ne!(c.parity(KERNEL_SETTLE), Parity::BitExact); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact); + assert!(honest(&settle) && honest(&sample)); +} + +/// p05, p12: a draw counter that restarts at each of a device's windows loses +/// its grade at the floor itself, and under the window vectors' cells. +#[test] +fn review_p05_p12_a_window_fault_loses_its_grade_at_the_floor() { + for (what, cap) in [("p12 the floor", floor()), ("p05 cells 63", Capacity { cells: Some(63), ..Capacity::NONE }), ("p05 cells 64", Capacity { cells: Some(64), ..Capacity::NONE })] { + let m = Declared(Fault::DeviceWindowRestart, cap); + let c = certify(&m).unwrap(); + let s = served(&c, "replay", &json!({ "program": EIGHT, "seeds": 20 })); + println!("PROBE {what}, DeviceWindowRestart: {:?}; 8 cells, 5,000 sweeps, 20 seeds: {s:?}", grades(&c)); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact, "{what}"); + assert!(honest(&s), "{what}: {s:?}"); + } +} + +/// certify asks the member for its capacity more than once: p06's member. +struct Shifty(Cell); + +impl Member for Shifty { + fn id(&self) -> String { + "gpu:shifty".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, _start: &[i8], bound: u32) -> Result { + Ok(w.settle(&vec![1; w.n()], bound)) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + Ok(chains.iter().map(|c| anneal_with(w, &c.start, temperatures, c.seed.wrapping_add(1))).collect()) + } + fn capacity(&self) -> Capacity { + let k = self.0.get(); + self.0.set(k + 1); + if k == 0 { Capacity::NONE } else { Capacity { cells: Some(0), ..Capacity::NONE } } + } +} + +/// p06: certification reads a capacity once, and holds the member to that one. +#[test] +fn review_p06_certification_reads_a_capacity_once() { + let m = Shifty(Cell::new(0)); + let c = certify(&m).unwrap(); + let s = served(&c, "run", &json!({ "program": "vars 4\nagree 0 1\noppose 1 2\nagree 2 3\nfield 0 2\nminimize\n" })); + println!("PROBE p06 Shifty: capacity read {} time(s); {:?}; stored capacity {}; a settle job {s:?}", m.0.get(), grades(&c), c.capacity().json()); + assert_eq!(m.0.get(), 1); + assert_eq!(c.capacity(), Capacity::NONE); + assert_eq!(c.parity(KERNEL_SETTLE), Parity::Uncertified); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact); + assert!(honest(&s)); +} + +/// The skeptic's member: the reference in every respect but one. It runs a +/// chain in the windows [`reach::window`] gives, as the GPU members do, and its +/// draw counter starts again at 0 in each. +struct Windowed(Capacity); + +impl Member for Windowed { + fn id(&self) -> String { + "gpu:windowed".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A device whose draw counter restarts each window.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + Ok(w.settle(start, bound)) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + let win = reach::window(w, &reach::reach(w), temperatures.iter().any(|&t| t > 0.0)).max(1); + Ok(chains + .iter() + .map(|c| { + let mut s = c.start.clone(); + let (mut accumulates, mut draws) = (0u64, 0u64); + for slice in temperatures.chunks(win) { + let o = anneal_with(w, &s, slice, c.seed); + s = o.state; + accumulates += o.accumulates; + draws += o.draws; + } + let term = if w.rests(&s) { Term::FixedPoint } else { Term::Bound }; + Outcome { state: s, sweeps: temperatures.len() as u32, term, accumulates, draws } + }) + .collect()) + } + fn capacity(&self) -> Capacity { + self.0 + } +} + +/// The skeptic's probe: a device whose draws restart each window is bit-exact +/// within no capacity it may declare, and at run time says what it is. +#[test] +fn review_skeptic_a_device_that_restarts_its_draws_each_window() { + let f = floor(); + for (what, cap) in [ + ("no capacity", Capacity::NONE), + ("the floor", f), + ("cells 63", Capacity { cells: Some(63), ..Capacity::NONE }), + ("cells 64", Capacity { cells: Some(64), ..Capacity::NONE }), + ("the floor with cells 64", Capacity { cells: Some(64), ..f }), + ("the floor with no cells", Capacity { cells: None, ..f }), + ("the floor with no cells or couplings", Capacity { cells: None, couplings: None, ..f }), + ] { + let m = Windowed(cap); + let c = certify(&m).unwrap(); + println!("PROBE skeptic Windowed, {what}: {:?}", grades(&c)); + assert_eq!(c.parity(KERNEL_SETTLE), Parity::BitExact, "{what}"); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact, "{what}"); + } + let m = Windowed(f); + let c = certify(&m).unwrap(); + let text = "vars 8\ncost 1\noppose 0 1\noppose 1 2\noppose 0 2\nagree 3 4\nagree 4 5\nagree 3 5\noppose 5 6\noppose 3 6\nagree 6 7\nsolve t0=3 restarts=1 steps=5000\n"; + let mut differ = 0; + for seed in 1..=20u64 { + let s = served(&c, "run", &json!({ "program": text, "seed": seed.to_string() })); + differ += usize::from(!s.1); + assert!(honest(&s), "seed {seed}: {s:?}"); + } + let placed = [c, Certified::reference()]; + let req = json!({ "program": text, "seed": "3" }); + let p = job::call_placed(&placed, "run", &req, MAX_JOB_BYTES).unwrap(); + let same = p["replay"] == job::call("run", &req).unwrap()["replay"]; + println!("PROBE skeptic at the floor: {differ} of 20 answers differ from the reference, none says bit-exact; placed ahead of the reference: {} at {}, same bits {same}", p["member"]["id"], p["member"]["parity"]); + assert!(differ > 0, "positive control: the fault shows at run time"); + assert!(p["member"]["parity"] != "bit-exact" || same); +} + +// ── the second review's probes (#678 round 2), kept ──────────────────────────────────────── +// Each fault below was certified bit-exact at 28fa25d93 within a capacity that +// takes a job it gets wrong; each now loses the grade by vectors chosen by rule. + +fn lanes(field_bound: Option, field: Option, cost: Option) -> Limits { + Limits { field, field_bound, cost } +} + +/// Each (fault, kernel, capacity) loses the kernel's `bit-exact`. +fn each_loses(cases: &[(&str, Fault, &str, Capacity)]) { + for (what, f, kernel, cap) in cases { + let m = Declared(*f, *cap); + let held = holds(&m, kernel); + println!("PROBE r2 {what}: {f:?} declaring {} -> {kernel} {}", cap.json(), if held { "bit-exact" } else { "not bit-exact" }); + assert!(!held, "{what}: {f:?} declaring {}", cap.json()); + } +} + +/// 1 and 14: cells past couplings / 2 + 1, and past a thousand with no capacity. +#[test] +fn review2_cells_past_the_couplings_and_past_a_thousand() { + let c992 = Capacity { couplings: Some(992), ..Capacity::NONE }; + let f = floor(); + each_loses(&[ + ("couplings 992 only", Fault::OnlyTheFirstCells(497), KERNEL_SETTLE, c992), + ("couplings 992 only", Fault::OnlyTheFirstCells(600), KERNEL_SETTLE, c992), + ("the floor with no cells", Fault::OnlyTheFirstCells(600), KERNEL_SETTLE, Capacity { cells: None, ..f }), + ("cells 1000, couplings 992", Fault::OnlyTheFirstCells(512), KERNEL_SETTLE, Capacity { cells: Some(1000), ..c992 }), + ("cells 4095, couplings 992", Fault::OnlyTheFirstCells(600), KERNEL_SETTLE, Capacity { cells: Some(4095), ..c992 }), + ("couplings 992 only", Fault::SampleOnlyTheFirstCells(600), KERNEL_SAMPLE, c992), + ("the floor with no cells", Fault::SampleOnlyTheFirstCells(600), KERNEL_SAMPLE, Capacity { cells: None, ..f }), + ("cells 4096, couplings 992", Fault::SampleOnlyTheFirstCells(600), KERNEL_SAMPLE, Capacity { cells: Some(4096), ..c992 }), + ("no capacity", Fault::OnlyTheFirstCells(1001), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::OnlyTheFirstCells(1024), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::OnlyTheFirstCells(4095), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::SampleOnlyTheFirstCells(1025), KERNEL_SAMPLE, Capacity::NONE), + ("cells 2048, couplings 2048", Fault::OnlyTheFirstCells(1100), KERNEL_SETTLE, Capacity { cells: Some(2048), couplings: Some(2048), ..Capacity::NONE }), + ]); +} + +/// 2 and 7: a sweep at 0 after hot ones on large fields, and a field beside a cost past a 32-bit lane. +#[test] +fn review2_annealing_ends_and_a_field_beside_a_cost() { + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(i64::from(i32::MAX)), None, None), above_zero: lanes(Some(i64::from(i32::MAX)), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let lanes_cost = |c: i64| Capacity { at_zero: lanes(Some(i64::from(i32::MAX)), None, Some(c)), above_zero: lanes(Some(i64::from(i32::MAX)), None, None), ..Capacity::NONE }; + each_loses(&[ + ("no capacity", Fault::MixedFinal16, KERNEL_SAMPLE, Capacity::NONE), + ("the FPGA's", Fault::MixedFinal16, KERNEL_SAMPLE, fpga), + ("the WebGPU member's numbers", Fault::MixedFinal16, KERNEL_SAMPLE, gpu), + ("32-bit lanes, a cost of 1,000", Fault::FieldCostWrap, KERNEL_SETTLE, lanes_cost(1000)), + ("32-bit lanes, a cost of 8", Fault::FieldCostWrap, KERNEL_SETTLE, lanes_cost(8)), + ("32-bit lanes, a cost of 2^20", Fault::FieldCostWrap, KERNEL_SETTLE, lanes_cost(1 << 20)), + ("32-bit lanes, a cost of 1,000", Fault::FieldCostWrap, KERNEL_SAMPLE, lanes_cost(1000)), + ("the floor joined with 32-bit lanes and a cost of 1,000", Fault::FieldCostWrap, KERNEL_SETTLE, floor().join(&Capacity { at_zero: lanes(Some(i64::from(i32::MAX)), Some(i64::from(i32::MAX)), Some(1000)), ..Capacity::NONE })), + ]); +} + +/// 3, 8 and 9: a workgroup's remainder, a row's length, a warp's remainder. +#[test] +fn review2_remainders_and_rows() { + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(i64::from(i32::MAX)), None, None), above_zero: lanes(Some(i64::from(i32::MAX)), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let cells = |n: u64| Capacity { cells: Some(n), ..Capacity::NONE }; + let mut cases = Vec::new(); + for (what, cap) in [("cells 64", cells(64)), ("cells 64 over the floor", Capacity { cells: Some(64), ..floor() }), ("the FPGA's", fpga), ("cells 128", cells(128)), ("cells 256", cells(256)), ("cells 288", cells(288)), ("no capacity", Capacity::NONE)] { + cases.push((what, Fault::TileRemainder, KERNEL_SETTLE, cap)); + cases.push((what, Fault::TileRemainder, KERNEL_SAMPLE, cap)); + } + for (what, cap) in [("no capacity", Capacity::NONE), ("the WebGPU member's numbers", gpu), ("cells 64", cells(64)), ("the FPGA's", fpga)] { + cases.push((what, Fault::RowCap(32), KERNEL_SAMPLE, cap)); + cases.push((what, Fault::RowCap(32), KERNEL_SETTLE, cap)); + cases.push((what, Fault::RowCap(15), KERNEL_SAMPLE, cap)); + } + let batch = |k: u64| Capacity { chains_per_call: Some(k), ..Capacity::NONE }; + for (what, cap) in [("no capacity", Capacity::NONE), ("a batch of 64 over the floor", Capacity { chains_per_call: Some(64), ..floor() }), ("a batch of 48", batch(48)), ("a batch of 4096", batch(4096)), ("cells 64", cells(64))] { + cases.push((what, Fault::WarpRemainder, KERNEL_SAMPLE, cap)); + } + each_loses(&cases); +} + +/// 4, 5 and 6: a table and a span at the declared number, and fields at a lane's wrap points. +#[test] +fn review2_tables_spans_and_lanes() { + let both = |l: Limits| Capacity { at_zero: l, above_zero: l, ..Capacity::NONE }; + each_loses(&[ + ("cells 32, a table of 200", Fault::TableUpTo(32), KERNEL_SAMPLE, Capacity { cells: Some(32), table_entries: Some(200), ..Capacity::NONE }), + ("fields to 40 above 0, a table of 200", Fault::TableUpTo(41), KERNEL_SAMPLE, Capacity { above_zero: lanes(None, Some(40), None), table_entries: Some(200), ..Capacity::NONE }), + ("fields to 40 above 0, a span of 1,000", Fault::SpanUpTo(81), KERNEL_SAMPLE, Capacity { above_zero: lanes(None, Some(40), None), uncertain_span: Some(1000), ..Capacity::NONE }), + ("a table of 200 alone", Fault::TableUpTo(199), KERNEL_SAMPLE, Capacity { table_entries: Some(200), ..Capacity::NONE }), + ("a field bound of 66,000 on both sides", Fault::Fields16, KERNEL_SETTLE, both(lanes(Some(66_000), None, None))), + ("a field bound of 69,999 on both sides", Fault::Fields16, KERNEL_SETTLE, both(lanes(Some(69_999), None, None))), + ("a field bound of 69,999 at 0", Fault::Fields16, KERNEL_SETTLE, Capacity { at_zero: lanes(Some(69_999), None, None), ..Capacity::NONE }), + ("fields to 66,000", Fault::Fields16, KERNEL_SETTLE, both(lanes(None, Some(66_000), None))), + ("fields to 32,767, no field bound", Fault::Fields16, KERNEL_SETTLE, both(lanes(None, Some(32_767), None))), + ("fields to 32,767, no field bound", Fault::Fields16, KERNEL_SAMPLE, both(lanes(None, Some(32_767), None))), + ("fields to 32,767 at 0", Fault::Fields16, KERNEL_SETTLE, Capacity { at_zero: lanes(None, Some(32_767), None), ..Capacity::NONE }), + ("fields to 32,767 and 64 cells", Fault::Fields16, KERNEL_SAMPLE, Capacity { cells: Some(64), ..both(lanes(None, Some(32_767), None)) }), + ]); +} + +/// 10: draw indices past 24 bits. A signed 32-bit index is left to the audit: +/// a run past 2³¹ is 2³¹ updates in one chain, minutes on a device (§7.1, §7.3). +#[test] +fn review2_draw_indices() { + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + each_loses(&[ + ("no capacity", Fault::Counter24, KERNEL_SAMPLE, Capacity::NONE), + ("the FPGA's", Fault::Counter24, KERNEL_SAMPLE, fpga), + ("cells 64 and 300,000 sweeps over the floor", Fault::Counter24, KERNEL_SAMPLE, Capacity { cells: Some(64), sweeps: Some(300_000), ..floor() }), + ("999,999 sweeps alone", Fault::Counter24, KERNEL_SAMPLE, Capacity { sweeps: Some(999_999), ..Capacity::NONE }), + ]); +} + +/// A sampler with draws of its own whose cells from `self.0` on never move. +struct Stuck(usize, Capacity); + +impl Member for Stuck { + fn id(&self) -> String { + "gpu:stuck".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + CpuReference.settle(w, start, bound) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + Ok(chains + .iter() + .map(|c| { + // The reference's chain from another seed: draws of its own, its law the same. + let mut o = anneal_with(w, &c.start, temperatures, c.seed ^ 0x5EED_5EED); + for i in self.0..w.n() { + o.state[i] = c.start[i]; + } + o.term = if w.rests(&o.state) { Term::FixedPoint } else { Term::Bound }; + o + }) + .collect()) + } + fn capacity(&self) -> Capacity { + self.1 + } +} + +/// 11: the battery at a declared cell count reads the cells past the fixed battery's. +#[test] +fn review2_the_battery_at_the_declared_cells() { + let control = certify(&Stuck(usize::MAX, Capacity { cells: Some(40), ..Capacity::NONE })).unwrap(); + let stuck = certify(&Stuck(32, Capacity { cells: Some(40), ..Capacity::NONE })).unwrap(); + println!("PROBE r2 battery: draws of its own, cells 40 -> {:?}; and cells 32 on stuck -> {:?}", grades(&control), grades(&stuck)); + assert_eq!(control.parity(KERNEL_SAMPLE), Parity::Statistical, "positive control: a correct sampler with draws of its own"); + assert_eq!(stuck.parity(KERNEL_SAMPLE), Parity::Uncertified); +} + +// ── the host's audit (§7.3) ──────────────────────────────────────────────────────────────── + +/// A member that is the reference until it is armed, and has its fault from then +/// on: certified `bit-exact`, wrong at run time. +struct Sleeper(Fault, Cell, String); + +impl Sleeper { + fn new(f: Fault, id: &str) -> Sleeper { + Sleeper(f, Cell::new(false), id.into()) + } +} + +impl Member for Sleeper { + fn id(&self) -> String { + self.2.clone() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + if self.1.get() { self.0.settle(w, start, bound) } else { CpuReference.settle(w, start, bound) } + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + if self.1.get() { self.0.chains(w, chains, temperatures) } else { CpuReference.chains(w, chains, temperatures) } + } +} + +fn program_of(l: &Landscape, tail: &str) -> String { + let mut p = format!("vars {}\n", l.len()); + for i in 0..l.len() { + if l.field()[i] != 0 { + p += &format!("field {i} {}\n", l.field()[i]); + } + for j in (i + 1)..l.len() { + match l.coupling(i, j) { + 1 => p += &format!("agree {i} {j}\n"), + -1 => p += &format!("oppose {i} {j}\n"), + _ => {} + } + } + } + if l.cost() != 0 { + p += &format!("cost {}\n", l.cost()); + } + p + tail +} + +/// Every fault class, certified while asleep, is caught by the audit on the +/// first job it gets wrong, demoted at once, the job answered by the reference, +/// and the demotion carried in every later answer. +#[test] +fn audit_demotes_a_member_on_the_first_job_it_gets_wrong() { + let lane = i64::from(i32::MAX); + let two = small(2, &[(0, 1, -1)], &[(1, 1)], 0); + let jobs: Vec<(Fault, String)> = vec![ + (Fault::Fields16, program_of(&small(3, &[(0, 1, 1), (1, 2, 1)], &[(0, 35_000)], 0), "minimize steps=1\n")), + (Fault::Fields32, program_of(&small(2, &[(0, 1, 1)], &[(0, 1 << 31)], 0), "minimize steps=2\n")), + (Fault::OnlyTheFirstCells(64), program_of(&ones(100), "minimize steps=1\n")), + (Fault::SettleEndsLate(64), path_program(80, 1000)), + (Fault::CellsBackwards, program_of(&eight(), "solve t0=1.5 restarts=4 steps=3\n")), + (Fault::OnlyTheFirstChains(64), program_of(&two, "solve t0=1 restarts=1 steps=2\n")), + (Fault::DeviceWindowRestart, EIGHT.to_string()), + (Fault::TileRemainder, program_of(&ones(40), "solve t0=1 restarts=2 steps=2\n")), + (Fault::RowCap(32), program_of(&row(40), "solve t0=0.5 restarts=4 steps=3\n")), + (Fault::WarpRemainder, program_of(&two, "solve t0=1 restarts=1 steps=2\n")), + // Draws past 2^24 while still hot: 4,096 cells, 2,048 opposed pairs, from sweep 4,096 on. + (Fault::Counter24, program_of(&opposed_pairs(4096), "solve t0=1.5 restarts=1 steps=8000\n")), + (Fault::FieldCostWrap, program_of(&small(1, &[], &[(0, -lane)], 1000), "minimize steps=1\n")), + (Fault::MixedFinal16, program_of(&small(1, &[], &[(0, 40_000)], 0), "solve t0=2 restarts=2 steps=5\n")), + (Fault::SampleOnlyTheFirstCells(600), program_of(&opposed_pairs(1000), "solve t0=1 restarts=2 steps=2\n")), + (Fault::TableUpTo(32), program_of(&hub(31, 0), "solve t0=1 restarts=4 steps=3\n")), + (Fault::SpanUpTo(81), program_of(&two_hubs(40, 160), "solve t0=401 restarts=4 steps=3\n")), + ]; + for (k, (f, program)) in jobs.into_iter().enumerate() { + let sleeper = Sleeper::new(f, &format!("gpu:sleeper-{k}")); + let c = certify(&sleeper).unwrap(); + assert_eq!(grades(&c), (Parity::BitExact, Parity::BitExact), "{f:?}: asleep, it is the reference"); + sleeper.1.set(true); + // Positive control: armed, run by name with no audit, it gets the job wrong at some seed. + let by_name = wai_quantum::quantum_landscape::member::named(&sleeper).unwrap(); + // A fault in a call's later chains shows in a replay of many seeds, one chain each, in one call. + let (op, seeds) = match f { + Fault::OnlyTheFirstChains(_) => ("replay", 100), + Fault::WarpRemainder => ("replay", 20), + _ => ("run", 1), + }; + let request = |seed: u64| if op == "replay" { json!({ "program": program, "seed": seed.to_string(), "seeds": seeds }) } else { json!({ "program": program, "seed": seed.to_string() }) }; + let same = |a: &Value, b: &Value| if op == "replay" { a["lines"].as_str().map(|l| l.lines().map(|x| x.split(' ').skip(2).collect::>()).collect::>()) == b["lines"].as_str().map(|l| l.lines().map(|x| x.split(' ').skip(2).collect::>()).collect::>()) } else { ["state", "energy", "replay", "work"].iter().all(|k| a[k] == b[k]) }; + let wrong_at = |seed: u64| { + let want = job::call(op, &request(seed)).unwrap(); + job::call_certified(&by_name, op, &request(seed), MAX_JOB_BYTES).map(|a| !same(&a, &want)).unwrap_or(true) + }; + let seed = (1..=20u64).find(|&s| wrong_at(s)).unwrap_or_else(|| panic!("{f:?}: positive control, the armed member gets the job wrong")); + let req = request(seed); + let want = job::call(op, &req).unwrap(); + let placed = [c, Certified::reference()]; + let a = job::call_placed(&placed, op, &req, MAX_JOB_BYTES).unwrap(); + let demoted = placed[0].demoted(); + println!("PROBE audit {f:?}: demoted on job 1: {}; answered by {}; why: {:?}", demoted.is_some(), a["member"]["id"], demoted); + assert!(demoted.is_some(), "{f:?}: the audit caught it on the first job"); + assert_eq!(a["member"]["id"].as_str(), Some("cpu:reference"), "{f:?}: answered by the reference"); + assert!(same(&a, &want), "{f:?}: the reference's bits"); + assert!(a["refusals"][0]["why"].as_str().unwrap().contains("demoted it to uncertified"), "{}", a["refusals"]); + assert_eq!(grades(&placed[0]), (Parity::Uncertified, Parity::Uncertified)); + // Later jobs: never placed on it again, and by name its answers say why. + let next = job::call_placed(&placed, op, &req, MAX_JOB_BYTES).unwrap(); + assert!(next["refusals"][0]["why"].as_str().unwrap().contains("not certified"), "{}", next["refusals"]); + if let Ok(a) = job::call_certified(&placed[0], op, &req, MAX_JOB_BYTES) { + assert_eq!(a["member"]["parity"], "uncertified"); + assert!(a["member"]["demoted"].is_string() && a["label"].as_str().unwrap().contains("demoted it to uncertified"), "{}", a["label"]); + } + } +} + +/// A member graded `bit-exact` that gives the reference's bits is never demoted. +#[test] +fn audit_leaves_a_correct_member_alone() { + let sleeper = Sleeper::new(Fault::DeclaresFieldBound(i64::MAX), "gpu:sleeper-correct"); + let c = certify(&sleeper).unwrap(); + sleeper.1.set(true); + for program in [EIGHT.to_string(), path_program(80, 1000), program_of(&ones(40), "solve t0=1 restarts=130 steps=2\n")] { + let a = job::call_certified(&c, "run", &json!({ "program": program, "seed": "3" }), MAX_JOB_BYTES).unwrap(); + assert_eq!(a["member"]["parity"], "bit-exact"); + } + assert!(c.demoted().is_none()); +} + +// ── the third review's probes (#678 round 3), kept ───────────────────────────────────────── + +/// 1, 8: the audit runs one chain of every seed's run. A member wrong on the first +/// chain of every call is demoted on its first replay of many seeds. +#[test] +fn review3_audit_a_chain_of_every_run() { + let sleeper = Sleeper::new(Fault::FirstOfCallWrong, "gpu:sleeper-r3-first-of-call"); + let c = certify(&sleeper).unwrap(); + sleeper.1.set(true); + let program = "vars 2\noppose 0 1\nfield 1 1\nsolve t0=1 restarts=1 steps=2\n"; + let placed = [c, Certified::reference()]; + let a = job::call_placed(&placed, "replay", &json!({ "program": program, "seeds": 100, "seed": "1" }), MAX_JOB_BYTES).unwrap(); + println!("PROBE r3 per-run audit: demoted on the first replay: {:?}; answered by {}", placed[0].demoted(), a["member"]["id"]); + assert!(placed[0].demoted().is_some() && a["member"]["id"] == "cpu:reference"); +} + +/// 2, 7: a settle's re-run is decided from the work the host asked for, not the +/// sweeps the member reports. A member that says every long settle ran to its +/// bound is demoted on the first. +#[test] +fn review3_a_settle_audit_the_member_cannot_opt_out_of() { + // A store of its own: this test demotes a member whose id other tests' stand-ins share. + let store = wai_quantum::quantum_landscape::member::Demotions::open(None); + let m = Declared(Fault::RestBlind, Capacity::NONE); + let c = wai_quantum::quantum_landscape::member::certify_with(&m, &store).unwrap(); + assert_eq!(grades(&c), (Parity::BitExact, Parity::BitExact), "certification never runs a bound past 65,536"); + let placed = [c, Certified::reference()]; + let a = job::call_placed(&placed, "run", &json!({ "program": path_program(30, 1_000_000) }), MAX_JOB_BYTES).unwrap(); + println!("PROBE r3 settle audit: demoted: {:?}; answered by {}", placed[0].demoted(), a["member"]["id"]); + assert!(placed[0].demoted().is_some() && a["member"]["id"] == "cpu:reference"); +} + +/// 16, 17: the audit draws with the host's key and afresh each time, and every +/// audited answer says what it checked (and, since #678 round 4, no digest of a +/// key that nothing could open). +#[test] +fn review3_the_audit_is_keyed_and_drawn_afresh() { + let m = Declared(Fault::DeclaresFieldBound(i64::MAX), Capacity::NONE); + let c = certify(&m).unwrap(); + let req = json!({ "program": "vars 3\nagree 0 1\noppose 1 2\nsolve t0=2 restarts=64 steps=4\n", "seed": "9" }); + let mut seen = Vec::new(); + for _ in 0..8 { + let a = job::call_certified(&c, "run", &req, MAX_JOB_BYTES).unwrap(); + assert!(a["audit"].get("key").is_none(), "{}", a["audit"]); + seen.push(a["audit"]["checked"][0]["chains"].clone()); + } + println!("PROBE r3 keyed audit: chains checked over 8 repeats: {seen:?}"); + assert!(seen.iter().any(|s| *s != seen[0]), "the same job is drawn afresh"); + assert!(job::call("run", &req).unwrap().get("audit").is_none(), "the reference's answers are not audited, and keep their bytes"); +} + +/// 9, 11: a demotion is kept under the member's identity, outlives the process +/// (a store read again from its file), and certifying again or declaring another +/// capacity does not clear it; another identity is certified afresh. +#[test] +fn review3_a_demotion_outlives_the_host() { + use wai_quantum::quantum_landscape::member::{Demotions, certify_with}; + let path = std::env::temp_dir().join(format!("wai-landscape-demotions-{}.json", std::process::id())); + let _ = std::fs::remove_file(&path); + let store = Demotions::open(Some(path.clone())); + let sleeper = Sleeper::new(Fault::Fields16, "gpu:sleeper-r3-durable"); + let c = certify_with(&sleeper, &store).unwrap(); + sleeper.1.set(true); + let job35k = json!({ "program": "vars 3\nfield 0 35000\nagree 0 1\nagree 1 2\nminimize steps=1\n" }); + job::call_certified(&c, "run", &job35k, MAX_JOB_BYTES).unwrap(); + assert!(c.demoted().is_some()); + // The host starts again: its store read back from the file. + let again = Demotions::open(Some(path.clone())); + sleeper.1.set(false); + let c = certify_with(&sleeper, &again).unwrap(); + assert_eq!(grades(&c), (Parity::Uncertified, Parity::Uncertified), "not certified again"); + assert!(c.demoted().is_some() && c.label().contains("demoted it to uncertified")); + // Another declared capacity does not clear it. + let wider = Declared(Fault::DeclaresFieldBound(i64::MAX), Capacity { cells: Some(64), ..Capacity::NONE }); + struct Same<'a>(&'a Declared); + impl Member for Same<'_> { + fn id(&self) -> String { + "gpu:sleeper-r3-durable".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, s: &[i8], b: u32) -> Result { + self.0.settle(w, s, b) + } + fn chains(&self, w: &Workload, c: &[Chain], t: &[f64]) -> Result, String> { + self.0.chains(w, c, t) + } + fn capacity(&self) -> Capacity { + self.0.capacity() + } + } + let same = Same(&wider); + let c = certify_with(&same, &again).unwrap(); + assert_eq!(grades(&c), (Parity::Uncertified, Parity::Uncertified), "another capacity does not clear a demotion"); + // Another identity is certified afresh. + let fresh = Sleeper::new(Fault::Fields16, "gpu:sleeper-r3-durable-new-bitstream"); + let c = certify_with(&fresh, &again).unwrap(); + assert_eq!(grades(&c), (Parity::BitExact, Parity::BitExact)); + println!("PROBE r3 durable demotion: kept across a restart of the store, and another capacity: true"); + let _ = std::fs::remove_file(&path); +} + +/// 6, 10: a kernel graded statistical is placed only where the job asks for it. +#[test] +fn review3_a_statistical_kernel_is_placed_only_when_asked() { + let m = Windowed(floor()); + let c = certify(&m).unwrap(); + assert_eq!(c.parity(KERNEL_SAMPLE), Parity::Statistical); + let placed = [c, Certified::reference()]; + let program = "vars 3\nagree 0 1\noppose 1 2\nsolve t0=2 restarts=2 steps=6\n"; + let a = job::call_placed(&placed, "run", &json!({ "program": program }), MAX_JOB_BYTES).unwrap(); + assert_eq!(a["member"]["id"], "cpu:reference"); + assert!(a["refusals"][0]["why"].as_str().unwrap().contains("statistical"), "{}", a["refusals"]); + let b = job::call_placed(&placed, "run", &json!({ "program": program, "parity": "statistical" }), MAX_JOB_BYTES).unwrap(); + assert_eq!((b["member"]["id"].as_str(), b["member"]["parity"].as_str()), (Some("gpu:windowed"), Some("statistical"))); + println!("PROBE r3 statistical placement: by default {}, asked {}", a["member"]["id"], b["member"]["id"]); +} + +/// 12, 13: the battery holds exactly the declared cell count, and with no capacity reaches past 24 cells. +#[test] +fn review3_the_battery_reaches_every_cell() { + for (what, k, cap) in [("cells 64, the last stuck", 63, Capacity { cells: Some(64), ..Capacity::NONE }), ("cells 40, the last stuck", 39, Capacity { cells: Some(40), ..Capacity::NONE }), ("no capacity, cells 32 on stuck", 32, Capacity::NONE)] { + let m = Stuck(k, cap); + let c = certify(&m).unwrap(); + println!("PROBE r3 battery {what}: {:?}", grades(&c)); + assert_eq!(c.parity(KERNEL_SAMPLE), Parity::Uncertified, "{what}"); + } +} + +/// 3, 4, 5, 14: generation scaled to the capacity. +#[test] +fn review3_generation_reaches_within_the_capacity() { + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + let finder = Capacity { at_zero: lanes(Some(i64::from(i32::MAX)), Some(i64::from(i32::MAX)), Some(1000)), above_zero: lanes(Some(i64::from(i32::MAX)), Some(i64::from(i32::MAX)), Some(1000)), ..floor() }; + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(i64::from(i32::MAX)), None, None), above_zero: lanes(Some(i64::from(i32::MAX)), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let cuda = Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }; + each_loses(&[ + ("cells 64", Fault::ColdTileRemainder, KERNEL_SAMPLE, Capacity { cells: Some(64), ..Capacity::NONE }), + ("cells 64 over the floor", Fault::ColdTileRemainder, KERNEL_SAMPLE, Capacity { cells: Some(64), ..floor() }), + ("the FPGA's", Fault::ColdTileRemainder, KERNEL_SAMPLE, fpga), + ("the finder's: the floor with 32-bit lanes and a cost of 1,000", Fault::MixedFieldCostWrap, KERNEL_SAMPLE, finder), + ("the finder's with a batch of 4", Fault::MixedFieldCostWrap, KERNEL_SAMPLE, Capacity { chains_per_call: Some(4), ..finder }), + ("no capacity", Fault::SettleEndsLate(400), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::SettleEndsLate(1024), KERNEL_SETTLE, Capacity::NONE), + ("CUDA's and Metal's", Fault::SettleEndsLate(1024), KERNEL_SETTLE, cuda), + ("the WebGPU member's numbers", Fault::SettleEndsLate(1024), KERNEL_SETTLE, gpu), + ("no capacity", Fault::CouplingSum8, KERNEL_SETTLE, Capacity::NONE), + ("the WebGPU member's numbers", Fault::CouplingSum8, KERNEL_SETTLE, gpu), + ]); +} + +// ── the fourth review's probes (#678 round 4), kept ──────────────────────────────────────── + +/// 1, 6: a sweep at 0 straight after hot ones (an engine's final sweep) that +/// leaves the end of a partial last workgroup of 64, or warp of 32, unmoved +/// loses its grade under every capacity that lets a workload reach the band. +#[test] +fn review4_a_sweep_at_0_after_hot_ones_in_a_partial_group() { + let wide = i64::from(i32::MAX); + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let lanes_only = Capacity { at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), ..Capacity::NONE }; + let cuda = Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }; + let mut cases = Vec::new(); + for f in [Fault::Final64, Fault::Final32] { + cases.extend([ + ("no capacity", f, KERNEL_SAMPLE, Capacity::NONE), + ("the FPGA's", f, KERNEL_SAMPLE, fpga), + ("the WebGPU member's numbers", f, KERNEL_SAMPLE, gpu), + ("32-bit lanes alone", f, KERNEL_SAMPLE, lanes_only), + ("CUDA's and Metal's", f, KERNEL_SAMPLE, cuda), + ("cells 64 over the floor", f, KERNEL_SAMPLE, Capacity { cells: Some(64), ..floor() }), + ("cells 100", f, KERNEL_SAMPLE, Capacity { cells: Some(100), ..Capacity::NONE }), + ]); + } + cases.push(("the floor", Fault::Final32, KERNEL_SAMPLE, floor())); + each_loses(&cases); + // A clean kernel keeps its grade. + for cap in [Capacity::NONE, fpga] { + assert!(holds(&Declared(Fault::DeclaresFieldBound(i64::MAX), cap), KERNEL_SAMPLE), "{}", cap.json()); + } +} + +/// 2, 5, 6: under a declared sweep limit the wave comes to rest within it; and a +/// long settle stopped at its bound, its last sweep moving, is in the set: a +/// settle-length fault, a result slot read at a narrow sweep count, a state one +/// sweep back, and a settle that stops moving and claims its bound all lose the grade. +#[test] +fn review4_long_settles_within_a_sweep_limit_and_at_their_bound() { + let sweeps = |s: u64| Capacity { sweeps: Some(s), ..Capacity::NONE }; + let floor_unbounded = Capacity { cells: None, couplings: None, ..floor() }; + let cuda = Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }; + each_loses(&[ + ("sweeps 5000, the floor's", Fault::SettleEndsLate(1024), KERNEL_SETTLE, sweeps(5000)), + ("sweeps 7000", Fault::SettleEndsLate(3000), KERNEL_SETTLE, sweeps(7000)), + ("sweeps 8193", Fault::Truncate(1024), KERNEL_SETTLE, sweeps(8193)), + ("cells 4095 and sweeps 5000", Fault::Truncate(400), KERNEL_SETTLE, Capacity { cells: Some(4095), sweeps: Some(5000), ..Capacity::NONE }), + ("couplings 8000 and sweeps 5000", Fault::Truncate(1024), KERNEL_SETTLE, Capacity { couplings: Some(8000), sweeps: Some(5000), ..Capacity::NONE }), + ("the floor with no cell count", Fault::SettleEndsLate(1024), KERNEL_SETTLE, floor_unbounded), + ("no capacity", Fault::Ring9, KERNEL_SETTLE, Capacity::NONE), + ("CUDA's and Metal's", Fault::Ring9, KERNEL_SETTLE, cuda), + ("cells 4095", Fault::Ring9, KERNEL_SETTLE, Capacity { cells: Some(4095), ..Capacity::NONE }), + ("sweeps 7000", Fault::Ring9, KERNEL_SETTLE, sweeps(7000)), + ("no capacity", Fault::Stale(130), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::Stale(4000), KERNEL_SETTLE, Capacity::NONE), + ("cells 600", Fault::Stale(500), KERNEL_SETTLE, Capacity { cells: Some(600), ..Capacity::NONE }), + ]); + for cap in [sweeps(5000), floor_unbounded, Capacity::NONE] { + assert!(holds(&Declared(Fault::DeclaresFieldBound(i64::MAX), cap), KERNEL_SETTLE), "{}", cap.json()); + } +} + +/// A member that keeps to the reference until armed, and then, in every call of a +/// job after its first, answers each chain but the call's last with the last's +/// outcome: the cheat of a member that, timing the host, knows the run's checked +/// chain was in its first call. +struct AfterTheFirstCall(Cell, Cell, Capacity); + +impl Member for AfterTheFirstCall { + fn id(&self) -> String { + "gpu:after-the-first-call".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + CpuReference.settle(w, start, bound) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + let call = self.1.get(); + self.1.set(call + 1); + if !self.0.get() || call == 0 { + return CpuReference.chains(w, chains, temperatures); + } + let last = chains.last().map(|c| anneal_with(w, &c.start, temperatures, c.seed)).ok_or("no chains")?; + Ok(vec![last; chains.len()]) + } + fn capacity(&self) -> Capacity { + self.2 + } +} + +/// 3: the audit draws in every call, so a member that knows where one call's +/// check fell learns nothing of the next: each call of a run has a chain of it +/// checked, drawn afresh, and the cheat above is demoted on its first job. +#[test] +fn review4_the_audit_draws_in_every_call() { + use wai_quantum::quantum_landscape::member::{Demotions, certify_with}; + // A ring of 32 cells, its couplings mixed, with chords: chains from different + // starts come to rest in different states, so a copied chain is a wrong one. + let mut l = Landscape::new(32); + for i in 0..32 { + l.couple(i, (i + 1) % 32, if i % 3 == 0 { -1 } else { 1 }); + if i % 4 == 0 { + l.couple(i, (i + 7) % 32, -1); + } + } + let program = program_of(&l, "solve t0=2 restarts=128 steps=4\n"); + let program = program.as_str(); + let cap = Capacity { chains_per_call: Some(32), ..floor() }; + // Every call has a chain of the run checked, and over repeats not only its last. + let honest = Declared(Fault::DeclaresFieldBound(i64::MAX), cap); + let c = certify(&honest).unwrap(); + let mut drawn_before_last = [false; 4]; + for _ in 0..8 { + let a = job::call_certified(&c, "run", &json!({ "program": program, "seed": "5" }), MAX_JOB_BYTES).unwrap(); + let checked: Vec = a["audit"]["checked"][0]["chains"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect(); + for call in 0..4u64 { + assert!(checked.contains(&(32 * call + 31)), "the last chain of call {call}: {checked:?}"); + drawn_before_last[call as usize] |= checked.iter().any(|&k| (32 * call..32 * call + 31).contains(&k)); + } + } + assert!(drawn_before_last.iter().all(|&d| d), "each call's draw falls before its last chain in some repeat: {drawn_before_last:?}"); + // The cheat, certified honest, is caught on its first job every time. + for trial in 0..10 { + let store = Demotions::open(None); + let m = AfterTheFirstCall(Cell::new(false), Cell::new(0), cap); + let c = certify_with(&m, &store).unwrap(); + assert_eq!(grades(&c), (Parity::BitExact, Parity::BitExact)); + m.0.set(true); + m.1.set(0); + let placed = [c, Certified::reference()]; + let a = job::call_placed(&placed, "run", &json!({ "program": program, "seed": (trial + 1).to_string() }), MAX_JOB_BYTES).unwrap(); + assert!(placed[0].demoted().is_some() && a["member"]["id"] == "cpu:reference", "trial {trial}: {}", a["member"]); + } + println!("PROBE r4 per-call draws: every call checked, the after-the-first-call cheat demoted on its first job in 10 of 10"); +} + +/// The reference under a name of its own, declaring the floor: quick to certify. +struct Called(&'static str); + +impl Member for Called { + fn id(&self) -> String { + self.0.into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + CpuReference.settle(w, start, bound) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + CpuReference.chains(w, chains, temperatures) + } + fn capacity(&self) -> Capacity { + floor() + } +} + +/// 4, 7, 8, 21: the store fails closed. A file that does not parse or cannot be +/// read, a folder that cannot be written, and a host with no file to name grade no +/// kernel bit-exact, and say why; a record that does not parse as a reason still demotes. +#[test] +fn review4_the_store_fails_closed() { + use wai_quantum::quantum_landscape::member::{Demotions, certify_with}; + let dir = std::env::temp_dir().join(format!("wai-landscape-r4-store-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let held = |store: &Demotions, id: &'static str| { + let m = Called(id); + let c = certify_with(&m, store).unwrap(); + (grades(&c), c.demoted()) + }; + // Positive control: an intact file, and no file yet, certify afresh. + let good = dir.join("good.json"); + let (g, d) = held(&Demotions::open(Some(good.clone())), "gpu:r4-store-a"); + assert_eq!((g, d), ((Parity::BitExact, Parity::BitExact), None)); + let bad_text: [(&str, &[u8]); 5] = [("truncated", b"{\"gpu:x \": {\"why\": \"a reason\""), ("empty", b""), ("a byte-order mark", b"\xEF\xBB\xBF{}"), ("not utf-8", b"{\"\xFF\": 1}"), ("junk after it", b"{}\nx")]; + for (what, bytes) in bad_text { + let p = dir.join(format!("{}.json", what.replace(' ', "-"))); + std::fs::write(&p, bytes).unwrap(); + let store = Demotions::open(Some(p.clone())); + let (g, d) = held(&store, "gpu:r4-store-a"); + println!("PROBE r4 store {what}: {g:?} {d:?}"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "{what}"); + assert!(d.unwrap().contains("cannot keep its demotions"), "{what}"); + // A demotion on it neither erases nor rewrites the file. + assert!(store.record("gpu:r4-store-b ", "a probe", &Capacity::NONE).is_err(), "{what}"); + assert_eq!(std::fs::read(&p).unwrap(), bytes, "{what}: the file is as it was"); + } + // A record whose reason is not a string still demotes. + let odd = dir.join("odd.json"); + std::fs::write(&odd, br#"{"gpu:r4-store-c ": {"why": 7}}"#).unwrap(); + let (g, d) = held(&Demotions::open(Some(odd)), "gpu:r4-store-c"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + assert!(d.is_some()); + // A path under a regular file cannot be read or written. + let under = dir.join("good.json.d").join("x.json"); + std::fs::write(dir.join("good.json.d"), b"a file").unwrap(); + let (g, _) = held(&Demotions::open(Some(under)), "gpu:r4-store-a"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "a path under a file"); + // A host with no file to name. + let (g, d) = held(&Demotions::broken("no HOME".into()), "gpu:r4-store-a"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + assert!(d.unwrap().contains("no HOME")); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + // Unreadable, where this user cannot read it (not as root). + let p = dir.join("unreadable.json"); + std::fs::write(&p, b"{}").unwrap(); + std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o000)).unwrap(); + if std::fs::read(&p).is_err() { + let (g, _) = held(&Demotions::open(Some(p.clone())), "gpu:r4-store-a"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "unreadable"); + } + std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o644)).unwrap(); + // A folder that cannot be written. + let ro = dir.join("read-only"); + std::fs::create_dir_all(&ro).unwrap(); + std::fs::set_permissions(&ro, std::fs::Permissions::from_mode(0o555)).unwrap(); + if std::fs::write(ro.join("probe"), b"").is_err() { + let (g, _) = held(&Demotions::open(Some(ro.join("demotions.json"))), "gpu:r4-store-a"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "a folder that cannot be written"); + } + std::fs::set_permissions(&ro, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + let _ = std::fs::remove_dir_all(&dir); +} + +/// 4, 21: stores that share a file keep one another's demotions: one opened before +/// another demotes sees it, and writers at once lose none. +#[test] +fn review4_stores_sharing_a_file_keep_each_other_s_demotions() { + use wai_quantum::quantum_landscape::member::{Demotions, certify_with}; + let dir = std::env::temp_dir().join(format!("wai-landscape-r4-shared-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let path = dir.join("demotions.json"); + let (one, two) = (Demotions::open(Some(path.clone())), Demotions::open(Some(path.clone()))); + let (x, y) = (Called("gpu:r4-shared-x"), Called("gpu:r4-shared-y")); + // Both certified before either demotes. + let (cx, cy) = (certify_with(&x, &one).unwrap(), certify_with(&y, &two).unwrap()); + cx.demote("x, by the first host".into()); + cy.demote("y, by the second".into()); + // The second host, opened before the first demoted x, does not certify x again. + let again = certify_with(&x, &two).unwrap(); + assert_eq!(grades(&again), (Parity::Uncertified, Parity::Uncertified), "a demotion another store made"); + let third = Demotions::open(Some(path.clone())); + for (m, id) in [(&x, "x"), (&y, "y")] { + assert_eq!(grades(&certify_with(m, &third).unwrap()), (Parity::Uncertified, Parity::Uncertified), "{id}"); + } + // Writers at once, each with a store of its own: none is lost. + std::thread::scope(|s| { + for t in 0..4 { + let path = path.clone(); + s.spawn(move || { + let store = Demotions::open(Some(path)); + for k in 0..30 { + store.record(&format!("gpu:r4-many-{t}-{k} fp"), "a probe", &Capacity::NONE).unwrap(); + } + }); + } + }); + let text = std::fs::read_to_string(&path).unwrap(); + let kept: serde_json::Map = serde_json::from_str(&text).unwrap(); + let many = kept.keys().filter(|k| k.starts_with("gpu:r4-many-")).count(); + println!("PROBE r4 shared store: {many} of 120 demotions written at once kept, and x and y both"); + assert_eq!(many, 120); + let _ = std::fs::remove_dir_all(&dir); +} + +/// 15: `"parity": null` is the default, as every optional field's null is. +#[test] +fn review4_a_null_parity_is_the_default() { + let program = "vars 3\nagree 0 1\noppose 1 2\nsolve t0=2 restarts=2 steps=6\n"; + let a = job::call("run", &json!({ "program": program, "parity": null })).unwrap(); + assert_eq!(a, job::call("run", &json!({ "program": program })).unwrap()); + assert!(job::call("run", &json!({ "program": program, "parity": 7 })).is_err()); +} + +/// The reference's chains from seeds of its own: a correct sampler with draws of its own. +struct OwnDraws(u64, Capacity); + +impl Member for OwnDraws { + fn id(&self) -> String { + "gpu:own-draws".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + CpuReference.settle(w, start, bound) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + Ok(chains.iter().map(|c| anneal_with(w, &c.start, temperatures, c.seed ^ self.0)).collect()) + } + fn capacity(&self) -> Capacity { + self.1 + } +} + +/// 12, 13, 14, 18: the battery's alike cells follow a declared cell count to +/// 4,096; the uncertain span is reached where a table or thin couplings are the +/// narrower; and the workloads a capacity adds pass a correct sampler with draws +/// of its own, the reviewer's among them. +#[test] +fn review4_the_battery_and_the_span_within_the_capacity() { + let wide = i64::from(i32::MAX); + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + for (what, k, cap) in [("cells 4096, from 1,023 on stuck", 1023, Capacity { cells: Some(4096), ..Capacity::NONE }), ("the WebGPU member's numbers, from 1,024 on stuck", 1024, gpu), ("the WebGPU member's numbers, from 2,048 on stuck", 2048, gpu)] { + let m = Stuck(k, cap); + let c = certify(&m).unwrap(); + println!("PROBE r4 battery {what}: {:?}", grades(&c)); + assert_eq!(c.parity(KERNEL_SAMPLE), Parity::Uncertified, "{what}"); + } + for (what, xor, cap) in [("the WebGPU member's numbers, the reviewer's draws", 0x5DEE_CE66_D1CE_4E5Bu64, gpu), ("a field bound declared", 0x5DEE_CE66_D1CE_4E5B, Capacity { above_zero: lanes(Some(1000), None, None), ..Capacity::NONE }), ("couplings and sweeps declared", 0x5EED_5EED, Capacity { couplings: Some(3000), sweeps: Some(6000), ..Capacity::NONE })] { + let m = OwnDraws(xor, cap); + let c = certify(&m).unwrap(); + println!("PROBE r4 own draws, {what}: {:?}", grades(&c)); + assert_eq!(c.parity(KERNEL_SAMPLE), Parity::Statistical, "{what}"); + } + let c1 = Capacity { couplings: Some(3058), at_zero: lanes(None, Some(46), Some(8)), above_zero: lanes(None, Some(40), Some(996)), table_entries: Some(401), uncertain_span: Some(412), chains_per_call: Some(3788), ..Capacity::NONE }; + let c2 = Capacity { cells: Some(170), couplings: Some(2032), above_zero: lanes(Some(4192), Some(40), Some(8)), ..Capacity::NONE }; + let c3 = Capacity { above_zero: lanes(None, Some(40), None), table_entries: Some(401), ..Capacity::NONE }; + each_loses(&[ + ("a table under the declared span", Fault::SpanUpTo(81), KERNEL_SAMPLE, c1), + ("thin couplings and no span", Fault::SpanUpTo(81), KERNEL_SAMPLE, c2), + ("a table and no span", Fault::SpanUpTo(261), KERNEL_SAMPLE, c3), + ]); +} + +// ── the fifth review's probes (#678 round 5), kept ───────────────────────────────────────── + +/// 1: at each certification the store proves its folder takes a rewrite of its +/// file. A folder that stops taking writes after its first use (its lock already +/// there), and a limit on a file's size (`ulimit -f 0`, as a full disk or a quota +/// would), grade nothing bit-exact. +#[test] +fn review5_the_store_fails_closed_when_its_folder_stops_taking_writes() { + use wai_quantum::quantum_landscape::member::{Demotions, certify_with}; + if let Some(path) = std::env::var_os("WAI_R5_STORE_CHILD") { + let store = Demotions::open(Some(std::path::PathBuf::from(path))); + let m = Called("gpu:r5-limited"); + let c = certify_with(&m, &store).unwrap(); + println!("R5CHILD {:?} {:?}", grades(&c), c.demoted()); + return; + } + let dir = std::env::temp_dir().join(format!("wai-landscape-r5-store-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("demotions.json"); + let m = Called("gpu:r5-store"); + assert_eq!(grades(&certify_with(&m, &Demotions::open(Some(path.clone()))).unwrap()), (Parity::BitExact, Parity::BitExact), "positive control"); + assert!(dir.join("demotions.json.lock").exists(), "a first use leaves the lock"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o555)).unwrap(); + if std::fs::write(dir.join("probe"), b"").is_err() { + let store = Demotions::open(Some(path.clone())); + let c = certify_with(&m, &store).unwrap(); + println!("PROBE r5 store, folder unwritable after its first use: {:?} {:?}", grades(&c), c.demoted()); + assert_eq!(grades(&c), (Parity::Uncertified, Parity::Uncertified)); + assert!(c.demoted().unwrap().contains("does not take a rewrite")); + } + std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap(); + // A limit on a file's size, in a process of its own. + let exe = std::env::current_exe().unwrap(); + let out = std::process::Command::new("sh") + .arg("-c") + .arg("trap '' XFSZ; ulimit -f 0; exec \"$0\" --exact review5_the_store_fails_closed_when_its_folder_stops_taking_writes --nocapture --test-threads=1") + .arg(&exe) + .env("WAI_R5_STORE_CHILD", &path) + .output() + .unwrap(); + let text = String::from_utf8_lossy(&out.stdout); + let line = text.lines().find_map(|l| l.find("R5CHILD").map(|at| &l[at..])).unwrap_or_else(|| panic!("the child said nothing: {text} {}", String::from_utf8_lossy(&out.stderr))); + println!("PROBE r5 store, a file size limit of 0: {line}"); + assert!(line.contains("(Uncertified, Uncertified)") && line.contains("does not take a rewrite"), "{line}"); + } + let _ = std::fs::remove_dir_all(&dir); +} + +/// 7, 8, 9, 10: a path naming no file is a broken store; a linked file is its +/// target's, and a link to nothing fails closed; a lock held past the wait grades +/// that certification uncertified and not the next; a rewrite keeps the file's +/// mode and removes what a crashed writer left. +#[test] +fn review5_the_store_follows_links_waits_a_bounded_time_and_cleans_up() { + use wai_quantum::quantum_landscape::member::{Demotions, STORE_LOCK_WAIT, certify_with}; + let m = Called("gpu:r5-links"); + let held = |store: &Demotions| { + let c = certify_with(&m, store).unwrap(); + (grades(&c), c.demoted()) + }; + let (g, d) = held(&Demotions::open(Some(std::path::PathBuf::new()))); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "an empty path"); + assert!(d.unwrap().contains("names no file")); + let dir = std::env::temp_dir().join(format!("wai-landscape-r5-links-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(dir.join("share")).unwrap(); + std::fs::create_dir_all(dir.join("host")).unwrap(); + let target = dir.join("share").join("demotions.json"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let link = dir.join("host").join("demotions.json"); + std::os::unix::fs::symlink(&target, &link).unwrap(); + let shared = Demotions::open(Some(target.clone())); + shared.record("gpu:r5-links ", "by the target's host", &Capacity::NONE).unwrap(); + let (g, _) = held(&Demotions::open(Some(link.clone()))); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "the link's host reads the target's demotions"); + let through = Demotions::open(Some(link.clone())); + through.record("gpu:r5-x ", "through the link", &Capacity::NONE).unwrap(); + assert!(std::fs::symlink_metadata(&link).unwrap().file_type().is_symlink(), "the link is still a link"); + assert!(std::fs::read_to_string(&target).unwrap().contains("gpu:r5-x "), "the target holds the demotion made through the link"); + // A link to nothing. + let dangling = dir.join("host").join("dangling.json"); + std::os::unix::fs::symlink(dir.join("not-mounted").join("demotions.json"), &dangling).unwrap(); + let (g, d) = held(&Demotions::open(Some(dangling.clone()))); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "a link to nothing"); + assert!(d.unwrap().contains("link to nothing")); + assert!(std::fs::symlink_metadata(&dangling).unwrap().file_type().is_symlink()); + // The mode kept, and a crashed writer's file removed. + std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap(); + let left = dir.join("share").join(".demotions.json.write.999999.0.tmp"); + std::fs::write(&left, b"half a write").unwrap(); + // A killed writer's file, unchanged for an hour and held by no one. + aged(&left); + shared.record("gpu:r5-y ", "a probe", &Capacity::NONE).unwrap(); + assert!(!left.exists(), "a crashed writer's file is removed"); + assert_eq!(std::fs::metadata(&target).unwrap().permissions().mode() & 0o777, 0o600, "the file's mode is kept"); + let strays: Vec<_> = std::fs::read_dir(dir.join("share")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).filter(|f| f.ends_with(".tmp")).collect(); + assert!(strays.is_empty(), "{strays:?}"); + } + // A lock held past the wait. + let busy = dir.join("busy.json"); + let lock = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(dir.join("busy.json.lock")).unwrap(); + lock.lock().unwrap(); + let store = Demotions::open(Some(busy.clone())); + let t = std::time::Instant::now(); + let (g, d) = held(&store); + let waited = t.elapsed(); + println!("PROBE r5 store, a lock held: {g:?} after {waited:?}: {d:?}"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + assert!(waited >= STORE_LOCK_WAIT && waited < STORE_LOCK_WAIT * 4, "{waited:?}"); + assert!(d.unwrap().contains("held past")); + lock.unlock().unwrap(); + assert_eq!(held(&store).0, (Parity::BitExact, Parity::BitExact), "the next certification, the lock free, is not held back"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// 2, 11: long moving settles are in the set: the wave stopped one sweep before +/// it would rest, and units of cells that keep moving, stopped past 2⁹ to 2¹³ +/// sweeps and at their last moving sweep. +#[test] +fn review5_the_longest_moving_settles_are_held() { + let sweeps = |s: u64| Capacity { sweeps: Some(s), ..Capacity::NONE }; + let wide = i64::from(i32::MAX); + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let cuda = Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }; + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + each_loses(&[ + ("no capacity", Fault::Stale(4095), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::Stale(6000), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::Ring12, KERNEL_SETTLE, Capacity::NONE), + ("CUDA's and Metal's", Fault::Stale(4095), KERNEL_SETTLE, cuda), + ("CUDA's and Metal's", Fault::Ring12, KERNEL_SETTLE, cuda), + ("the WebGPU member's numbers", Fault::Ring12, KERNEL_SETTLE, gpu), + ("sweeps 8193", Fault::Stale(4095), KERNEL_SETTLE, sweeps(8193)), + ("sweeps 8193", Fault::Ring12, KERNEL_SETTLE, sweeps(8193)), + ("cells 1500", Fault::Stale(1499), KERNEL_SETTLE, Capacity { cells: Some(1500), ..Capacity::NONE }), + ("cells 600", Fault::Stale(600), KERNEL_SETTLE, Capacity { cells: Some(600), ..Capacity::NONE }), + ("the FPGA's", Fault::Stale(63), KERNEL_SETTLE, fpga), + ]); + for cap in [Capacity::NONE, fpga] { + assert!(holds(&Declared(Fault::DeclaresFieldBound(i64::MAX), cap), KERNEL_SETTLE), "{}", cap.json()); + } +} + +/// 12: the final sweep at 0 after sweeps above 0, at even and middle remainders +/// of a partial workgroup of 64 and warp of 32, after a cold sweep, after three +/// hot ones, and at an odd index. +#[test] +fn review5_the_final_sweep_at_every_remainder() { + let wide = i64::from(i32::MAX); + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let cuda = Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }; + let caps = [("no capacity", Capacity::NONE), ("the FPGA's", fpga), ("the WebGPU member's numbers", gpu), ("CUDA's and Metal's", cuda), ("cells 64 over the floor", Capacity { cells: Some(64), ..floor() }), ("cells 4000", Capacity { cells: Some(4000), ..Capacity::NONE })]; + let mut cases = Vec::new(); + for f in [Fault::Final64Even, Fault::Final64Mid, Fault::Final64AfterCold, Fault::Final64AfterThreeHot, Fault::Final64OddIndex, Fault::Final32Even] { + for (what, cap) in caps { + cases.push((what, f, KERNEL_SAMPLE, cap)); + } + } + cases.push(("the floor", Fault::Final32Even, KERNEL_SAMPLE, floor())); + each_loses(&cases); + for cap in [Capacity::NONE, fpga] { + assert!(holds(&Declared(Fault::DeclaresFieldBound(i64::MAX), cap), KERNEL_SAMPLE), "{}", cap.json()); + } +} + +/// How a sampler with draws of its own goes wrong, if it does. +#[derive(Clone, Copy, Debug)] +enum Flaw { + None, + /// Cells past lane 32 of a partial last workgroup of 64 never move. + Tail64, + /// A call's last partial warp of more than 16 chains: lanes 16 on at their starts. + CallTail32, + /// Cells from this one on never move. + FrozenFrom(usize), + /// Cells from this one on drawn at random, each state alike. + CoinFrom(usize), + /// These cells never move. + Band(usize, usize), + /// A call's last partial workgroup holding 34 to 62 chains: lanes 32 on at their starts. + CallTail64Mid, + /// A call's last partial warp holding an even count of more than 16: lanes 16 on at their starts. + CallTail32Even, + /// [`Flaw::Tail64`] only where the partial workgroup holds an even count. + Tail64Even, + /// Cells from this one on drawn at this many times each sweep's temperature. + WarmFrom(usize, f64), +} + +/// The reference's chains from seeds of its own, with a flaw. +struct Flawed(Flaw, Capacity); + +impl Member for Flawed { + fn id(&self) -> String { + "gpu:flawed".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + CpuReference.settle(w, start, bound) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + let n = w.n(); + let warp_start = chains.len() - (chains.len() - 1) % 32 - 1; + let partial = chains.len() % 32 > 16; + let (tile, warp) = (chains.len() % 64, chains.len() % 32); + Ok(chains + .iter() + .enumerate() + .map(|(k, c)| { + let mut o = anneal_with(w, &c.start, temperatures, c.seed ^ 0x5EED_5EED); + if let Flaw::WarmFrom(a, f) = self.0 { + let warm: Vec = temperatures.iter().map(|t| t * f).collect(); + let hot = anneal_with(w, &c.start, &warm, c.seed ^ 0x5EED_5EED); + o.state[a.min(n)..].copy_from_slice(&hot.state[a.min(n)..]); + } + let keep = |i: usize| match self.0 { + Flaw::Tail64 => in_the_tile_gap(n, i), + Flaw::Tail64Even => in_the_tile_gap(n, i) && n % 2 == 0, + Flaw::CallTail32 => partial && k >= warp_start + 16, + Flaw::CallTail64Mid => (34..=62).contains(&tile) && k >= chains.len() - tile + 32, + Flaw::CallTail32Even => warp > 16 && warp % 2 == 0 && k >= chains.len() - warp + 16, + Flaw::FrozenFrom(a) => i >= a, + Flaw::Band(a, b) => (a..b).contains(&i), + _ => false, + }; + for i in 0..n { + if keep(i) { + o.state[i] = c.start[i]; + } else if matches!(self.0, Flaw::CoinFrom(a) if i >= a) { + o.state[i] = (out(c.seed, i as u64 + 7) % 3) as i8 - 1; + } + } + o.term = if w.rests(&o.state) { Term::FixedPoint } else { Term::Bound }; + o + }) + .collect()) + } + fn capacity(&self) -> Capacity { + self.1 + } +} + +/// 3, 14: a sampler with draws of its own that leaves cells unmoved or draws them +/// from another law, past a remainder, at the end of a call, past cell 1,023 or +/// in a band between powers of two, loses `statistical`; a correct one keeps it. +#[test] +fn review5_statistical_samplers_are_held_on_every_cell() { + let wide = i64::from(i32::MAX); + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let cells = |c: u64| Capacity { cells: Some(c), ..Capacity::NONE }; + for (what, flaw, cap, want) in [ + ("correct, no capacity", Flaw::None, Capacity::NONE, Parity::Statistical), + ("correct, the FPGA's", Flaw::None, fpga, Parity::Statistical), + ("correct, the WebGPU member's numbers", Flaw::None, gpu, Parity::Statistical), + ("a partial workgroup's tail, cells 2048", Flaw::Tail64, cells(2048), Parity::Uncertified), + ("a partial workgroup's tail, the FPGA's", Flaw::Tail64, fpga, Parity::Uncertified), + ("a partial workgroup's tail, the WebGPU member's numbers", Flaw::Tail64, gpu, Parity::Uncertified), + ("a call's partial warp, no capacity", Flaw::CallTail32, Capacity::NONE, Parity::Uncertified), + ("a call's partial warp, the floor", Flaw::CallTail32, floor(), Parity::Uncertified), + ("a call's partial warp, the FPGA's", Flaw::CallTail32, fpga, Parity::Uncertified), + ("frozen from cell 1,023, no capacity", Flaw::FrozenFrom(1023), Capacity::NONE, Parity::Uncertified), + ("drawn at random from cell 1,024, no capacity", Flaw::CoinFrom(1024), Capacity::NONE, Parity::Uncertified), + ("cell 100 unmoved, no capacity", Flaw::Band(100, 101), Capacity::NONE, Parity::Uncertified), + ("cell 100 unmoved, the WebGPU member's numbers", Flaw::Band(100, 101), gpu, Parity::Uncertified), + ("cells 1,500 to 1,520 unmoved, cells 2000", Flaw::Band(1500, 1520), cells(2000), Parity::Uncertified), + ] { + let m = Flawed(flaw, cap); + let c = certify(&m).unwrap(); + println!("PROBE r5 statistical, {what}: {:?}", grades(&c)); + assert_eq!(c.parity(KERNEL_SAMPLE), want, "{what}"); + } +} + +/// The reference's kernels, answering a repeat from a cache once `on`: a member +/// faster than the reference, so that an audited replay's work is the audit's. +struct Cached(Cell, std::cell::RefCell, Vec), Outcome>>); + +impl Member for Cached { + fn id(&self) -> String { + "gpu:r5-cached".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, s: &[i8], b: u32) -> Result { + CpuReference.settle(w, s, b) + } + fn chains(&self, w: &Workload, chains: &[Chain], t: &[f64]) -> Result, String> { + if !self.0.get() { + return CpuReference.chains(w, chains, t); + } + let key: Vec = t.iter().map(|x| x.to_bits()).collect(); + let mut c = self.1.borrow_mut(); + Ok(chains.iter().map(|ch| c.entry((ch.seed, ch.start.clone(), key.clone())).or_insert_with(|| anneal_with(w, &ch.start, t, ch.seed)).clone()).collect()) + } +} + +/// 6: an honest job's throughput beside an audited replay of 256 seeds of one +/// chain each (every chain checked) on a fast member, against the same replay +/// run by name and against nothing. Times are printed. Run with `--ignored --nocapture`. +#[test] +#[ignore] +fn review5_an_audited_replay_beside_an_honest_job() { + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + let secs: f64 = std::env::var("WAI_R5_SECS").ok().and_then(|s| s.parse().ok()).unwrap_or(6.0); + let load = std::fs::read_to_string("/proc/loadavg").ok().or_else(|| std::process::Command::new("sysctl").args(["-n", "vm.loadavg"]).output().ok().map(|o| String::from_utf8_lossy(&o.stdout).into_owned())); + let mut ring = Landscape::new(64); + for i in 0..64 { + ring.couple(i, (i + 1) % 64, if i % 3 == 0 { -1 } else { 1 }).couple(i, (i + 9) % 64, 1); + } + let replay = json!({ "program": program_of(&ring, "solve t0=2 restarts=1 steps=500\n"), "seed": "1", "seeds": 256 }); + let mut fg = Landscape::new(128); + for i in 0..128 { + fg.couple(i, (i + 1) % 128, if i % 4 == 0 { -1 } else { 1 }).couple(i, (i + 17) % 128, -1); + } + let honest = json!({ "program": program_of(&fg, "solve t0=2 restarts=16 steps=300\n"), "seed": "2" }); + println!("PROBE r5 starve: {} cores, audit threads {}, load {}", std::thread::available_parallelism().map_or(1, |p| p.get()), wai_quantum::quantum_landscape::run::audit_threads(), load.as_deref().map(str::trim).unwrap_or("not given")); + for round in 0..3 { + let mut idle = 0.0; + for mode in ["idle", "by name", "audited"] { + let stop = Arc::new(AtomicBool::new(false)); + let replays = Arc::new(AtomicU64::new(0)); + let ready = Arc::new(AtomicBool::new(mode == "idle")); + let bg = (mode != "idle").then(|| { + let (stop, replays, ready, replay) = (stop.clone(), replays.clone(), ready.clone(), replay.clone()); + std::thread::spawn(move || { + let m = Cached(Cell::new(false), std::cell::RefCell::new(std::collections::HashMap::new())); + let store = wai_quantum::quantum_landscape::member::Demotions::open(None); + let c = if mode == "by name" { wai_quantum::quantum_landscape::member::named(&m).unwrap() } else { wai_quantum::quantum_landscape::member::certify_with(&m, &store).unwrap() }; + m.0.set(true); + job::call_certified(&c, "replay", &replay, MAX_JOB_BYTES).unwrap(); + ready.store(true, Ordering::SeqCst); + while !stop.load(Ordering::SeqCst) { + job::call_certified(&c, "replay", &replay, MAX_JOB_BYTES).unwrap(); + replays.fetch_add(1, Ordering::SeqCst); + } + assert!(c.demoted().is_none()); + }) + }); + while !ready.load(Ordering::SeqCst) { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + let t = std::time::Instant::now(); + let mut times = Vec::new(); + while t.elapsed().as_secs_f64() < secs { + let one = std::time::Instant::now(); + job::call("run", &honest).unwrap(); + times.push(one.elapsed().as_secs_f64()); + } + let wall = t.elapsed().as_secs_f64(); + stop.store(true, Ordering::SeqCst); + if let Some(h) = bg { + h.join().unwrap(); + } + times.sort_by(f64::total_cmp); + let rate = times.len() as f64 / wall; + if mode == "idle" { + idle = rate; + } + println!( + "PROBE r5 starve r{round} {mode:<8}: honest jobs {:.1}/s ({:+.0}% against none beside it), median {:.4} s, p90 {:.4} s; replays beside it {:.2}/s", + rate, + 100.0 * (rate / idle - 1.0), + times[times.len() / 2], + times[times.len() * 9 / 10], + replays.load(Ordering::SeqCst) as f64 / wall + ); + } + } +} + +// ── the sixth review's probes (#678 round 6), kept ───────────────────────────────────────── + +/// A file's modification time an hour back: as a killed writer's file looks. +fn aged(p: &std::path::Path) { + let f = std::fs::OpenOptions::new().write(true).open(p).unwrap(); + f.set_modified(std::time::SystemTime::now() - std::time::Duration::from_secs(3600)).unwrap(); +} + +/// A fresh folder of the system's temporary folder for one test. +fn fresh(what: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("wai-landscape-r6-{what}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +/// A member's grades on a store at `path`, and why it is held where it is. +fn graded_on(m: &dyn Member, path: &std::path::Path) -> ((Parity, Parity), Option) { + let store = wai_quantum::quantum_landscape::member::Demotions::open(Some(path.to_path_buf())); + let c = wai_quantum::quantum_landscape::member::certify_with(m, &store).unwrap(); + (grades(&c), c.demoted()) +} + +/// 1: a certification rewrites the store's file as a demotion does, renaming its +/// own file over it, so that a file it cannot replace grades nothing bit-exact: +/// one marked immutable or append-only (chflags uchg and uappnd on macOS, chattr +i +/// on Linux where sudo allows it), and another user's in a sticky folder of theirs +/// (where `sudo -u nobody` is allowed). Each is checked where the host allows it, +/// and says which; macOS always checks the flags. +#[test] +fn review6_the_store_proves_it_can_replace_its_file() { + let m = Called("gpu:r6-replace"); + let dir = fresh("replace"); + let path = dir.join("demotions.json"); + std::fs::write(&path, b"{}").unwrap(); + assert_eq!(graded_on(&m, &path).0, (Parity::BitExact, Parity::BitExact), "positive control: a file the store can replace"); + let held = |what: &str, path: &std::path::Path| { + let (g, d) = graded_on(&m, path); + println!("PROBE r6 store, {what}: {g:?} {d:?}"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified), "{what}"); + assert!(d.as_deref().is_some_and(|d| d.contains("does not take a rewrite")), "{what}: {d:?}"); + }; + let mut checked: Vec<&str> = Vec::new(); + #[cfg(any(target_vendor = "apple", target_os = "freebsd"))] + { + for flag in ["uchg", "uappnd"] { + let set = std::process::Command::new("chflags").arg(flag).arg(&path).status().is_ok_and(|s| s.success()); + assert!(set, "the owner sets {flag}"); + held(&format!("a file marked {flag}"), &path); + assert!(std::process::Command::new("chflags").arg(format!("no{flag}")).arg(&path).status().is_ok_and(|s| s.success())); + checked.push(flag); + } + } + #[cfg(target_os = "linux")] + { + if std::process::Command::new("sudo").args(["-n", "chattr", "+i"]).arg(&path).stderr(std::process::Stdio::null()).status().is_ok_and(|s| s.success()) { + let (g, d) = graded_on(&m, &path); + let _ = std::process::Command::new("sudo").args(["-n", "chattr", "-i"]).arg(&path).status(); + println!("PROBE r6 store, a file marked immutable (chattr +i): {g:?} {d:?}"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + checked.push("chattr +i"); + } + } + assert_eq!(graded_on(&m, &path).0, (Parity::BitExact, Parity::BitExact), "the flags cleared, the store is replaced again"); + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + // Another user's file in a sticky folder of theirs: the store reads and locks it, and cannot replace it. + let me = std::fs::metadata(&path).unwrap().uid(); + let nobody = |script: &str, at: &std::path::Path| std::process::Command::new("sudo").args(["-n", "-u", "nobody", "sh", "-c", script]).arg(at).stderr(std::process::Stdio::null()).status().is_ok_and(|s| s.success()); + let theirs = std::path::PathBuf::from("/tmp").join(format!("wai-landscape-r6-sticky-{}", std::process::id())); + if me != 0 && nobody("umask 0 && rm -rf \"$0\" && mkdir -m 1777 \"$0\" && printf '{}' > \"$0/demotions.json\" && : > \"$0/demotions.json.lock\"", &theirs) { + held("another user's file in a sticky folder of theirs", &theirs.join("demotions.json")); + nobody("rm -rf \"$0\"", &theirs); + checked.push("sticky"); + } + } + println!("PROBE r6 store, replacing checked against: {checked:?}"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// 2: a demotion is never dropped for a busy lock. It is kept at once in a file of +/// its own, which every certification reads; with the store's lock held past the +/// wait, it is not merged, and a later process still finds it; the next demotion +/// merges it and removes its file. +#[test] +fn review6_a_demotion_is_kept_while_the_lock_is_busy() { + use wai_quantum::quantum_landscape::member::{Demotions, certify_with}; + let dir = fresh("busy"); + let path = dir.join("demotions.json"); + let m = Called("gpu:r6-busy"); + let store = Demotions::open(Some(path.clone())); + let c = certify_with(&m, &store).unwrap(); + assert_eq!(grades(&c), (Parity::BitExact, Parity::BitExact)); + // Another holds the store's lock, shared, past the wait: a certification stopped in its read. + let lock = std::fs::OpenOptions::new().read(true).write(true).open(dir.join("demotions.json.lock")).unwrap(); + lock.lock_shared().unwrap(); + let t = std::time::Instant::now(); + c.demote("r6: other bits than the reference's".into()); + let took = t.elapsed(); + let why = c.demoted().unwrap(); + println!("PROBE r6 store, a demotion with the lock held: {took:?}: {why}"); + assert!(!why.contains("could not keep"), "{why}"); + lock.unlock().unwrap(); + // A later process finds it. + let (g, d) = graded_on(&m, &path); + println!("PROBE r6 store, a later process: {g:?} {d:?}"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + assert!(d.unwrap().contains("r6: other bits")); + // The next demotion merges it into the file. + Demotions::open(Some(path.clone())).record("gpu:r6-next ", "a probe", &Capacity::NONE).unwrap(); + let text = std::fs::read_to_string(&path).unwrap(); + assert!(text.contains("gpu:r6-busy ") && text.contains("gpu:r6-next "), "{text}"); + let own: Vec = std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).filter(|f| f.starts_with(".demotions.json.")).collect(); + assert!(own.is_empty(), "files left beside the store: {own:?}"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// 3: a store removes only its own files: never another store's whose name starts +/// with this one's and a dot, nor a file of another's it does not write, however old. +#[test] +fn review6_a_store_removes_only_its_own_files() { + use wai_quantum::quantum_landscape::member::Demotions; + let dir = fresh("own"); + let others = [ + ".demotions.json.gpu.write.424242.7.tmp", + ".demotions.json.gpu.probe.424242.8.tmp", + ".demotions.json.gpu.demotion.424242.9.tmp", + ".demotions.json.junk.tmp", + ".demotions.json.write.1.tmp", + ".demotions.json.write.x.1.tmp", + ]; + for f in others { + std::fs::write(dir.join(f), b"another store's write in progress").unwrap(); + aged(&dir.join(f)); + } + // This store's own, as a killed writer left it. + let left = dir.join(".demotions.json.write.999999.0.tmp"); + std::fs::write(&left, b"half a write").unwrap(); + aged(&left); + Demotions::open(Some(dir.join("demotions.json"))).record("gpu:r6-own ", "a probe", &Capacity::NONE).unwrap(); + // A store of a shorter name beside it takes none of the longer one's. + Demotions::open(Some(dir.join("demotions"))).record("gpu:r6-own ", "a probe", &Capacity::NONE).unwrap(); + let kept: Vec<&str> = others.iter().copied().filter(|f| dir.join(f).exists()).collect(); + println!("PROBE r6 store, files of others kept: {kept:?}; its own killed writer's removed: {}", !left.exists()); + assert_eq!(kept, others.to_vec()); + assert!(!left.exists()); + let _ = std::fs::remove_dir_all(&dir); +} + +/// A reference copy whose fingerprint is `n` bytes long. +struct LongPrint(usize); + +impl Member for LongPrint { + fn id(&self) -> String { + "gpu:r6-long".into() + } + fn class(&self) -> &'static str { + "gpu" + } + fn label(&self) -> String { + "A kernel under test.".into() + } + fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { + CpuReference.settle(w, start, bound) + } + fn chains(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result, String> { + CpuReference.chains(w, chains, temperatures) + } + fn capacity(&self) -> Capacity { + floor() + } + fn fingerprint(&self) -> String { + "f".repeat(self.0) + } +} + +/// 4: the probe is as large as the largest rewrite a demotion can make: of a store +/// written compact, and with an identity of 5,000 bytes. Under a limit on a file's +/// size that the file and 4,096 bytes fit and the rewrite does not, nothing is graded +/// bit-exact. +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[test] +fn review6_the_probe_is_as_large_as_a_rewrite() { + use std::os::unix::process::CommandExt; + if let (Some(path), Some(n)) = (std::env::var_os("WAI_R6_PROBE_CHILD"), std::env::var("WAI_R6_PROBE_PRINT").ok()) { + let (g, d) = graded_on(&LongPrint(n.parse().unwrap()), std::path::Path::new(&path)); + println!("R6CHILD {g:?} {d:?}"); + return; + } + unsafe extern "C" { + fn setrlimit(resource: i32, limits: *const [u64; 2]) -> i32; + fn signal(sig: i32, handler: usize) -> usize; + } + let under = |path: &std::path::Path, print: usize, limit: u64| { + let mut c = std::process::Command::new(std::env::current_exe().unwrap()); + c.args(["--exact", "review6_the_probe_is_as_large_as_a_rewrite", "--nocapture", "--test-threads=1"]).env("WAI_R6_PROBE_CHILD", path).env("WAI_R6_PROBE_PRINT", print.to_string()); + // SAFETY: after fork, before exec: SIGXFSZ ignored (25) and RLIMIT_FSIZE (1) + // set, so a write past the limit fails with EFBIG instead of ending the process. + unsafe { + c.pre_exec(move || { + signal(25, 1); + if setrlimit(1, &[limit, limit]) != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + let out = c.output().unwrap(); + let text = String::from_utf8_lossy(&out.stdout).into_owned(); + text.lines().find_map(|l| l.find("R6CHILD").map(|at| l[at..].to_string())).unwrap_or_else(|| panic!("the child said nothing: {text} {}", String::from_utf8_lossy(&out.stderr))) + }; + let dir = fresh("size"); + let records: serde_json::Map = (0..600).map(|i| (format!("gpu:r6-record-{i} engine 0x{i:08x}"), json!({ "why": "r6: other bits than the reference's", "capacity": null }))).collect(); + let (pretty, compact) = (dir.join("pretty.json"), dir.join("compact.json")); + std::fs::write(&pretty, serde_json::to_string_pretty(&records).unwrap()).unwrap(); + std::fs::write(&compact, serde_json::to_string(&records).unwrap()).unwrap(); + for (what, path, print) in [("a long identity", &pretty, 5000usize), ("a store written compact", &compact, 8)] { + let limit = std::fs::metadata(path).unwrap().len() + 4096; + let line = under(path, print, limit); + println!("PROBE r6 store, {what}, a file size limit of {limit}: {line}"); + assert!(line.contains("(Uncertified, Uncertified)") && line.contains("does not take a rewrite"), "{what}: {line}"); + let line = under(path, print, 1 << 24); + assert!(line.contains("(BitExact, BitExact)"), "{what}, a limit it fits: {line}"); + } + let _ = std::fs::remove_dir_all(&dir); +} + +/// 5: a link in a folder anyone may write that has the sticky bit is not followed, +/// whoever made it; the same link in another folder is (positive control). A lock +/// that is a link is not opened through it. +#[cfg(unix)] +#[test] +fn review6_a_link_in_a_sticky_folder_is_not_followed() { + use std::os::unix::fs::PermissionsExt; + let m = Called("gpu:r6-link"); + let dir = fresh("link"); + for d in ["private", "open", "plain", "locked"] { + std::fs::create_dir_all(dir.join(d)).unwrap(); + } + let victim = dir.join("private").join("settings.json"); + std::fs::write(&victim, br#"{"theme": "dark"}"#).unwrap(); + std::fs::set_permissions(dir.join("open"), std::fs::Permissions::from_mode(0o1777)).unwrap(); + let link = dir.join("open").join("demotions.json"); + std::os::unix::fs::symlink(&victim, &link).unwrap(); + let (g, d) = graded_on(&m, &link); + println!("PROBE r6 store, a link in a sticky folder: {g:?} {d:?}"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + assert!(d.unwrap().contains("sticky bit")); + assert_eq!(std::fs::read(&victim).unwrap(), br#"{"theme": "dark"}"#); + assert!(!dir.join("private").join("settings.json.lock").exists(), "nothing made beside the link's target"); + // The same link in a folder without the sticky bit is followed. + let plain = dir.join("plain").join("demotions.json"); + std::os::unix::fs::symlink(&victim, &plain).unwrap(); + assert_eq!(graded_on(&m, &plain).0, (Parity::BitExact, Parity::BitExact), "positive control: a link elsewhere is followed"); + // A lock that is a link. + std::os::unix::fs::symlink(dir.join("private").join("elsewhere"), dir.join("locked").join("demotions.json.lock")).unwrap(); + let (g, d) = graded_on(&m, &dir.join("locked").join("demotions.json")); + println!("PROBE r6 store, a lock that is a link: {g:?} {d:?}"); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + assert!(!dir.join("private").join("elsewhere").exists(), "nothing made through the lock's link"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// 6: a store's file with two names is refused before a rewrite could split it; +/// with one name again it is certified. +#[cfg(unix)] +#[test] +fn review6_a_hard_linked_store_is_refused() { + let m = Called("gpu:r6-hard"); + let dir = fresh("hard"); + let (share, host) = (dir.join("share.json"), dir.join("host.json")); + std::fs::write(&share, b"{}").unwrap(); + std::fs::hard_link(&share, &host).unwrap(); + for p in [&share, &host] { + let (g, d) = graded_on(&m, p); + println!("PROBE r6 store, a hard-linked file {}: {g:?} {d:?}", p.display()); + assert_eq!(g, (Parity::Uncertified, Parity::Uncertified)); + assert!(d.unwrap().contains("hard link")); + } + std::fs::remove_file(&host).unwrap(); + assert_eq!(graded_on(&m, &share).0, (Parity::BitExact, Parity::BitExact), "one name again"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// 7: a certification removes a probe a killed certification left, and leaves one +/// a live writer holds. +#[test] +fn review6_a_killed_certification_s_probe_is_removed() { + let m = Called("gpu:r6-probe"); + let dir = fresh("probe"); + let path = dir.join("demotions.json"); + std::fs::write(&path, b"{}").unwrap(); + let stale = dir.join(".demotions.json.probe.999999.0.tmp"); + std::fs::write(&stale, vec![b'\n'; 1 << 20]).unwrap(); + aged(&stale); + let live = dir.join(".demotions.json.probe.999998.0.tmp"); + std::fs::write(&live, b"\n").unwrap(); + aged(&live); + let writer = std::fs::OpenOptions::new().write(true).open(&live).unwrap(); + writer.lock().unwrap(); + assert_eq!(graded_on(&m, &path).0, (Parity::BitExact, Parity::BitExact)); + println!("PROBE r6 store, after a certification: the killed one's probe {}, the live one's {}", if stale.exists() { "left" } else { "removed" }, if live.exists() { "kept" } else { "removed" }); + assert!(!stale.exists() && live.exists()); + drop(writer); + // Its writer gone, the next certification takes it. (A process another test spawns + // holds a copy of every open file between its fork and its exec, and so the lock for + // that moment: a few certifications are allowed.) + let mut tries = 0; + while live.exists() && tries < 50 { + let (g, d) = graded_on(&m, &path); + assert_eq!(g, (Parity::BitExact, Parity::BitExact), "{d:?}"); + tries += 1; + if live.exists() { + std::thread::sleep(std::time::Duration::from_millis(100)); + } + } + println!("PROBE r6 store, the live one's probe removed once its writer was gone, after {tries} certifications"); + assert!(!live.exists(), "once its writer is gone"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// 8: settles that keep moving past 2¹³ sweeps: a stale read at a bound past 8,182 +/// sweeps (to 14,955) and a 13-bit sweep counter lose the settle grade. +#[test] +fn review6_settles_that_move_past_2_13() { + let sweeps = |s: u64| Capacity { sweeps: Some(s), ..Capacity::NONE }; + let wide = i64::from(i32::MAX); + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let cuda = Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }; + each_loses(&[ + ("no capacity", Fault::Stale(8182), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::Stale(10000), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::Stale(14955), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::Ring13, KERNEL_SETTLE, Capacity::NONE), + ("CUDA's and Metal's", Fault::Stale(8182), KERNEL_SETTLE, cuda), + ("CUDA's and Metal's", Fault::Ring13, KERNEL_SETTLE, cuda), + ("the WebGPU member's numbers", Fault::Ring13, KERNEL_SETTLE, gpu), + ("sweeps 20000", Fault::Stale(10000), KERNEL_SETTLE, sweeps(20000)), + ("sweeps 20000", Fault::Ring13, KERNEL_SETTLE, sweeps(20000)), + ]); +} + +/// 9: settles that end in a two-cycle late: at the first sweep of a later batch of +/// 64, and past 4,096 sweeps, where a 9- to 12-bit sweep counter read at the ending +/// gives the other phase; a stale read there, and a two-cycle no longer looked for, +/// lose the settle grade. +#[test] +fn review6_two_cycles_end_late() { + let sweeps = |s: u64| Capacity { sweeps: Some(s), ..Capacity::NONE }; + let wide = i64::from(i32::MAX); + let gpu = Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: lanes(Some(wide), None, None), above_zero: lanes(Some(wide), None, None), table_entries: Some(1 << 22), ..Capacity::NONE }; + let cuda = Capacity { table_entries: Some(1 << 22), ..Capacity::NONE }; + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + each_loses(&[ + ("no capacity", Fault::CycleRing(4095), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::CycleRing(511), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::CycleStale(21), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::CycleStale(4000), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::MissCycleAfter(21), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::MissCycleAfter(4095), KERNEL_SETTLE, Capacity::NONE), + ("no capacity", Fault::BatchCycle, KERNEL_SETTLE, Capacity::NONE), + ("CUDA's and Metal's", Fault::CycleRing(4095), KERNEL_SETTLE, cuda), + ("CUDA's and Metal's", Fault::BatchCycle, KERNEL_SETTLE, cuda), + ("the WebGPU member's numbers", Fault::CycleRing(4095), KERNEL_SETTLE, gpu), + ("the WebGPU member's numbers", Fault::BatchCycle, KERNEL_SETTLE, gpu), + ("sweeps 8193", Fault::CycleRing(4095), KERNEL_SETTLE, sweeps(8193)), + ("cells 1500", Fault::CycleStale(21), KERNEL_SETTLE, Capacity { cells: Some(1500), ..Capacity::NONE }), + ("the FPGA's", Fault::CycleStale(21), KERNEL_SETTLE, fpga), + ("the FPGA's", Fault::MissCycleAfter(21), KERNEL_SETTLE, fpga), + ]); +} + +/// 10, 11: what a `statistical` grade rests on, as §7.2 states it. The workloads a +/// capacity adds run in calls ending in every partial warp of more than 16 chains +/// and workgroup of more than 32, so a sampler wrong in a call's partial group of +/// any such size loses the grade. A fault at a count of cells the battery does not +/// run, and a law near the exact one, keep it, as the text says; a law 15% too warm +/// does not. Every answer from such a kernel says what the grade rests on. +#[test] +fn review6_the_statistical_grade_is_as_stated() { + use wai_quantum::quantum_landscape::member::STATISTICAL_GRADE; + let fpga = Capacity { cells: Some(64), above_zero: lanes(None, Some(524_287), Some(1_048_575)), uncertain_span: Some(8192), ..Capacity::NONE }; + for (what, flaw, cap, want) in [ + ("a call's partial workgroup of 34 to 62, lanes 32 on, no capacity", Flaw::CallTail64Mid, Capacity::NONE, Parity::Uncertified), + ("a call's partial workgroup of 34 to 62, lanes 32 on, the FPGA's", Flaw::CallTail64Mid, fpga, Parity::Uncertified), + ("a call's partial warp of an even count, lanes 16 on, no capacity", Flaw::CallTail32Even, Capacity::NONE, Parity::Uncertified), + ("a law 15% too warm from cell 1,024, no capacity", Flaw::WarmFrom(1024, 1.15), Capacity::NONE, Parity::Uncertified), + ("as stated: a law 10% too warm from cell 1,024, no capacity", Flaw::WarmFrom(1024, 1.10), Capacity::NONE, Parity::Statistical), + ("as stated: a partial workgroup's tail at even counts of cells, no capacity", Flaw::Tail64Even, Capacity::NONE, Parity::Statistical), + ] { + let m = Flawed(flaw, cap); + let c = certify(&m).unwrap(); + println!("PROBE r6 statistical, {what}: {:?}", grades(&c)); + assert_eq!(c.parity(KERNEL_SAMPLE), want, "{what}"); + } + let m = Flawed(Flaw::None, floor()); + let c = certify(&m).unwrap(); + assert_eq!(c.parity(KERNEL_SAMPLE), Parity::Statistical); + let a = job::call_certified(&c, "run", &json!({ "program": "vars 3\nagree 0 1\noppose 1 2\nsolve t0=2 restarts=2 steps=6\n", "parity": "statistical" }), MAX_JOB_BYTES).unwrap(); + println!("PROBE r6 statistical, an answer's label: {}", a["label"]); + assert!(a["label"].as_str().unwrap().contains(STATISTICAL_GRADE), "{}", a["label"]); +} diff --git a/wai/wai-quantum/tests/landscape_devices.rs b/wai/wai-quantum/tests/landscape_devices.rs index 7c5b1de2c..ffbf7d953 100644 --- a/wai/wai-quantum/tests/landscape_devices.rs +++ b/wai/wai-quantum/tests/landscape_devices.rs @@ -501,4 +501,39 @@ fn timings() { let t = middle(&mut || time(|| job::call_certified(&c, "replay", &req, MAX_JOB_BYTES).unwrap()).1); println!(" {} {t:.3} s, the same states, energies, verdicts, work and replay digests", m.id()); } + // What the host's audit (§7.3) costs in wall time: each job on a member certified + // bit-exact, so audited, against the same member run by name, so not, and against + // the reference alone. + let jobs = [ + ("replay of 256 seeds, 8 chains each, 64 cells, 500 sweeps", "replay", program(&sparse(64, 4, 0xa0d1), "solve t0=2 restarts=8 steps=500"), 256), + ("replay of 256 seeds, 1 chain each, 64 cells, 500 sweeps", "replay", program(&sparse(64, 4, 0xa0d1), "solve t0=2 restarts=1 steps=500"), 256), + ("run of 4,096 chains, 64 cells, 200 sweeps", "run", program(&sparse(64, 4, 0xa0d2), "solve t0=2 restarts=4096 steps=200"), 1), + ("run of 1,024 chains, 1,024 cells, 200 sweeps", "run", program(&sparse(1024, 6, 0xa0d3), "solve t0=2 restarts=1024 steps=200"), 1), + ]; + println!("audit threads beside a job's own: {}", wai_quantum::quantum_landscape::run::audit_threads()); + for (what, op, text, seeds) in &jobs { + let req = if *op == "replay" { json!({ "program": text, "seed": "1", "seeds": seeds }) } else { json!({ "program": text, "seed": "1" }) }; + let t_cpu = middle(&mut || time(|| job::call(op, &req).unwrap()).1); + println!("audit, {what}: cpu:reference alone {t_cpu:.3} s"); + for m in &ms { + let c = member::certify(m.as_ref()).unwrap(); + if c.parity(member::KERNEL_SAMPLE) != member::Parity::BitExact { + continue; + } + let by_name = member::named(m.as_ref()).unwrap(); + // Five runs, each the middle of three of each, audited and not in turn. + let runs: Vec<(f64, f64)> = (0..5) + .map(|_| { + let audited = middle(&mut || time(|| job::call_certified(&c, op, &req, MAX_JOB_BYTES).unwrap()).1); + let unaudited = middle(&mut || time(|| job::call_certified(&by_name, op, &req, MAX_JOB_BYTES).unwrap()).1); + (audited / unaudited - 1.0, audited / t_cpu) + }) + .collect(); + assert!(c.demoted().is_none(), "{}", m.id()); + let (lo, hi) = runs.iter().fold((f64::MAX, f64::MIN), |(lo, hi), r| (lo.min(r.0), hi.max(r.0))); + let most = runs.iter().map(|r| r.1).fold(0.0, f64::max); + let each: Vec = runs.iter().map(|r| format!("{:+.0}%", 100.0 * r.0)).collect(); + println!(" {} audited against unaudited over 5 runs: {} (from {:+.0}% to {:+.0}%), at most {most:.2} of the reference's time", m.id(), each.join(" "), 100.0 * lo, 100.0 * hi); + } + } }