From f65b87720e176e953df1cb93eaa650f5ed97f71b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 04:05:36 -0500 Subject: [PATCH 01/15] wai-quantum: a landscape member declares its capacity, and is held to everything within it A member states once, as fixed numbers, what its kernels hold (quantum_landscape::capacity::Capacity): 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; and the chains a call. The host declines for it exactly the jobs past those numbers, by Capacity::admits, a function of the job and the numbers alone, so no member declines by choice; Member::capacity takes the place of Member::takes. member::certify runs every vector and battery workload within the capacity, and the grade holds within it: member.capacity in every answer, a sentence in the label, the number a job is past in its refusal. A member that refuses or fails a case within its capacity loses the grade, and one that declares less than the floor, the least capacity that takes every case of the core set, is not certified. The GPU members declare their real limits; 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; a test holds those numbers to the engine's own limits over the whole set, its host shortens a table's run of temperatures before it refuses a window, and the host certifies its register-level model bit-exact within that capacity on the set as it stands. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-landscape.yml | 12 +- .../2026-11-07-landscape-declared-capacity.md | 6 + wai/extensions/quantum-landscape.md | 94 +++-- wai/wai-quantum-landscape-fpga/README.md | 45 +- wai/wai-quantum-landscape-fpga/src/conform.rs | 25 +- wai/wai-quantum-landscape-fpga/src/member.rs | 75 +++- wai/wai-quantum/README.md | 2 +- wai/wai-quantum/README.zh-CN.md | 2 +- wai/wai-quantum/docs/landscape.md | 32 +- wai/wai-quantum/schemas/landscape.v1.json | 236 ++++++++++- wai/wai-quantum/src/quantum_landscape.rs | 1 + .../src/quantum_landscape/capacity.rs | 399 ++++++++++++++++++ .../src/quantum_landscape/conformance.rs | 81 ++-- wai/wai-quantum/src/quantum_landscape/cuda.rs | 5 +- wai/wai-quantum/src/quantum_landscape/job.rs | 98 ++++- .../src/quantum_landscape/member.rs | 67 ++- .../src/quantum_landscape/metal.rs | 5 +- .../src/quantum_landscape/reach.rs | 12 + wai/wai-quantum/src/quantum_landscape/run.rs | 18 +- .../src/quantum_landscape/webgpu.rs | 46 +- .../tests/landscape_conformance.rs | 55 ++- 21 files changed, 1104 insertions(+), 212 deletions(-) create mode 100644 wai/changes/2026-11-07-landscape-declared-capacity.md create mode 100644 wai/wai-quantum/src/quantum_landscape/capacity.rs diff --git a/.github/workflows/wai-quantum-landscape.yml b/.github/workflows/wai-quantum-landscape.yml index 561456f18..c538ac76e 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -125,8 +125,12 @@ 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\. 99 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (99 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::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 +147,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 \ @@ -354,7 +358,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\. 98 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (98 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..b21378b06 --- /dev/null +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -0,0 +1,6 @@ +### 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; and the chains a call. 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.** `member::certify` runs every vector and battery workload within the capacity, and the grade holds within it: `member.capacity` in every answer, 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, 8 chains a call, and the table and span those cases need) is not certified at all. +- 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. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 75dbab1d4..0fa03110e 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -181,7 +181,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`, and the `capacity` that parity holds within, null where none is declared (§5) | | `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 | @@ -197,8 +197,9 @@ A `run` answers with these fields (the schema has them all): | `refusals` | each member that a placing host passed over, with why (§5); absent where there was none | 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`), `seeds` and `lines`. A `sweep` and a `compare` give `member` as +`id`, `class`, `capacity` 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. @@ -399,17 +400,43 @@ 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 | +| `chains_per_call` | the chains a host gives it in one call; a host gives it no more | + +A workload fits when every declared number 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); +60 entries of cut points a sweep and an uncertain span of 60 (the same +chains at their hottest); and 8 chains a call (a run of 8 starts, held to the reference +in one call). A device that cannot hold these cannot be held to the core set, and the +grade would vouch for nothing. **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 +466,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 +487,22 @@ 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 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`, null where none is +declared). 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 exactly. Sample: it gives every `chains` vector within its capacity exactly, each case's chains in one call | +| `statistical` | sample: not `bit-exact`, and every workload of the `statistical` battery within its capacity passes (§7.2) | | `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. +A member is held to every vector within its capacity, and refusing one is failing it. +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 +510,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 +534,8 @@ 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 +its parity holds within that capacity, in its numbers (§5). ## 6. The comparison @@ -521,6 +556,11 @@ 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: the +`settle` vectors but `fields-at-the-bound`, the first fifteen `chains` vectors (three +landscapes cooling at five schedules) and the first eight battery workloads. It sets the +floor of a declared capacity (§5). + ### 7.1. Bits - `settle`: whole workloads, starts and bounds, each with its field bound, and the state, diff --git a/wai/wai-quantum-landscape-fpga/README.md b/wai/wai-quantum-landscape-fpga/README.md index 82b9fc342..0bb12e145 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -58,20 +58,24 @@ 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, for a sampling kernel that misses, trying the statistical battery within it). -**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 (`the_declared_capacity_is_exactly_what_the_engine_holds`). + +**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 +259,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 +388,11 @@ 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 a job past + them goes to another member (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. - **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/member.rs b/wai/wai-quantum-landscape-fpga/src/member.rs index c0ea2f027..9d54a38a3 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" @@ -308,19 +328,10 @@ 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 } fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result { @@ -461,6 +472,42 @@ 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 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 369cb9ba3..3b0f8eb1f 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 e11da2990..d20f1911a 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..ffeadf32e 100644 --- a/wai/wai-quantum/docs/landscape.md +++ b/wai/wai-quantum/docs/landscape.md @@ -176,7 +176,7 @@ 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_webgpu` | `quantum_landscape::webgpu`: Metal, Vulkan or DX12 through WebGPU | 32-bit: it declares a field bound of 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 | @@ -191,9 +191,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 +228,27 @@ 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; and how many chains a call. The host declines for it +exactly the jobs past those numbers (`Capacity::admits`, a function of the job and the numbers +alone), certifies it on every vector and battery workload within them, and states them 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, 8 chains a call and so on) +is not certified at all. + +```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..b0d5e5e15 100644 --- a/wai/wai-quantum/schemas/landscape.v1.json +++ b/wai/wai-quantum/schemas/landscape.v1.json @@ -103,7 +103,8 @@ "id", "class", "kernel", - "parity" + "parity", + "capacity" ], "properties": { "id": { @@ -128,6 +129,62 @@ "uncertified" ], "description": "What the member has earned for this kernel." + }, + "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); null where none is declared, as the reference's.", + "anyOf": [ + { + "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" + }, + "chains_per_call": { + "type": "integer" + } + } + }, + { + "type": "null" + } + ] } } }, @@ -718,7 +775,8 @@ "required": [ "id", "class", - "kernels" + "kernels", + "capacity" ], "properties": { "id": { @@ -754,6 +812,62 @@ } } } + }, + "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); null where none is declared, as the reference's.", + "anyOf": [ + { + "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" + }, + "chains_per_call": { + "type": "integer" + } + } + }, + { + "type": "null" + } + ] } } }, @@ -921,7 +1035,8 @@ "required": [ "id", "class", - "kernels" + "kernels", + "capacity" ], "properties": { "id": { @@ -957,6 +1072,62 @@ } } } + }, + "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); null where none is declared, as the reference's.", + "anyOf": [ + { + "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" + }, + "chains_per_call": { + "type": "integer" + } + } + }, + { + "type": "null" + } + ] } } }, @@ -1091,7 +1262,8 @@ "id", "class", "kernel", - "parity" + "parity", + "capacity" ], "properties": { "id": { @@ -1116,6 +1288,62 @@ "uncertified" ], "description": "What the member has earned for this kernel." + }, + "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); null where none is declared, as the reference's.", + "anyOf": [ + { + "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" + }, + "chains_per_call": { + "type": "integer" + } + } + }, + { + "type": "null" + } + ] } } }, 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..25ddc9604 --- /dev/null +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -0,0 +1,399 @@ +//! 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, the cases the set held +//! before it grew to reach large jobs, so a member cannot be certified by +//! declaring itself too small to be held to anything. + +use super::conformance; +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 chains it takes in one call. The host gives it no more. + 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, chains_per_call: None }; + + /// Whether any limit is declared. + pub fn is_declared(&self) -> bool { + *self != Capacity::NONE + } + + /// Whether a workload run at `temperatures` fits, or which declared number + /// it is past, in a sentence. A settle is `temperatures` empty. 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}")); + 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(()) + } + + /// Whether `count` chains go to it in one call. + pub fn takes_chains(&self, count: u64) -> bool { + self.chains_per_call.is_none_or(|c| count <= c) + } + + /// 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("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. + 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("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. + pub fn words(&self) -> 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.chains_per_call { + parts.push(format!("{} chains a call", group(c))); + } + Some(format!("Its parity holds within the capacity it declares, and jobs past it go elsewhere: {}.", parts.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. +pub fn floor() -> Capacity { + static FLOOR: OnceLock = OnceLock::new(); + *FLOOR.get_or_init(|| { + let mut f = Capacity { cells: Some(0), couplings: Some(0), at_zero: Limits { field: Some(0), field_bound: Some(0), cost: Some(0) }, above_zero: Limits { field: Some(0), field_bound: Some(0), cost: Some(0) }, table_entries: Some(0), uncertain_span: Some(0), chains_per_call: Some(0) }; + let up = |slot: &mut Option, v: i64| *slot = Some(slot.unwrap_or(0).max(v)); + let upu = |slot: &mut Option, v: u64| *slot = Some(slot.unwrap_or(0).max(v)); + let mut take = |w: &Workload, temperatures: &[f64], chains: u64| { + let zero = vec![0i8; w.n()]; + let field = (0..w.n()).map(|i| w.field(&zero, i).abs()).max().unwrap_or(0); + upu(&mut f.cells, w.n() as u64); + upu(&mut f.couplings, w.nonzeros()); + upu(&mut f.chains_per_call, chains); + let hot: Vec = temperatures.iter().copied().filter(|&t| t > 0.0).collect(); + if temperatures.is_empty() || hot.len() < temperatures.len() { + up(&mut f.at_zero.field, field); + up(&mut f.at_zero.field_bound, w.field_bound()); + up(&mut f.at_zero.cost, w.cost()); + } + if !hot.is_empty() { + up(&mut f.above_zero.field, field); + up(&mut f.above_zero.field_bound, w.field_bound()); + up(&mut f.above_zero.cost, w.cost()); + upu(&mut f.table_entries, reach::reach(w).entries); + upu(&mut f.uncertain_span, uncertain_span(w, hot.iter().copied().fold(f64::MIN, f64::max))); + } + }; + for c in conformance::core_settle_cases() { + take(&c.landscape.lower(), &[], 1); + } + for c in conformance::core_chain_cases() { + take(&c.landscape.lower(), &c.temperatures.each(), c.restarts); + } + for c in conformance::core_battery() { + take(&c.landscape.lower(), &c.temperatures, 1); + } + f + }) +} + +#[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, Some(8), "a run of 8 starts in one call"); + assert_eq!((f.couplings, f.table_entries, f.uncertain_span), (Some(992), Some(60), Some(60)), "the numbers §5 states"); + 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().unwrap(); + assert!(words.contains("at most 64 cells") && words.contains("fields to ±524,287") && words.contains("span of 8,192"), "{words}"); + assert_eq!(Capacity::NONE.json(), Value::Null); + assert!(Capacity::NONE.words().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()]); + } +} diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index 237aab132..df778ee65 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,13 @@ //! 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 ([`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`]). //! - `jobs`: whole requests, what their answers hold, refusals included: the //! worked examples and the rules a reader must follow. @@ -192,25 +197,34 @@ 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. One past it is not run. pub fn settle_vectors_hold(m: &dyn Member) -> bool { + let cap = m.capacity(); settle_cases().iter().all(|c| { let w = c.landscape.lower(); - if let Some(may) = declines(m, &w, false) { - return may; + if cap.admits(&w, &[]).is_err() { + return true; } 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) @@ -326,6 +340,12 @@ 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. +pub fn core_chain_cases() -> Vec { + chain_cases().into_iter().take(15).collect() +} + 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)); @@ -370,11 +390,12 @@ 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 { + let cap = m.capacity(); chain_cases().iter().all(|c| { 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; + if cap.admits(&w, &temperatures).is_err() || !cap.takes_chains(c.restarts) { + return true; } let chains = c.chains(); let want: Vec = chains.iter().map(|ch| anneal_with(&w, &ch.start, &temperatures, ch.seed)).collect(); @@ -560,6 +581,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]; @@ -613,8 +639,7 @@ pub struct BatteryResult { 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,19 +688,21 @@ 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 { + let cap = m.capacity(); + let per_call = cap.chains_per_call.map_or(4096, |c| c.clamp(1, 4096) as usize); battery() .iter() .enumerate() .map(|(k, 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 mut ok = true; - for chunk in case.chains(k).chunks(4096) { + for chunk in case.chains(k).chunks(per_call) { 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 { diff --git a/wai/wai-quantum/src/quantum_landscape/cuda.rs b/wai/wai-quantum/src/quantum_landscape/cuda.rs index 78ad755e9..1fd00fe1b 100644 --- a/wai/wai-quantum/src/quantum_landscape/cuda.rs +++ b/wai/wai-quantum/src/quantum_landscape/cuda.rs @@ -273,8 +273,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..4e6c43d71 100644 --- a/wai/wai-quantum/src/quantum_landscape/job.rs +++ b/wai/wai-quantum/src/quantum_landscape/job.rs @@ -101,8 +101,9 @@ pub struct Prepared { temperature: Option, seeds: u32, temperatures: Vec, - /// Whether any sweep of the job is above temperature 0. - pub sampling: bool, + /// Every schedule the job runs a member on: a settle's is empty, a run of + /// chains has a temperature a sweep. A member's capacity is held to each. + pub schedules: Vec>, /// The kernels the job runs. pub kernels: Vec<&'static str>, } @@ -161,11 +162,26 @@ 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: Vec> = Vec::new(); + for (plan, _) in &runs { + let mut each = vec![match plan { + Plan::Settle { .. } => Vec::new(), + _ => 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, kernels })) } /// Answer a prepared job on a certified member. @@ -198,7 +214,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 })) } } } @@ -215,8 +231,9 @@ pub fn call_certified(c: &Certified<'_>, op: &str, request: &Value, max_bytes: u /// 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,7 +244,7 @@ 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(why) = p.schedules.iter().find_map(|t| c.capacity().admits(&p.job.workload, t).err()) { why } else { match answer(c, &p) { @@ -543,8 +560,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 +580,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 +619,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 +628,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 jobs past it go elsewhere: 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 say so. + assert_eq!(call("run", &json!({ "program": example("talks") })).unwrap()["member"]["capacity"], Value::Null); } /// A member whose chains come back where they started: its settle kernel is the @@ -682,7 +740,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..101df3b67 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -17,19 +17,26 @@ //! 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}; @@ -127,13 +134,13 @@ 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> { - Ok(()) + /// 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 } } @@ -168,13 +175,14 @@ pub struct Certified<'a> { member: &'a dyn Member, settle: Parity, sample: Parity, + capacity: Capacity, } 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 } } } @@ -183,10 +191,24 @@ 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`]. pub fn parity(&self, kernel: &str) -> Parity { if kernel == KERNEL_SETTLE { self.settle } else { self.sample } } + + /// 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, and the + /// capacity its parity holds within where it declares one. + pub fn label(&self) -> String { + match self.capacity.words() { + Some(w) => format!("{} {w}", self.member.label()), + None => self.member.label(), + } + } } /// A member's id is `class:name`, its class one of the fabric's and the one it @@ -214,14 +236,21 @@ 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 }) + Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified, capacity: m.capacity() }) } -/// 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> { check_id(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("; "))); + } let settle = if conformance::settle_vectors_hold(m) { Parity::BitExact } else { Parity::Uncertified }; let sample = if conformance::chain_vectors_hold(m) { Parity::BitExact @@ -230,7 +259,7 @@ pub fn certify(m: &dyn Member) -> Result, String> { } else { Parity::Uncertified }; - Ok(Certified { member: m, settle, sample }) + Ok(Certified { member: m, settle, sample, capacity }) } fn check_cells(o: &Outcome, n: usize) -> Result<(), String> { @@ -293,7 +322,7 @@ pub fn check_chain(o: &Outcome, w: &Workload, temperatures: &[f64]) -> Result<() /// What an answer says of the member that ran it. 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() }) + json!({ "id": m.id(), "class": m.class(), "kernel": kernel, "parity": c.parity(kernel).label(), "capacity": c.capacity().json() }) } /// What an answer that may run both kernels says of the member that ran it: @@ -306,7 +335,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::>() }) + json!({ "id": m.id(), "class": m.class(), "kernels": used.iter().map(|k| json!({ "kernel": k, "parity": c.parity(k).label() })).collect::>(), "capacity": c.capacity().json() }) } #[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..2f1c3ac9e 100644 --- a/wai/wai-quantum/src/quantum_landscape/run.rs +++ b/wai/wai-quantum/src/quantum_landscape/run.rs @@ -325,7 +325,7 @@ fn execute(job: &Job, plan: Plan, seed: u64, c: &Certified<'_>) -> Result { // The host gives a member only what it takes, however it was reached. - m.takes(w, false).map_err(refused)?; + c.capacity().admits(w, &[]).map_err(refused)?; let o = m.settle(w, &vec![0i8; n], sweeps).map_err(refused)?; member::check_settle(&o, w, sweeps).map_err(refused)?; let energy = job.program.landscape.energy(&o.state); @@ -353,8 +353,10 @@ 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 cap = c.capacity(); + cap.admits(w, temperatures).map_err(refused)?; + // No more chains in a call than the member declares it takes. + let batch = (CHUNK_CELLS / n.max(1)).clamp(1, CHUNK_CHAINS).min(cap.chains_per_call.map_or(usize::MAX, |c| (c as usize).max(1))); let r = u64::from(restarts); // Every (seed, start) in order: seed after seed, start after start. let total = seeds.len() as u64 * r; @@ -535,7 +537,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 +570,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 +645,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 +685,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 +758,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, diff --git a/wai/wai-quantum/src/quantum_landscape/webgpu.rs b/wai/wai-quantum/src/quantum_landscape/webgpu.rs index ec401dd52..3c9cec53e 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}; @@ -377,25 +379,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); @@ -433,7 +437,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 { @@ -558,8 +562,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..c6fa8e90a 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -6,6 +6,7 @@ use wai_quantum::quantum_landscape::conformance::{chi_square, run_battery, vecto 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::capacity::{Capacity, Limits}; 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}; @@ -379,7 +380,7 @@ 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, @@ -393,8 +394,10 @@ 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), + /// 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) { @@ -480,6 +483,11 @@ 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 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 }); @@ -500,6 +508,11 @@ 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()); + } Ok(chains.iter().enumerate().map(|(k, c)| match *self { Fault::OnlyTheFirstChains(at) if k >= at => { let term = if w.rests(&c.start) { Term::FixedPoint } else { Term::Bound }; @@ -508,12 +521,13 @@ 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, } } } @@ -521,7 +535,7 @@ impl Member for Fault { /// 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 clean = Fault::DeclaresFieldBound(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)]; @@ -555,18 +569,23 @@ fn every_fault_loses_the_grade_of_the_kernel_it_is_in() { } } -/// 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 -- 2.54.0 (Apple Git-157) From 75edfe063b5afb096d740e17da3e3958e28416bd Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 04:15:34 -0500 Subject: [PATCH 02/15] wai-quantum: an answer states a capacity only where one is declared, and the floor's numbers are the core set's With none declared, as the reference's, an answer carries no capacity, so the reference's answers keep their exact bytes: CORPUS_20 and every pinned digest stand. The schema makes capacity an optional object; the spec and changelog say it is absent where none is declared. The floor's table entries are 31 (the dense chains, 16 cells with every pair coupled, at their hottest) and its uncertain span 61 (the wide-fields chains at their hottest), as worked out from the core set; the test pins them. Co-Authored-By: Claude Opus 5.5 --- .../2026-11-07-landscape-declared-capacity.md | 2 +- wai/extensions/quantum-landscape.md | 20 +- wai/wai-quantum/schemas/landscape.v1.json | 328 ++++++++---------- .../src/quantum_landscape/capacity.rs | 5 +- wai/wai-quantum/src/quantum_landscape/job.rs | 4 +- .../src/quantum_landscape/member.rs | 14 +- 6 files changed, 176 insertions(+), 197 deletions(-) diff --git a/wai/changes/2026-11-07-landscape-declared-capacity.md b/wai/changes/2026-11-07-landscape-declared-capacity.md index b21378b06..996e2f057 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -1,6 +1,6 @@ ### 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; and the chains a call. 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.** `member::certify` runs every vector and battery workload within the capacity, and the grade holds within it: `member.capacity` in every answer, 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. +- **Graded within it.** `member::certify` runs every vector and battery workload within the capacity, and the grade holds within it: `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, 8 chains a call, and the table and span those cases need) is not certified at all. - 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. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 0fa03110e..6eb55ffee 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -181,7 +181,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`, `parity`, and the `capacity` that parity holds within, null where none is declared (§5) | +| `member` | what ran it: `id`, `class`, `kernel`, `parity`, and the `capacity` that parity holds within, absent where none is declared (§5) | | `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 | @@ -425,13 +425,13 @@ its capacity has failed it. The reference declares no capacity. 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); -60 entries of cut points a sweep and an uncertain span of 60 (the same -chains at their hottest); and 8 chains a call (a run of 8 starts, held to the reference -in one call). A device that cannot hold these cannot be held to the core set, and the -grade would vouch for nothing. +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 8 chains a call (a +run of 8 starts, held to the reference in one call). A device that cannot hold these +cannot be held to the core set, and the grade would vouch for nothing. **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), within @@ -489,8 +489,8 @@ at most 64 characters in all. `cpu:reference` is the reference's, and no other m A member does not state its parity: the host finds it, by running the member's kernels on the conformance set (§7) within the member's declared capacity (§5), 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`, null where none is -declared). A parity is a statement about the jobs within that capacity, and no others. +`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 | |---|---| diff --git a/wai/wai-quantum/schemas/landscape.v1.json b/wai/wai-quantum/schemas/landscape.v1.json index b0d5e5e15..1c34406db 100644 --- a/wai/wai-quantum/schemas/landscape.v1.json +++ b/wai/wai-quantum/schemas/landscape.v1.json @@ -103,8 +103,7 @@ "id", "class", "kernel", - "parity", - "capacity" + "parity" ], "properties": { "id": { @@ -131,60 +130,53 @@ "description": "What the member has earned for this kernel." }, "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); null where none is declared, as the reference's.", - "anyOf": [ - { + "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": { - "cells": { + "field": { "type": "integer" }, - "couplings": { + "field_bound": { "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" - }, - "chains_per_call": { + "cost": { "type": "integer" } } }, - { - "type": "null" + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" } - ] + } } } }, @@ -775,8 +767,7 @@ "required": [ "id", "class", - "kernels", - "capacity" + "kernels" ], "properties": { "id": { @@ -814,60 +805,53 @@ } }, "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); null where none is declared, as the reference's.", - "anyOf": [ - { + "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": { - "cells": { + "field": { "type": "integer" }, - "couplings": { + "field_bound": { "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" - }, - "chains_per_call": { + "cost": { "type": "integer" } } }, - { - "type": "null" + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" } - ] + } } } }, @@ -1035,8 +1019,7 @@ "required": [ "id", "class", - "kernels", - "capacity" + "kernels" ], "properties": { "id": { @@ -1074,60 +1057,53 @@ } }, "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); null where none is declared, as the reference's.", - "anyOf": [ - { + "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": { - "cells": { + "field": { "type": "integer" }, - "couplings": { + "field_bound": { "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" - }, - "chains_per_call": { + "cost": { "type": "integer" } } }, - { - "type": "null" + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" } - ] + } } } }, @@ -1262,8 +1238,7 @@ "id", "class", "kernel", - "parity", - "capacity" + "parity" ], "properties": { "id": { @@ -1290,60 +1265,53 @@ "description": "What the member has earned for this kernel." }, "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); null where none is declared, as the reference's.", - "anyOf": [ - { + "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": { - "cells": { + "field": { "type": "integer" }, - "couplings": { + "field_bound": { "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" - }, - "chains_per_call": { + "cost": { "type": "integer" } } }, - { - "type": "null" + "above_zero": { + "type": "object", + "properties": { + "field": { + "type": "integer" + }, + "field_bound": { + "type": "integer" + }, + "cost": { + "type": "integer" + } + } + }, + "table_entries": { + "type": "integer" + }, + "uncertain_span": { + "type": "integer" + }, + "chains_per_call": { + "type": "integer" } - ] + } } } }, diff --git a/wai/wai-quantum/src/quantum_landscape/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs index 25ddc9604..13d85b212 100644 --- a/wai/wai-quantum/src/quantum_landscape/capacity.rs +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -193,7 +193,8 @@ impl Capacity { out } - /// The declared numbers as an answer states them; null where none is declared. + /// 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; @@ -339,7 +340,7 @@ mod tests { assert_eq!(f.above_zero.field, Some(40)); assert_eq!(f.above_zero.cost, Some(8)); assert_eq!(f.chains_per_call, Some(8), "a run of 8 starts in one call"); - assert_eq!((f.couplings, f.table_entries, f.uncertain_span), (Some(992), Some(60), Some(60)), "the numbers §5 states"); + assert_eq!((f.couplings, f.table_entries, f.uncertain_span), (Some(992), Some(31), Some(61)), "the numbers §5 states"); for c in conformance::core_settle_cases() { assert_eq!(f.admits(&c.landscape.lower(), &[]), Ok(()), "{}", c.name); } diff --git a/wai/wai-quantum/src/quantum_landscape/job.rs b/wai/wai-quantum/src/quantum_landscape/job.rs index 4e6c43d71..4b4039570 100644 --- a/wai/wai-quantum/src/quantum_landscape/job.rs +++ b/wai/wai-quantum/src/quantum_landscape/job.rs @@ -652,8 +652,8 @@ mod tests { // 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 say so. - assert_eq!(call("run", &json!({ "program": example("talks") })).unwrap()["member"]["capacity"], Value::Null); + // 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 diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index 101df3b67..0edae1d4d 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -320,9 +320,19 @@ 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(), "capacity": c.capacity().json() }) + 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(); + } + v } /// What an answer that may run both kernels says of the member that ran it: @@ -335,7 +345,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::>(), "capacity": c.capacity().json() }) + 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)] -- 2.54.0 (Apple Git-157) From 28fa25d9326290e9dd5d855262011f6a975b51df Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 09:00:28 -0500 Subject: [PATCH 03/15] wai-quantum: a declared capacity is held at its edges, a batch is split, and the core set crosses windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adversarial check of 75edfe063 found faulty members certified bit-exact by declaring a capacity: a declared number removed fixed vectors, and the set samples each number at a few points only. - Capacity::edges: for each number declared below what a job can reach, certification runs vectors at exactly that number, within every other number declared, the reference their oracle; hot and cold above 0, so a field reached exactly is drawn exactly. A member that declares nothing runs the fixed set only, and the reference's answer bytes are unchanged. - sweeps is a capacity number (a settle's bound, a schedule's length); the core set holds 5,000 sweeps on 32 cells within the floor, which cross windows by table and by work, and the full set 200,000 sweeps of two cells. - chains_per_call is a batch, not a limit: certification splits every vector and battery workload into calls of the size run time uses (Capacity::batch, shared with run.rs), plus one call of exactly that many; words() no longer sends such jobs elsewhere, and the floor holds no batch. - certify reads the capacity once and holds the vectors to that reading. - The FPGA host's grade lines name the capacity; docs state what the GPU members declare; spec §5, §5.1, §7 and §7.1, the schema and the change note follow. - Every fault test also runs declaring its most hostile capacity, behind a positive control; the review's probes (p02 to p12, the skeptic's window member, the FPGA's random jobs) are kept as tests. vectors.json rebuilt. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-landscape.yml | 12 +- .../2026-11-07-landscape-declared-capacity.md | 7 +- wai/extensions/quantum-landscape.md | 64 +- .../landscape/vectors.json | 1349 +++++++++++++++++ wai/wai-quantum-landscape-fpga/README.md | 10 +- wai/wai-quantum-landscape-fpga/src/main.rs | 2 +- wai/wai-quantum-landscape-fpga/src/member.rs | 71 +- wai/wai-quantum/docs/landscape.md | 25 +- wai/wai-quantum/schemas/landscape.v1.json | 12 + .../src/quantum_landscape/capacity.rs | 445 +++++- .../src/quantum_landscape/conformance.rs | 93 +- wai/wai-quantum/src/quantum_landscape/job.rs | 25 +- .../src/quantum_landscape/member.rs | 8 +- wai/wai-quantum/src/quantum_landscape/run.rs | 4 +- .../tests/landscape_conformance.rs | 431 +++++- 15 files changed, 2405 insertions(+), 153 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape.yml b/.github/workflows/wai-quantum-landscape.yml index c538ac76e..182db0f48 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -125,10 +125,11 @@ 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\. 99 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (99 expected)"; exit 1; } + grep -qE '^test result: ok\. 100 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (100 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::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 \ @@ -233,7 +234,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\. 17 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (17 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 @@ -308,8 +309,9 @@ 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 review's probes test certification, 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_ 2>&1 | tee "$RUNNER_TEMP/vectors.log" + grep -qE '^test result: ok\. 12 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } done wasip2: @@ -358,7 +360,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\. 98 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (98 expected)"; exit 1; } + grep -qE '^test result: ok\. 99 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (99 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 index 996e2f057..82e7a514f 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -1,6 +1,7 @@ ### 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; and the chains a call. 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.** `member::certify` runs every vector and battery workload within the capacity, and the grade holds within it: `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, 8 chains a call, and the table and span those cases need) is not certified at all. +- **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. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 6eb55ffee..f16613640 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -412,9 +412,10 @@ A member declares what its kernels hold, once and before anything runs, as fixed | `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 | -| `chains_per_call` | the chains a host gives it in one call; a host gives it no more | +| `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 holds for it. Whether it does is a function of +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 @@ -429,9 +430,29 @@ large jobs. Its numbers are 32 cells and 992 couplings (the recall vector of 32 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 8 chains a call (a -run of 8 starts, held to the reference in one call). A device that cannot hold these -cannot be held to the core set, and the grade would vouch for nothing. +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. + +**Edge vectors.** A declared number takes vectors out of what a member is held to, and +the set samples each number at a few points only. So 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), the host adds vectors at exactly that number, each also within every other +number declared: built from the capacity alone and the same on every host, with the +reference's bits as their expected bits. They are a wave settling down that many cells +and a hot chain on them; that many couplings, each cell's as near as the declared field +bounds allow; on each side of temperature 0, a field bound reached through couplings and +by a field alone, the largest field and the largest cost, drawn above 0 both at a +temperature of their own size, where the draw is uncertain, and cold (1/64), where a +field reached exactly is drawn exactly; a table of that many entries; two fields that +span the uncertain span, drawn at a temperature as large; a schedule of that many sweeps; +and, for a batch, one call of exactly that many chains. The battery gets the same +workloads above temperature 0. So a grade is earned at the edge of a capacity, not at +the nearest vector inside it. A capacity that declares no number below what a job can +reach, and no batch, adds none. **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), within @@ -487,7 +508,8 @@ 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) within the member's declared capacity (§5), and an answer +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. @@ -495,11 +517,13 @@ is declared, so that the reference's answers are what they were). A parity is a | parity | granted when | |---|---| | `reference` | to the host's own reference kernels, and to nothing else | -| `bit-exact` | settle: the member gives every `settle` vector within its capacity exactly. Sample: it gives every `chains` vector within its capacity exactly, each case's chains in one call | -| `statistical` | sample: not `bit-exact`, and every workload of the `statistical` battery within its capacity passes (§7.2) | +| `bit-exact` | settle: the member gives every `settle` vector within its capacity, and every one at its edges (§5), exactly. Sample: it gives every `chains` vector within its capacity and at its edges 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 at its edges 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 within its capacity, and refusing one is failing it. +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. 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. @@ -556,10 +580,12 @@ 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: the -`settle` vectors but `fields-at-the-bound`, the first fifteen `chains` vectors (three -landscapes cooling at five schedules) and the first eight battery workloads. It sets the -floor of a declared capacity (§5). +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 at a declared +capacity's edges (§5) are not in the file: a host builds them from the capacity. ### 7.1. Bits @@ -588,10 +614,14 @@ floor of a declared capacity (§5). 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. - `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 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 0bb12e145..f9ddfe7a5 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -58,14 +58,18 @@ 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 within its declared capacity comes back bit for -bit (and, for a sampling kernel that misses, trying the statistical battery within it). +granting `bit-exact` to a kernel only if every case within its declared capacity, and every case +the host generates at each number it declares (a wave down 64 cells, fields at ±524,287 and a cost +of 1,048,575 drawn hot and cold, two fields 8,192 apart drawn at temperature 8,192), comes back bit +for bit (and, for a sampling kernel that misses, trying the statistical battery within it). **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 (`the_declared_capacity_is_exactly_what_the_engine_holds`). +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 diff --git a/wai/wai-quantum-landscape-fpga/src/main.rs b/wai/wai-quantum-landscape-fpga/src/main.rs index d2fe8bde4..b0c30a5ba 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) } diff --git a/wai/wai-quantum-landscape-fpga/src/member.rs b/wai/wai-quantum-landscape-fpga/src/member.rs index 9d54a38a3..5832fd2ed 100644 --- a/wai/wai-quantum-landscape-fpga/src/member.rs +++ b/wai/wai-quantum-landscape-fpga/src/member.rs @@ -307,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().unwrap_or_default())) } } @@ -495,6 +495,75 @@ mod tests { 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 diff --git a/wai/wai-quantum/docs/landscape.md b/wai/wai-quantum/docs/landscape.md index ffeadf32e..732ccb5d2 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 declares a field bound of 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 @@ -232,13 +233,15 @@ what a member takes and returns, what the host checks, and what each parity take 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; and how many chains a call. The host declines for it -exactly the jobs past those numbers (`Capacity::admits`, a function of the job and the numbers -alone), certifies it on every vector and battery workload within them, and states them 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, 8 chains a call and so on) -is not certified at all. +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), certifies it on every vector and battery workload within +them and on vectors generated at each number it declares (`Capacity::edges`), and states them 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. ```rust norun=account use wai_quantum::quantum_landscape::capacity::{Capacity, Limits}; diff --git a/wai/wai-quantum/schemas/landscape.v1.json b/wai/wai-quantum/schemas/landscape.v1.json index 1c34406db..6b7691ad2 100644 --- a/wai/wai-quantum/schemas/landscape.v1.json +++ b/wai/wai-quantum/schemas/landscape.v1.json @@ -173,6 +173,9 @@ "uncertain_span": { "type": "integer" }, + "sweeps": { + "type": "integer" + }, "chains_per_call": { "type": "integer" } @@ -848,6 +851,9 @@ "uncertain_span": { "type": "integer" }, + "sweeps": { + "type": "integer" + }, "chains_per_call": { "type": "integer" } @@ -1100,6 +1106,9 @@ "uncertain_span": { "type": "integer" }, + "sweeps": { + "type": "integer" + }, "chains_per_call": { "type": "integer" } @@ -1308,6 +1317,9 @@ "uncertain_span": { "type": "integer" }, + "sweeps": { + "type": "integer" + }, "chains_per_call": { "type": "integer" } diff --git a/wai/wai-quantum/src/quantum_landscape/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs index 13d85b212..035afa498 100644 --- a/wai/wai-quantum/src/quantum_landscape/capacity.rs +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -10,11 +10,19 @@ //! 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, the cases the set held -//! before it grew to reach large jobs, so a member cannot be certified by -//! declaring itself too small to be held to anything. +//! 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}; @@ -61,7 +69,11 @@ pub struct Capacity { /// 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 chains it takes in one call. The host gives it no more. + /// 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, } @@ -96,18 +108,34 @@ fn group(v: impl Into) -> String { 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, chains_per_call: None }; + 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 is `temperatures` empty. A pure - /// function of the job and the declared numbers. + /// 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 @@ -163,9 +191,52 @@ impl Capacity { Ok(()) } - /// Whether `count` chains go to it in one call. - pub fn takes_chains(&self, count: u64) -> bool { - self.chains_per_call.is_none_or(|c| count <= c) + /// 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 as usize).clamp(1, host_most.max(1))) } /// Where a declared number is under the floor's, a sentence for each. @@ -189,6 +260,7 @@ impl Capacity { } 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 } @@ -222,6 +294,7 @@ impl Capacity { 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) } @@ -265,54 +338,289 @@ impl Capacity { if !above.is_empty() { parts.push(format!("above temperature 0, {}", above.join(", "))); } - if let Some(c) = self.chains_per_call { - parts.push(format!("{} chains a call", group(c))); + if let Some(c) = self.sweeps { + parts.push(format!("at most {} sweeps a run", group(c))); } - Some(format!("Its parity holds within the capacity it declares, and jobs past it go elsewhere: {}.", parts.join("; "))) + let batch = self.chains_per_call.map(|c| format!("It takes at most {} chains a call, and the host splits a job, as certification split every vector, into calls of that many.", group(c))); + let limits = (!parts.is_empty()).then(|| format!("Its parity holds within the capacity it declares, and jobs past it go elsewhere: {}.", 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. +/// 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 mut f = Capacity { cells: Some(0), couplings: Some(0), at_zero: Limits { field: Some(0), field_bound: Some(0), cost: Some(0) }, above_zero: Limits { field: Some(0), field_bound: Some(0), cost: Some(0) }, table_entries: Some(0), uncertain_span: Some(0), chains_per_call: Some(0) }; - let up = |slot: &mut Option, v: i64| *slot = Some(slot.unwrap_or(0).max(v)); - let upu = |slot: &mut Option, v: u64| *slot = Some(slot.unwrap_or(0).max(v)); - let mut take = |w: &Workload, temperatures: &[f64], chains: u64| { - let zero = vec![0i8; w.n()]; - let field = (0..w.n()).map(|i| w.field(&zero, i).abs()).max().unwrap_or(0); - upu(&mut f.cells, w.n() as u64); - upu(&mut f.couplings, w.nonzeros()); - upu(&mut f.chains_per_call, chains); - let hot: Vec = temperatures.iter().copied().filter(|&t| t > 0.0).collect(); - if temperatures.is_empty() || hot.len() < temperatures.len() { - up(&mut f.at_zero.field, field); - up(&mut f.at_zero.field_bound, w.field_bound()); - up(&mut f.at_zero.cost, w.cost()); + 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") + }) +} + +// ── edge vectors ──────────────────────────────────────────────────────────────────────────── + +/// The most a job can reach on each axis (§2.0). A declared number at or above +/// one removes no job, and needs no vector of its own. +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 vectors certification adds for a declared capacity, at its edges +/// ([`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 !hot.is_empty() { - up(&mut f.above_zero.field, field); - up(&mut f.above_zero.field_bound, w.field_bound()); - up(&mut f.above_zero.cost, w.cost()); - upu(&mut f.table_entries, reach::reach(w).entries); - upu(&mut f.uncertain_span, uncertain_span(w, hot.iter().copied().fold(f64::MIN, f64::max))); + 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 once drawn cold, 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) -> 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)], 0)) +} + +/// 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 +} + +impl Capacity { + /// The vectors certification adds for this capacity: for each number it + /// declares below what a job can reach ([`reachable`]), workloads at that + /// number, each also within every other number it declares, and for a batch + /// one call of exactly that many chains. Deterministic, the reference their + /// oracle. A capacity that declares nothing below what a job can reach, and + /// no batch, has none. + pub fn edges(&self) -> Edges { + use conformance::{BatteryCase, ChainCase, SettleCase, Temperatures}; + let mut e = Edges::default(); + 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 cells_cap = self.cells.unwrap_or(reachable::CELLS).min(reachable::CELLS) as usize; + let pairs_cap = (self.couplings.unwrap_or(reachable::COUPLINGS).min(reachable::COUPLINGS) / 2) as usize; + let sweeps_cap = self.sweeps.unwrap_or(reachable::SWEEPS).min(reachable::SWEEPS) as u32; + let side = |l: &Limits| { + let f = l.field.unwrap_or(reachable::FIELD).min(reachable::FIELD); + let b = l.field_bound.unwrap_or(reachable::FIELD_BOUND).min(reachable::FIELD_BOUND); + let c = l.cost.unwrap_or(reachable::FIELD).min(reachable::FIELD); + (f, b, c) + }; + let (fz, bz, _) = side(&self.at_zero); + let (fa, ba, _) = side(&self.above_zero); + // The widest coupling a cell may have, with a field of 3, on both sides. + let degree = (bz.min(ba) - 3).clamp(1, cells_cap.max(2) as i64 - 1) as usize; + let mut settle = |name: String, l: Landscape, bound: u32| { + let w = l.lower(); + let bound = bound.min(sweeps_cap).max(1); + if self.admits_settle(&w, bound).is_ok() { + e.settle.push(SettleCase { name, start: vec![0; l.len()], landscape: l, bound }); } }; - for c in conformance::core_settle_cases() { - take(&c.landscape.lower(), &[], 1); + let mut chains: Vec = Vec::new(); + let mut chain = |name: String, l: Landscape, temperatures: Temperatures, restarts: u64| { + if self.admits(&l.lower(), &temperatures.each()).is_ok() { + chains.push(ChainCase { name, landscape: l, temperatures, seed: 0xED6E, restarts }); + } + }; + let hot = |t: f64, k: u32| Temperatures::Held(vec![(t, k)]); + let quench = Temperatures::Cooling { t0: 0.0, sweeps: 5 }; + let mut battery: Vec = Vec::new(); + let mut law = |name: &'static str, l: Landscape, t: f64| { + let temps = vec![t; 32]; + if self.admits(&l.lower(), &temps).is_ok() { + battery.push(BatteryCase::of(name, l, temps)); + } + }; + + // Cells: a wave down every one of them, and every one of them drawn. + if let Some(c) = below_u(self.cells, reachable::CELLS) { + let c = c as usize; + let l = banded(c, (c - 1).min(pairs_cap), 2); + settle(format!("edge/cells-{c}"), l.clone(), 2 * c as u32 + 2); + chain(format!("edge/cells-{c}/hot"), l, hot(1.5, 3), 4); } - for c in conformance::core_chain_cases() { - take(&c.landscape.lower(), &c.temperatures.each(), c.restarts); + // Couplings: as many as it declares, as near neighbours as its field bounds allow. + if let Some(p) = below_u(self.couplings, reachable::COUPLINGS) { + let pairs = (p / 2) as usize; + if pairs > 0 { + let n = (pairs + 1).min(cells_cap).max(2); + let l = banded(n, pairs, degree); + settle(format!("edge/couplings-{p}"), l.clone(), 2 * n as u32 + 2); + chain(format!("edge/couplings-{p}/hot"), l, hot(1.5, 3), 4); + } } - for c in conformance::core_battery() { - take(&c.landscape.lower(), &c.temperatures, 1); + // At temperature 0: the field bound with couplings, the field alone, and the cost. + if let Some(b) = below_i(self.at_zero.field_bound, reachable::FIELD_BOUND) { + let f = (b - 2).min(fz).max(0); + for s in [f, -f] { + settle(format!("edge/field-bound-{b}-at-0/{s}"), star(s), 20); + chain(format!("edge/field-bound-{b}-at-0/{s}/quench"), star(s), quench.clone(), 4); + } } - f - }) + if let Some(f) = below_i(self.at_zero.field, reachable::FIELD) { + let f = f.min(bz); + settle(format!("edge/field-{f}-at-0"), singles(&[f, -f, 1], 0), 5); + chain(format!("edge/field-{f}-at-0/quench"), singles(&[f, -f, 1], 0), quench.clone(), 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); + settle(format!("edge/cost-{c}-at-0"), l.clone(), 5); + chain(format!("edge/cost-{c}-at-0/quench"), l, quench.clone(), 4); + } + // Above temperature 0: the same, each field drawn at a temperature of its own size, where + // its draw is uncertain; and drawn cold, at 1/64, where a draw a unit from its threshold is + // certain, so that a field reached exactly is drawn exactly whatever the draws. + let cold = |k: u32| Temperatures::Held(vec![(0.015625, k)]); + 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] { + chain(format!("edge/field-bound-{b}-above-0/{s}"), singles(&[s], 0), hot(f.max(1) as f64, 3), 8); + } + let f2 = (b - 2).min(fa).max(0); + chain(format!("edge/field-bound-{b}-above-0/star"), star(f2), hot(f2.max(1) as f64, 4), 32); + for sign in [1, -1] { + if let Some(l) = pinned(b, sign, fa) { + chain(format!("edge/field-bound-{b}-above-0/pinned/{sign}"), l, cold(4), 4); + } + } + law("edge/field-bound-above-0", singles(&[f], 0), f.max(1) as f64); + } + if let Some(f) = below_i(self.above_zero.field, reachable::FIELD) { + let f = f.min(ba); + for s in [f, -f] { + chain(format!("edge/field-{f}-above-0/{s}"), singles(&[s], 0), hot(f.max(1) as f64, 3), 8); + } + chain(format!("edge/field-{f}-above-0/cold"), singles(&[f, -f], 0), cold(3), 4); + law("edge/field-above-0", singles(&[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; + chain(format!("edge/cost-{c}-above-0"), singles(&[0], c), hot(t, 3), 8); + let f = fa.min(ba).min(c); + chain(format!("edge/cost-{c}-above-0/at-the-field"), singles(&[f], c), hot(t, 3), 8); + // Each side of the threshold the cost sets, a unit away, drawn cold. + let g = c.saturating_add(1).min(fa).min(ba); + chain(format!("edge/cost-{c}-above-0/cold"), singles(&[g, g - 1, -g, 1 - g], c), cold(3), 4); + law("edge/cost-above-0", singles(&[0], c), t); + } + // Above temperature 0: a table of as many entries as it declares, and a span as wide. + if let Some(t) = below_u(self.table_entries, reachable::TABLE_ENTRIES) { + let f = fa.min(ba).max(0) as u64; + // Uncoupled cells two fields apart: one entry each. + let n = (t as usize).min(cells_cap).min(f as usize + 1).max(1); + let fields: Vec = (0..n as i64).map(|i| 2 * i - (n as i64 - 1)).collect(); + chain(format!("edge/table-entries-{t}"), singles(&fields, 0), hot(1.0, 3), 4); + } + if let Some(s) = below_u(self.uncertain_span, reachable::UNCERTAIN_SPAN) { + let m = fa.min(ba).max(0); + let (a, z) = if (s as i64 - 1) <= 2 * m { (-((s as i64 - 1) / 2), -((s as i64 - 1) / 2) + s as i64 - 1) } else { (-m, m) }; + let l = singles(&[a, z], 0); + chain(format!("edge/uncertain-span-{s}"), l.clone(), hot(s.max(1) as f64, 3), 8); + law("edge/uncertain-span", l, s.max(1) as f64); + } + // 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) { + chain(format!("edge/sweeps-{s}"), long_ring(), hot(20.0, s as u32), 2); + } + // A batch: one call of exactly as many chains as the host gives it, on two cells. + if self.chains_per_call.is_some() { + let k = self.batch(super::CHUNK_CHAINS.min(super::CHUNK_CELLS / 2)); + chain(format!("edge/a-call-of-{k}-chains"), landscape(2, &[(0, 1, -1)], &[(1, 1)], 0), hot(1.0, 2), k as u64); + } + e.chains = chains; + e.battery = battery; + e + } } #[cfg(test)] @@ -339,8 +647,9 @@ mod tests { 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, Some(8), "a run of 8 starts in one call"); + 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); } @@ -396,5 +705,55 @@ mod tests { assert!(Capacity::NONE.words().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 goes elsewhere for it, and the words say so. + let batch = Capacity { chains_per_call: Some(100), ..Capacity::NONE }; + let words = batch.words().unwrap(); + assert!(!words.contains("go elsewhere") && words.contains("splits a job"), "{words}"); + let both = Capacity { cells: Some(64), chains_per_call: Some(100), ..Capacity::NONE }; + let words = both.words().unwrap(); + assert!(words.contains("go elsewhere: at most 64 cells.") && words.contains("at most 100 chains a call"), "{words}"); + } + + /// Every number declared below what a job can reach has vectors at it, + /// within every other number declared; none for what removes no job. + #[test] + fn every_declared_number_has_vectors_at_its_edge() { + let names = |e: &Edges| -> Vec { e.settle.iter().map(|c| c.name.clone()).chain(e.chains.iter().map(|c| c.name.clone())).collect() }; + assert!(names(&Capacity::NONE.edges()).is_empty(), "the reference declares none"); + // A batch: every vector split into calls of it, and one call of exactly that many. + let e = Capacity { chains_per_call: Some(100), ..Capacity::NONE }.edges(); + assert_eq!((names(&e), e.chains[0].restarts), (vec!["edge/a-call-of-100-chains".to_string()], 100)); + // 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 == "edge/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. + 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), "{:?}", names(&e)); + assert!(e.chains.iter().any(|c| c.landscape.lower().field_bound() == 39_999 && c.temperatures.each().iter().all(|&t| t > 0.0)), "{:?}", names(&e)); + // A schedule length: a schedule that long. + let e = Capacity { sweeps: Some(7000), ..Capacity::NONE }.edges(); + assert!(e.chains.iter().any(|c| c.temperatures.each().len() == 7000)); + // The FPGA reference design's capacity: each of its numbers, and every edge 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 ["edge/cells-64", "edge/field-524287-above-0/524287", "edge/cost-1048575-above-0", "edge/uncertain-span-8192"] { + assert!(n.iter().any(|x| x == want), "{want} in {n:?}"); + } + assert!(e.chains.iter().any(|c| uncertain_span(&c.landscape.lower(), c.temperatures.each().iter().copied().fold(0.0, f64::max)) == 8192)); + 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. + 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!(names(&gpu.edges()).iter().all(|x| x.starts_with("edge/field-bound-2147483647")), "{:?}", names(&gpu.edges())); } } diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index df778ee65..a9036f10c 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -29,9 +29,13 @@ //! declared capacity. //! //! The core set is the part of these the set held before it grew to reach -//! large jobs ([`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`]). +//! 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. @@ -40,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}; @@ -218,14 +223,17 @@ pub fn core_settle_cases() -> Vec { } /// Whether a member's settle kernel gives every `settle` vector within its -/// declared capacity exactly. One past it is not run. +/// 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 { - let cap = m.capacity(); - 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 fixed = settle_cases().into_iter().filter(|c| cap.admits_settle(&c.landscape.lower(), c.bound).is_ok()); + fixed.chain(cap.edges().settle).all(|c| { let w = c.landscape.lower(); - if cap.admits(&w, &[]).is_err() { - return true; - } 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) }) @@ -341,11 +349,16 @@ impl ChainCase { 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. +/// 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().take(15).collect() + 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)); @@ -377,6 +390,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() { @@ -390,16 +419,29 @@ 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 { - let cap = m.capacity(); - 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()); + fixed.chain(cap.edges().chains).all(|c| { let w = c.landscape.lower(); let temperatures = c.temperatures.each(); - if cap.admits(&w, &temperatures).is_err() || !cap.takes_chains(c.restarts) { - return true; - } 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 }) } @@ -475,7 +517,8 @@ pub struct BatteryCase { } 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 } } @@ -688,12 +731,20 @@ 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 { - let cap = m.capacity(); - let per_call = cap.chains_per_call.map_or(4096, |c| c.clamp(1, 4096) as usize); + run_battery_within(m, &m.capacity()) +} + +/// [`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 per_call = cap.batch(4096); battery() - .iter() + .into_iter() + .chain(cap.edges().battery) .enumerate() .map(|(k, case)| { + let case = &case; let w = case.landscape.lower(); 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 }; diff --git a/wai/wai-quantum/src/quantum_landscape/job.rs b/wai/wai-quantum/src/quantum_landscape/job.rs index 4b4039570..0465cd96c 100644 --- a/wai/wai-quantum/src/quantum_landscape/job.rs +++ b/wai/wai-quantum/src/quantum_landscape/job.rs @@ -101,9 +101,11 @@ pub struct Prepared { temperature: Option, seeds: u32, temperatures: Vec, - /// Every schedule the job runs a member on: a settle's is empty, a run of - /// chains has a temperature a sweep. A member's capacity is held to each. + /// 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>, } @@ -162,12 +164,17 @@ 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 mut schedules: Vec> = Vec::new(); + let (mut schedules, mut settles): (Vec>, Vec) = (Vec::new(), Vec::new()); for (plan, _) in &runs { - let mut each = vec![match plan { - Plan::Settle { .. } => Vec::new(), - _ => plan.temperatures(), - }]; + 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]); @@ -181,7 +188,7 @@ pub fn prepare(op: &str, request: &Value, max_bytes: u64) -> Result = 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, schedules, kernels })) + Ok(Some(Prepared { job: Job::new(p), op: op.into(), seed, temperature, seeds, temperatures, schedules, settles, kernels })) } /// Answer a prepared job on a certified member. @@ -244,7 +251,7 @@ 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 Some(why) = p.schedules.iter().find_map(|t| c.capacity().admits(&p.job.workload, t).err()) { + } 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 { match answer(c, &p) { diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index 0edae1d4d..7f9b4962c 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -251,10 +251,12 @@ pub fn certify(m: &dyn Member) -> Result, String> { if !under.is_empty() { return Err(format!("{} declares a capacity under the floor every member is held to: {}", m.id(), under.join("; "))); } - let settle = if conformance::settle_vectors_hold(m) { Parity::BitExact } else { Parity::Uncertified }; - let sample = if conformance::chain_vectors_hold(m) { + // 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 diff --git a/wai/wai-quantum/src/quantum_landscape/run.rs b/wai/wai-quantum/src/quantum_landscape/run.rs index 2f1c3ac9e..a5c3f7488 100644 --- a/wai/wai-quantum/src/quantum_landscape/run.rs +++ b/wai/wai-quantum/src/quantum_landscape/run.rs @@ -325,7 +325,7 @@ fn execute(job: &Job, plan: Plan, seed: u64, c: &Certified<'_>) -> Result { // The host gives a member only what it takes, however it was reached. - c.capacity().admits(w, &[]).map_err(refused)?; + c.capacity().admits_settle(w, sweeps).map_err(refused)?; let o = m.settle(w, &vec![0i8; n], sweeps).map_err(refused)?; member::check_settle(&o, w, sweeps).map_err(refused)?; let energy = job.program.landscape.energy(&o.state); @@ -356,7 +356,7 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 let cap = c.capacity(); cap.admits(w, temperatures).map_err(refused)?; // No more chains in a call than the member declares it takes. - let batch = (CHUNK_CELLS / n.max(1)).clamp(1, CHUNK_CHAINS).min(cap.chains_per_call.map_or(usize::MAX, |c| (c as usize).max(1))); + 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; diff --git a/wai/wai-quantum/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index c6fa8e90a..a94cb1038 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -1,12 +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::capacity::{Capacity, Limits}; +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}; @@ -386,6 +388,9 @@ enum Fault { 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. @@ -455,7 +460,11 @@ impl Fault { } 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 counter = match self { + Fault::CounterRestartsEachWindow(at) => k % at, + Fault::DeviceWindowRestart => k % reach::window(w, &reach::reach(w), true), + _ => k, + }; let j = (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); @@ -532,40 +541,174 @@ impl Member for Fault { } } -/// With no fault the stand-in is the reference, so each fault below is the only difference. +/// 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() + } + 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()) } + } + + /// 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) +} + +/// 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))), + ]; + 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.iter().any(|(a, _)| *a == at) { + windows.push((at, c.name)); + } + } + 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 twice: declaring no capacity, and +/// declaring the most hostile capacity it may, the least at or above the floor +/// that still takes a job it gets wrong. #[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)); - 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 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() { - let w = c.landscape.lower(); - let at = reach::window(&w, &reach::reach(&w), true); - if at < c.temperatures.each().len() { - windows.push(at); - } - } - 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:?}"); + for (f, kernel, witness) in faults() { + assert!(!holds(&f, kernel), "{f:?}"); + let cap = witness.hostile(); + assert!(cap.under(&floor()).is_empty()); + 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()); } } @@ -614,3 +757,223 @@ 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); +} -- 2.54.0 (Apple Git-157) From df2f96781e8b844f24039ba632281f354260f1b1 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 11:15:22 -0500 Subject: [PATCH 04/15] wai-quantum: certification chooses vectors by rule for every capacity, and the host audits every bit-exact answer The second adversarial check of #678 found ten more fault classes that a finite set misses, each another axis sampled at a few points. So: - Capacity::generate makes vectors by rule from any capacity, none included: every cell count at and beside a power of two up to what a workload can hold (4,096 with none), every cell moving; every call size to 32 and each band of a wavefront past it, and a whole batch; the longest row, its whole length needed; 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; a run past draw index 2^24. The edges reach a declared table and span exactly, and the field bound at 0 by a field alone. Battery workloads at a declared cell count, couplings and schedule length read a few blocks of many (BatteryCase::read). The cost is bounded and pinned by a test: at most 200 vectors. - The audit (spec 7.3): the last chain of every call and one chain chosen by the job's digest run again on the reference, and every settle within a work budget. At the first difference the member is demoted to uncertified, the job answered from the reference, and later answers carry member.demoted. - The reference's sampler skips sweeps at 0 after a fixed point; its bits are unchanged. - The CUDA chains kernel reads its table only within its length: a field past it, which only a fault gives, leaves a cell no answer passes, and no illegal address poisons the context. - Text: a decline is the host's; a member run by name claims no grade; the FPGA workflow's greps follow the renamed lines; Breaking lines for every API break against main. - Every fault test also runs under realistic capacities; the review's probes are kept as tests; spec 5, 5.1, 5.3, 7, 7.1, 7.2 and a new 7.3. Co-Authored-By: Claude Opus 5.5 --- .../workflows/wai-quantum-landscape-fpga.yml | 6 +- .github/workflows/wai-quantum-landscape.yml | 16 +- .../2026-11-07-landscape-declared-capacity.md | 9 + wai/extensions/quantum-landscape.md | 135 ++- wai/wai-quantum-landscape-fpga/README.md | 17 +- wai/wai-quantum-landscape-fpga/src/member.rs | 2 +- wai/wai-quantum/docs/landscape.md | 21 +- wai/wai-quantum/schemas/landscape.v1.json | 16 + .../src/quantum_landscape/capacity.rs | 866 +++++++++++++++--- .../src/quantum_landscape/conformance.rs | 69 +- wai/wai-quantum/src/quantum_landscape/cuda.rs | 11 +- wai/wai-quantum/src/quantum_landscape/job.rs | 23 +- .../src/quantum_landscape/member.rs | 56 +- wai/wai-quantum/src/quantum_landscape/run.rs | 62 +- .../src/quantum_landscape/sample.rs | 10 +- .../tests/landscape_conformance.rs | 510 ++++++++++- 16 files changed, 1597 insertions(+), 232 deletions(-) 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 182db0f48..6919a3e7e 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -125,11 +125,14 @@ 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\. 100 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (100 expected)"; exit 1; } + grep -qE '^test result: ok\. 103 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (103 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::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 \ @@ -234,7 +237,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\. 17 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (17 expected)"; exit 1; } + grep -qE '^test result: ok\. 25 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (25 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 @@ -309,9 +312,10 @@ 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; } - # The review's probes test certification, 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_ 2>&1 | tee "$RUNNER_TEMP/vectors.log" - grep -qE '^test result: ok\. 12 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } + # The reviews' probes and the audit's tests test certification and the audit, 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_ 2>&1 | tee "$RUNNER_TEMP/vectors.log" + grep -qE '^test result: ok\. 12 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } done wasip2: @@ -360,7 +364,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\. 99 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (99 expected)"; exit 1; } + grep -qE '^test result: ok\. 102 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (102 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 index 82e7a514f..86195ff31 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -5,3 +5,12 @@ - **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 chosen by the job's digest 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`. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index f16613640..13bbcb0de 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -435,24 +435,25 @@ sweeps (the core set's long schedule, §7.1). It declares no batch: any number o 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. -**Edge vectors.** A declared number takes vectors out of what a member is held to, and -the set samples each number at a few points only. So 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), the host adds vectors at exactly that number, each also within every other -number declared: built from the capacity alone and the same on every host, with the -reference's bits as their expected bits. They are a wave settling down that many cells -and a hot chain on them; that many couplings, each cell's as near as the declared field -bounds allow; on each side of temperature 0, a field bound reached through couplings and -by a field alone, the largest field and the largest cost, drawn above 0 both at a -temperature of their own size, where the draw is uncertain, and cold (1/64), where a -field reached exactly is drawn exactly; a table of that many entries; two fields that -span the uncertain span, drawn at a temperature as large; a schedule of that many sweeps; -and, for a batch, one call of exactly that many chains. The battery gets the same -workloads above temperature 0. So a grade is earned at the edge of a capacity, not at -the nearest vector inside it. A capacity that declares no number below what a job can -reach, and no batch, adds none. +**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), within @@ -517,13 +518,16 @@ is declared, so that the reference's answers are what they were). A parity is a | parity | granted when | |---|---| | `reference` | to the host's own reference kernels, and to nothing else | -| `bit-exact` | settle: the member gives every `settle` vector within its capacity, and every one at its edges (§5), exactly. Sample: it gives every `chains` vector within its capacity and at its edges 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 at its edges passes (§7.2), in calls of the same size | +| `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 | 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. +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. @@ -559,7 +563,9 @@ priced or read from a meter, and that no quantum effect is involved where none i 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); and, for a member that declares a capacity, that -its parity holds within that capacity, in its numbers (§5). +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 @@ -584,8 +590,9 @@ The **core set** is the part of it the set held before it grew to reach large jo 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 at a declared -capacity's edges (§5) are not in the file: a host builds them from the capacity. +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 @@ -622,6 +629,50 @@ capacity's edges (§5) are not in the file: a host builds them from the capacity `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; + - 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, one workload with 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. 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 + where both sides take it, the shape an annealing job ends in; + - a corner on each side of 0: `C` cells with every coupling `P` allows, fields at the + largest field of both signs, a field bound reached exactly, and the largest cost; + - 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; and a + schedule of exactly that many sweeps; + - for the battery (§7.2), at a declared cell count, `C` alike cells in blocks of three, + read at the cell counts above; at declared couplings, alike triangles; at a declared + schedule length, one alike triangle warm and then cold, to 1,024 sweeps; at a + declared field bound, field, cost and span, the workloads above. Identical blocks + cannot reach a table, so a table has none. + + The rules bound what certification costs, whatever the capacity: at most 200 generated + vectors, at most 300 million cell sweeps hot or settled (nearly all of them battery + workloads, run only for a kernel that is not `bit-exact`), and at most 20 million at + temperature 0 (the run past `2²⁴`, at a fixed point, which the reference skips). The + exact counts are pinned by a test. - `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 @@ -635,7 +686,9 @@ capacity's edges (§5) are not in the file: a host builds them from the capacity 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 @@ -662,6 +715,38 @@ 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. 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: the last chain of every call to the member, and one chain of each run chosen + by the job's digest (the low 64 bits of BLAKE3 over the program, the seed and a purpose + number), so that the choice can be repeated; +- settling: the whole settle, when its work, sweeps times couplings and cells, is at most + 2²⁴; past that, one job in 8, chosen by the job's digest. The checks of §5 hold as before. + +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. + +Its cost: at most one chain for each call and one for each run, so for a run of `R` +chains in calls of `B` at most `(⌈R/B⌉ + 1)/R` of its sampling work again; for a settle +within the budget, that settle's work again, and past it one in 8. What it catches: a +fault that makes a fraction `q` of a run's chains wrong at places the digest does not +favour, on each such job with a chance of at least `q`; 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, on the first job that shows it within the +budget, and past the budget within `N` such jobs with a chance of `1 − (7/8)^N` (0.74 at +10 jobs, 0.99 at 35). 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. A fault that only ever touches chains the audit does +not pick, on settles past the budget, can go unseen. 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/wai-quantum-landscape-fpga/README.md b/wai/wai-quantum-landscape-fpga/README.md index f9ddfe7a5..5c0764617 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -58,10 +58,14 @@ 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 within its declared capacity, and every case -the host generates at each number it declares (a wave down 64 cells, fields at ±524,287 and a cost -of 1,048,575 drawn hot and cold, two fields 8,192 apart drawn at temperature 8,192), comes back bit -for bit (and, for a sampling kernel that misses, trying the statistical battery within it). +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. **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 @@ -393,8 +397,9 @@ with its couplings fixed in the logic. 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. - **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 a job past - them goes to another member (above). At temperature 0 large fields are mapped exactly; more cells + 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 diff --git a/wai/wai-quantum-landscape-fpga/src/member.rs b/wai/wai-quantum-landscape-fpga/src/member.rs index 5832fd2ed..c204b75e1 100644 --- a/wai/wai-quantum-landscape-fpga/src/member.rs +++ b/wai/wai-quantum-landscape-fpga/src/member.rs @@ -307,7 +307,7 @@ impl BoardMember

{ }; self.settle_parity = settle; self.sample_parity = sample; - Ok(format!("certified by the host on the conformance set, within its declared capacity: settle {}, sample {}. {}\n", settle.label(), sample.label(), CAPACITY.words().unwrap_or_default())) + 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())) } } diff --git a/wai/wai-quantum/docs/landscape.md b/wai/wai-quantum/docs/landscape.md index 732ccb5d2..c02ed0662 100644 --- a/wai/wai-quantum/docs/landscape.md +++ b/wai/wai-quantum/docs/landscape.md @@ -236,12 +236,21 @@ of temperature 0, its largest field, field bound and cost; above 0, how many cut 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), certifies it on every vector and battery workload within -them and on vectors generated at each number it declares (`Capacity::edges`), and states them 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. +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 the last chain +of every call and one chain of each run chosen by the job's digest again on the reference, and the +whole settle where its work is at most 2²⁴ (past that, one job in 8). 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`). ```rust norun=account use wai_quantum::quantum_landscape::capacity::{Capacity, Limits}; diff --git a/wai/wai-quantum/schemas/landscape.v1.json b/wai/wai-quantum/schemas/landscape.v1.json index 6b7691ad2..32865a288 100644 --- a/wai/wai-quantum/schemas/landscape.v1.json +++ b/wai/wai-quantum/schemas/landscape.v1.json @@ -129,6 +129,10 @@ ], "description": "What the member has earned for this kernel." }, + "demoted": { + "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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", @@ -807,6 +811,10 @@ } } }, + "demoted": { + "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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", @@ -1062,6 +1070,10 @@ } } }, + "demoted": { + "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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", @@ -1273,6 +1285,10 @@ ], "description": "What the member has earned for this kernel." }, + "demoted": { + "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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", diff --git a/wai/wai-quantum/src/quantum_landscape/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs index 035afa498..9d82038da 100644 --- a/wai/wai-quantum/src/quantum_landscape/capacity.rs +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -236,7 +236,7 @@ impl Capacity { /// 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 as usize).clamp(1, host_most.max(1))) + 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. @@ -299,8 +299,10 @@ impl Capacity { Value::Object(o) } - /// The declared numbers in words, for an answer's label; `None` where none is declared. - pub fn words(&self) -> Option { + /// 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; } @@ -341,8 +343,20 @@ impl Capacity { if let Some(c) = self.sweeps { parts.push(format!("at most {} sweeps a run", group(c))); } - let batch = self.chains_per_call.map(|c| format!("It takes at most {} chains a call, and the host splits a job, as certification split every vector, into calls of that many.", group(c))); - let limits = (!parts.is_empty()).then(|| format!("Its parity holds within the capacity it declares, and jobs past it go elsewhere: {}.", parts.join("; "))); + 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(" ")) } } @@ -365,10 +379,10 @@ pub fn floor() -> Capacity { }) } -// ── edge vectors ──────────────────────────────────────────────────────────────────────────── +// ── generated vectors ─────────────────────────────────────────────────────────────────────── /// The most a job can reach on each axis (§2.0). A declared number at or above -/// one removes no job, and needs no vector of its own. +/// one removes no job. pub mod reachable { use super::super::{MAX_FIELD, MAX_PROGRAM_BYTES, MAX_STEPS, MAX_VARS}; /// Cells. @@ -388,8 +402,59 @@ pub mod reachable { pub const SWEEPS: u64 = MAX_STEPS as u64; } -/// The vectors certification adds for a declared capacity, at its edges -/// ([`Capacity::edges`]): their expected bits are the reference's. +/// 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 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 and the blocks a battery twin reads: +/// 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, @@ -446,12 +511,12 @@ fn star(b: i64) -> Landscape { } /// 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 once drawn cold, 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) -> Option { +/// 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)], 0)) + (p >= 1 && f0 >= 0).then(|| landscape(3, &[(0, 1, 1), (0, 2, 1)], &[(0, sign * f0), (1, sign * p), (2, sign * p)], cost)) } /// Uncoupled cells with these fields, at cost `cost`. @@ -473,153 +538,582 @@ fn long_ring() -> Landscape { 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), + } + } +} + impl Capacity { - /// The vectors certification adds for this capacity: for each number it - /// declares below what a job can reach ([`reachable`]), workloads at that - /// number, each also within every other number it declares, and for a batch - /// one call of exactly that many chains. Deterministic, the reference their - /// oracle. A capacity that declares nothing below what a job can reach, and - /// no batch, has none. + 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 { - use conformance::{BatteryCase, ChainCase, SettleCase, Temperatures}; + 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); + } + // 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); + } + } + // 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)] { + let alone: Vec = wraps.iter().copied().filter(|&v| v <= s.field).collect(); + let coupled: Vec = wraps.iter().copied().filter(|&v| v <= s.bound && v - 2 <= s.field).collect(); + if alone.is_empty() && coupled.is_empty() { + continue; + } + let c = s.cost.min(wraps[0] - 1); + for cost in [0, c] { + let edge = cost + 1 <= s.field; + let mut l = Landscape::new(2 * alone.len() + 6 * coupled.len() + if edge { 2 } else { 0 }); + let mut at = 0; + for &v in &alone { + l.set_field(at, v).set_field(at + 1, -v); + at += 2; + } + for &v in &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; + } + } + // 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); + o.on_side(side, format!("lane/{side}/cost-{cost}"), &l, (1u64 << 15) as f64, 3); + if c == 0 { + break; + } + } + } + // A corner: every number at its most at once, on each side of 0. + for (side, s) in [("at-0", &r.zero), ("above-0", &r.hot), ("both", &r.both)] { + let n = r.cells.min(4096); + if n < 5 || !o.wants(side == "at-0", true) { + continue; + } + let f0 = s.field.min(s.bound); + let fill = banded(n - 5, r.pairs.saturating_sub(2), (s.bound - 1).clamp(1, 1 << 20) as usize); + let mut l = Landscape::new(n); + for i in 0..n - 5 { + for j in (i + 1)..n - 5 { + let c = fill.coupling(i, j); + if c != 0 { + l.couple(5 + i, 5 + j, c); + } + } + l.set_field(5 + i, if i % 2 == 0 { 1 } else { -1 }); + } + l.set_field(0, f0).set_field(1, -f0); + if let Some(p) = pinned(s.bound, 1, s.field, 0) { + l.couple(2, 3, 1).couple(2, 4, 1); + l.set_field(2, p.field()[0]).set_field(3, p.field()[1]).set_field(4, p.field()[2]); + } + l.set_cost(s.cost.min(f0.max(1))); + o.on_side(side, format!("corner/{side}"), &l, f0.max(1) as f64, 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 cells_cap = self.cells.unwrap_or(reachable::CELLS).min(reachable::CELLS) as usize; - let pairs_cap = (self.couplings.unwrap_or(reachable::COUPLINGS).min(reachable::COUPLINGS) / 2) as usize; - let sweeps_cap = self.sweeps.unwrap_or(reachable::SWEEPS).min(reachable::SWEEPS) as u32; - let side = |l: &Limits| { - let f = l.field.unwrap_or(reachable::FIELD).min(reachable::FIELD); - let b = l.field_bound.unwrap_or(reachable::FIELD_BOUND).min(reachable::FIELD_BOUND); - let c = l.cost.unwrap_or(reachable::FIELD).min(reachable::FIELD); - (f, b, c) - }; - let (fz, bz, _) = side(&self.at_zero); - let (fa, ba, _) = side(&self.above_zero); - // The widest coupling a cell may have, with a field of 3, on both sides. - let degree = (bz.min(ba) - 3).clamp(1, cells_cap.max(2) as i64 - 1) as usize; - let mut settle = |name: String, l: Landscape, bound: u32| { - let w = l.lower(); - let bound = bound.min(sweeps_cap).max(1); - if self.admits_settle(&w, bound).is_ok() { - e.settle.push(SettleCase { name, start: vec![0; l.len()], landscape: l, bound }); - } - }; - let mut chains: Vec = Vec::new(); - let mut chain = |name: String, l: Landscape, temperatures: Temperatures, restarts: u64| { - if self.admits(&l.lower(), &temperatures.each()).is_ok() { - chains.push(ChainCase { name, landscape: l, temperatures, seed: 0xED6E, restarts }); - } - }; - let hot = |t: f64, k: u32| Temperatures::Held(vec![(t, k)]); - let quench = Temperatures::Cooling { t0: 0.0, sweeps: 5 }; - let mut battery: Vec = Vec::new(); - let mut law = |name: &'static str, l: Landscape, t: f64| { - let temps = vec![t; 32]; - if self.admits(&l.lower(), &temps).is_ok() { - battery.push(BatteryCase::of(name, l, temps)); - } - }; - - // Cells: a wave down every one of them, and every one of them drawn. + 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(c) = below_u(self.cells, reachable::CELLS) { let c = c as usize; - let l = banded(c, (c - 1).min(pairs_cap), 2); - settle(format!("edge/cells-{c}"), l.clone(), 2 * c as u32 + 2); - chain(format!("edge/cells-{c}/hot"), l, hot(1.5, 3), 4); + if o.wants(true, false) { + let l = banded(c, (c - 1).min(r.pairs), 2); + o.settle(format!("edge/cells-{c}"), &l, &l.lower(), 2 * c as u32 + 2); + } } - // Couplings: as many as it declares, as near neighbours as its field bounds allow. if let Some(p) = below_u(self.couplings, reachable::COUPLINGS) { let pairs = (p / 2) as usize; - if pairs > 0 { - let n = (pairs + 1).min(cells_cap).max(2); + if pairs > 0 && o.wants(true, true) { + let n = (pairs + 1).min(r.cells).max(2); let l = banded(n, pairs, degree); - settle(format!("edge/couplings-{p}"), l.clone(), 2 * n as u32 + 2); - chain(format!("edge/couplings-{p}/hot"), l, hot(1.5, 3), 4); + let w = l.lower(); + o.settle(format!("edge/couplings-{p}"), &l, &w, 2 * n as u32 + 2); + 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 with couplings, the field alone, and the cost. + // 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) { - let f = (b - 2).min(fz).max(0); - for s in [f, -f] { - settle(format!("edge/field-bound-{b}-at-0/{s}"), star(s), 20); - chain(format!("edge/field-bound-{b}-at-0/{s}/quench"), star(s), quench.clone(), 4); + 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); - settle(format!("edge/field-{f}-at-0"), singles(&[f, -f, 1], 0), 5); - chain(format!("edge/field-{f}-at-0/quench"), singles(&[f, -f, 1], 0), quench.clone(), 4); + 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); - settle(format!("edge/cost-{c}-at-0"), l.clone(), 5); - chain(format!("edge/cost-{c}-at-0/quench"), l, quench.clone(), 4); + 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: the same, each field drawn at a temperature of its own size, where - // its draw is uncertain; and drawn cold, at 1/64, where a draw a unit from its threshold is - // certain, so that a field reached exactly is drawn exactly whatever the draws. - let cold = |k: u32| Temperatures::Held(vec![(0.015625, k)]); + // 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] { - chain(format!("edge/field-bound-{b}-above-0/{s}"), singles(&[s], 0), hot(f.max(1) as f64, 3), 8); + 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); - chain(format!("edge/field-bound-{b}-above-0/star"), star(f2), hot(f2.max(1) as f64, 4), 32); + 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) { - chain(format!("edge/field-bound-{b}-above-0/pinned/{sign}"), l, cold(4), 4); + 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); } } - law("edge/field-bound-above-0", singles(&[f], 0), f.max(1) as f64); + o.law(BatteryCase::of("edge/field-bound-above-0", singles(&[f], 0), vec![f.max(1) as f64; 32])); + 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] { - chain(format!("edge/field-{f}-above-0/{s}"), singles(&[s], 0), hot(f.max(1) as f64, 3), 8); + o.every_side(&format!("edge/field-{f}-above-0/{s}"), &singles(&[s], 0), f.max(1) as f64, 3, 8); } - chain(format!("edge/field-{f}-above-0/cold"), singles(&[f, -f], 0), cold(3), 4); - law("edge/field-above-0", singles(&[f], 0), f.max(1) as f64); + o.law(BatteryCase::of("edge/field-above-0", singles(&[f], 0), vec![f.max(1) as f64; 32])); } if let Some(c) = below_i(self.above_zero.cost, reachable::FIELD) { let t = c.max(1) as f64; - chain(format!("edge/cost-{c}-above-0"), singles(&[0], c), hot(t, 3), 8); let f = fa.min(ba).min(c); - chain(format!("edge/cost-{c}-above-0/at-the-field"), singles(&[f], c), hot(t, 3), 8); - // Each side of the threshold the cost sets, a unit away, drawn cold. + 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); - chain(format!("edge/cost-{c}-above-0/cold"), singles(&[g, g - 1, -g, 1 - g], c), cold(3), 4); - law("edge/cost-above-0", singles(&[0], c), t); + o.every_side(&format!("edge/cost-{c}-above-0/each-side"), &singles(&[g, g - 1, -g, 1 - g], c), t, 3, 4); + o.law(BatteryCase::of("edge/cost-above-0", singles(&[0], c), vec![t; 32])); } - // Above temperature 0: a table of as many entries as it declares, and a span as wide. - if let Some(t) = below_u(self.table_entries, reachable::TABLE_ENTRIES) { - let f = fa.min(ba).max(0) as u64; - // Uncoupled cells two fields apart: one entry each. - let n = (t as usize).min(cells_cap).min(f as usize + 1).max(1); - let fields: Vec = (0..n as i64).map(|i| 2 * i - (n as i64 - 1)).collect(); - chain(format!("edge/table-entries-{t}"), singles(&fields, 0), hot(1.0, 3), 4); + // 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); } if let Some(s) = below_u(self.uncertain_span, reachable::UNCERTAIN_SPAN) { - let m = fa.min(ba).max(0); - let (a, z) = if (s as i64 - 1) <= 2 * m { (-((s as i64 - 1) / 2), -((s as i64 - 1) / 2) + s as i64 - 1) } else { (-m, m) }; - let l = singles(&[a, z], 0); - chain(format!("edge/uncertain-span-{s}"), l.clone(), hot(s.max(1) as f64, 3), 8); - law("edge/uncertain-span", l, s.max(1) as f64); + let l = span_reaching(s, r.cells, r.pairs, fa.min(ba), ba); + let w = l.lower(); + let t = s.max(1) as f64; + o.chain(format!("edge/uncertain-span-{s}"), &l, &w, Held(vec![(t, 3)]), 8); + o.chain(format!("edge/uncertain-span-{s}/both"), &l, &w, Held(vec![(t, 2), (COLD, 1), (0.0, 1)]), 8); + if 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) { - chain(format!("edge/sweeps-{s}"), long_ring(), hot(20.0, s as u32), 2); + 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); } - // A batch: one call of exactly as many chains as the host gives it, on two cells. - if self.chains_per_call.is_some() { - let k = self.batch(super::CHUNK_CHAINS.min(super::CHUNK_CELLS / 2)); - chain(format!("edge/a-call-of-{k}-chains"), landscape(2, &[(0, 1, -1)], &[(1, 1)], 0), hot(1.0, 2), k as u64); + // The battery at the declared cells, couplings and sweeps: blocks of + // three, each held to its law by itself, the blocks read at the ladder's sizes. + let read = |blocks: usize, cells_each: usize| -> Vec { + let mut out: Vec = ladder_sizes(blocks * cells_each).into_iter().map(|c| (c - 1) / cells_each).collect(); + out.push(blocks / 2); + out.retain(|&b| b < blocks); + out.sort_unstable(); + out.dedup(); + out + }; + if let Some(c) = below_u(self.cells, reachable::CELLS) { + let blocks = (c as usize / 3).max(1); + let l = singles(&vec![1; 3 * blocks], 0); + o.law(BatteryCase { name: "edge/cells", landscape: l, temperatures: vec![1.0], law: Law::Settled, block: 3, read: read(blocks, 3) }); + } + 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: read(blocks, 3) }); + } + } + 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() }); } - e.chains = chains; - e.battery = battery; - e } } @@ -699,30 +1193,35 @@ mod tests { 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().unwrap(); + 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().is_none()); + 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 goes elsewhere for it, and the words say so. + // 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().unwrap(); - assert!(!words.contains("go elsewhere") && words.contains("splits a job"), "{words}"); + 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().unwrap(); - assert!(words.contains("go elsewhere: at most 64 cells.") && words.contains("at most 100 chains a call"), "{words}"); + 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; none for what removes no job. + /// within every other number declared. #[test] fn every_declared_number_has_vectors_at_its_edge() { - let names = |e: &Edges| -> Vec { e.settle.iter().map(|c| c.name.clone()).chain(e.chains.iter().map(|c| c.name.clone())).collect() }; - assert!(names(&Capacity::NONE.edges()).is_empty(), "the reference declares none"); - // A batch: every vector split into calls of it, and one call of exactly that many. - let e = Capacity { chains_per_call: Some(100), ..Capacity::NONE }.edges(); - assert_eq!((names(&e), e.chains[0].restarts), (vec!["edge/a-call-of-100-chains".to_string()], 100)); + 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 == "edge/cells-299").expect("a settle at 299 cells"); @@ -731,29 +1230,122 @@ mod tests { 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. + // 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), "{:?}", names(&e)); - assert!(e.chains.iter().any(|c| c.landscape.lower().field_bound() == 39_999 && c.temperatures.each().iter().all(|&t| t > 0.0)), "{:?}", names(&e)); - // A schedule length: a schedule that long. + 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!(e.chains.iter().any(|c| c.temperatures.each().len() == 7000)); - // The FPGA reference design's capacity: each of its numbers, and every edge within all of them. + 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 ["edge/cells-64", "edge/field-524287-above-0/524287", "edge/cost-1048575-above-0", "edge/uncertain-span-8192"] { - assert!(n.iter().any(|x| x == want), "{want} in {n:?}"); + for want in ["edge/cells-64", "edge/field-524287-above-0/524287", "edge/cost-1048575-above-0/hot", "edge/uncertain-span-8192"] { + assert!(n.iter().any(|x| x.starts_with(want)), "{want} in {n:?}"); } - assert!(e.chains.iter().any(|c| uncertain_span(&c.landscape.lower(), c.temperatures.each().iter().copied().fold(0.0, f64::max)) == 8192)); 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. + // 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!(names(&gpu.edges()).iter().all(|x| x.starts_with("edge/field-bound-2147483647")), "{:?}", names(&gpu.edges())); + 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()); + } + } + + /// 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/cold", "both"] { + for cost in [0, 32_766] { + let name = format!("lane/{}/cost-{cost}", side.split('/').next().unwrap()); + let c = e.chains.iter().find(|c| c.name.starts_with(&name) && c.name.contains(side.split('/').next_back().unwrap())).unwrap_or_else(|| panic!("{name} {side}")); + let w = c.landscape.lower(); + for v in [32_767i64, 32_768, 49_152, 65_536, 8_388_608, 2_147_483_648, 4_294_967_296] { + let zero = vec![0i8; w.n()]; + assert!((0..w.n()).any(|i| w.field(&zero, i) == v) && (0..w.n()).any(|i| w.field(&zero, i) == -v), "{name}: a cell at ±{v}"); + assert!((0..w.n()).any(|i| w.field(&zero, 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 numbers §7.1 states. + #[test] + fn generated_vectors_cost_what_the_spec_says() { + 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 }; + for (name, cap) in [("none", Capacity::NONE), ("floor", floor()), ("fpga", fpga), ("biggest", biggest)] { + let (mut vectors, mut cold, mut work) = (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) => work += u64::from(conformance::BATTERY_CHAINS) * c.landscape.len() as u64 * c.temperatures.len() as u64, + } + true + }); + eprintln!("generated for {name}: {vectors} vectors, {work} cell-sweeps hot or settled, {cold} at temperature 0"); + assert!(vectors <= 200, "{name}: {vectors}"); + assert!(work <= 300_000_000, "{name}: {work}"); + assert!(cold <= 20_000_000, "{name}: {cold}"); + } } } diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index a9036f10c..eaa65a084 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -231,12 +231,22 @@ pub fn settle_vectors_hold(m: &dyn Member) -> bool { /// [`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 fixed = settle_cases().into_iter().filter(|c| cap.admits_settle(&c.landscape.lower(), c.bound).is_ok()); - fixed.chain(cap.edges().settle).all(|c| { + let holds = |c: &SettleCase| { let w = c.landscape.lower(); 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 { @@ -428,7 +438,23 @@ pub fn chain_vectors_hold(m: &dyn Member) -> bool { /// 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()); - fixed.chain(cap.edges().chains).all(|c| { + 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(); let chains = c.chains(); @@ -442,7 +468,7 @@ pub fn chain_vectors_hold_within(m: &dyn Member, cap: &Capacity) -> bool { } } got == want - }) + } } fn chains_json() -> Vec { @@ -514,13 +540,17 @@ pub struct BatteryCase { pub temperatures: Vec, pub law: Law, pub block: usize, + /// The blocks whose final states are counted, in order; empty, every block. + /// A workload of many blocks reads a few, so that so many tests at once do + /// not fail a correct sampler by chance. + pub read: Vec, } impl 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. @@ -601,7 +631,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 } @@ -664,8 +699,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)), @@ -741,7 +776,7 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec let per_call = cap.batch(4096); battery() .into_iter() - .chain(cap.edges().battery) + .chain(cap.edges_of(super::capacity::Want::BATTERY).battery) .enumerate() .map(|(k, case)| { let case = &case; @@ -750,20 +785,24 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec 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).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(per_call) { - match m.chains(&w, chunk, &case.temperatures) { + let mut at = 0u64; + while at < u64::from(BATTERY_CHAINS) { + 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() { + for (c, &b) in counts.iter_mut().zip(&blocks) { c[index_of(&o.state[b * case.block..(b + 1) * case.block])] += 1; } } } _ => ok = false, } + at = to; } let expected = binned(case.law, case.block, &exact); let worst = counts diff --git a/wai/wai-quantum/src/quantum_landscape/cuda.rs b/wai/wai-quantum/src/quantum_landscape/cuda.rs index 1fd00fe1b..a66ab137d 100644 --- a/wai/wai-quantum/src/quantum_landscape/cuda.rs +++ b/wai/wai-quantum/src/quantum_landscape/cuda.rs @@ -67,7 +67,7 @@ __device__ __forceinline__ u64 splitmix(u64 s, u64 j) { return x ^ (x >> 31); } -extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* cols, const int* vals, const i64* fields, i64 cost, unsigned n, unsigned chains, const i64* offsets, unsigned k0, unsigned k1, const u64* seeds, const unsigned* sweeps, const u64* table, int* cells) { +extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* cols, const int* vals, const i64* fields, i64 cost, unsigned n, unsigned chains, const i64* offsets, unsigned k0, unsigned k1, const u64* seeds, const unsigned* sweeps, const u64* table, u64 entries, int* cells) { unsigned c = blockIdx.x * blockDim.x + threadIdx.x; if (c >= chains) return; int* s = cells + (u64)c * n; @@ -83,6 +83,9 @@ extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* i64 h = field_of(rows, cols, vals, fields, s, i); u64 u = splitmix(seed, (u64)k * n + i + 1) >> 11; u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i]; + // A field past the table can only come of a fault: the cell is left at a + // state no cell has, which the host refuses, and nothing past it is read. + if (e >= entries) { s[i] = 2; continue; } s[i] = u < table[2 * e] ? -1 : (u < table[2 * e + 1] ? 0 : 1); } } @@ -239,12 +242,14 @@ impl CudaLandscape { while k0 < temperatures.len() { let k1 = (k0 + window).min(temperatures.len()); let (sweeps, table) = device::window_sweeps(w, &reach, &temperatures[k0..k1]); + let entries = (table.len() / 2) as u64; let (sweeps, table) = (s.clone_htod(&sweeps).map_err(driver)?, s.clone_htod(&nonempty(&table)).map_err(driver)?); let (a0, a1) = (k0 as u32, k1 as u32); let mut a = s.launch_builder(&self.kernels.chains); - a.arg(&rows).arg(&cols).arg(&vals).arg(&fields).arg(&cost).arg(&n32).arg(&count32).arg(&offsets).arg(&a0).arg(&a1).arg(&seeds).arg(&sweeps).arg(&table).arg(&mut cells); + a.arg(&rows).arg(&cols).arg(&vals).arg(&fields).arg(&cost).arg(&n32).arg(&count32).arg(&offsets).arg(&a0).arg(&a1).arg(&seeds).arg(&sweeps).arg(&table).arg(&entries).arg(&mut cells); // SAFETY: 2·count·n cells, an offset a cell, a seed a chain, a word a sweep of the - // window, and the window's table of (a, b) pairs for every field a cell can reach. + // window, and the window's table of `entries` (a, b) pairs for every field a cell + // can reach, every read of it checked against `entries`. unsafe { a.launch(launch(count as u64)) }.map_err(driver)?; // One window at a time: only its table is held, and a failure leaves no more queued. s.synchronize().map_err(driver)?; diff --git a/wai/wai-quantum/src/quantum_landscape/job.rs b/wai/wai-quantum/src/quantum_landscape/job.rs index 0465cd96c..a103008f7 100644 --- a/wai/wai-quantum/src/quantum_landscape/job.rs +++ b/wai/wai-quantum/src/quantum_landscape/job.rs @@ -232,10 +232,24 @@ 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`]), within whose declared @@ -254,6 +268,7 @@ pub fn call_placed(members: &[Certified<'_>], op: &str, request: &Value, max_byt } 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() { @@ -261,6 +276,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(), } }; @@ -651,7 +670,7 @@ mod tests { 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 jobs past it go elsewhere: at temperature 0, a field bound to 41; above temperature 0, a field bound to 41."), "{}", a["label"]); + 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(); diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index 7f9b4962c..8c825661e 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -40,6 +40,16 @@ use super::capacity::{Capacity, floor}; use super::conformance; use super::settle::{Outcome, Term, Workload}; use serde_json::{Value, json}; +use std::cell::RefCell; + +/// The host's audit of a kernel graded `bit-exact` (§7.3): a settle whose work, +/// its sweeps times its couplings and cells, is at most this is run again on +/// the reference whole, every time. +pub const AUDIT_SETTLE_WORK: u64 = 1 << 24; + +/// Past [`AUDIT_SETTLE_WORK`], one settle in this many is run again, chosen by +/// the job's digest so that the choice can be repeated. +pub const AUDIT_SETTLE_ONE_IN: u64 = 8; /// The deterministic kernel's name. pub const KERNEL_SETTLE: &str = "wai.quantum.landscape/settle"; @@ -176,13 +186,19 @@ pub struct Certified<'a> { settle: Parity, sample: Parity, capacity: Capacity, + /// Whether the host certified it, or runs it by name ([`named`]). + certified: bool, + /// 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). From + /// then on both its kernels are `uncertified`, and its label says why. + demoted: 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, capacity: Capacity::NONE } + Certified { member: &REFERENCE, settle: Parity::Reference, sample: Parity::Reference, capacity: Capacity::NONE, certified: true, demoted: RefCell::new(None) } } } @@ -191,9 +207,26 @@ impl<'a> Certified<'a> { self.member } - /// The parity the host found for a kernel, which holds within [`Certified::capacity`]. + /// 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 } + } + + /// 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() { + *d = Some(why); + } } /// The capacity the member declared when the host certified it. @@ -204,10 +237,14 @@ impl<'a> Certified<'a> { /// What every answer from it says of itself: its own label, and the /// capacity its parity holds within where it declares one. pub fn label(&self) -> String { - match self.capacity.words() { - Some(w) => format!("{} {w}", self.member.label()), - None => self.member.label(), + let mut label = self.member.label(); + if let Some(w) = self.capacity.words(self.certified) { + label = format!("{label} {w}"); } + if let Some(d) = self.demoted() { + label = format!("{label} The host demoted it to uncertified: {d}."); + } + label } } @@ -236,7 +273,7 @@ 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, capacity: m.capacity() }) + Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified, capacity: m.capacity(), certified: false, demoted: RefCell::new(None) }) } /// Certify a member within its declared capacity: its settle kernel on every @@ -261,7 +298,7 @@ pub fn certify(m: &dyn Member) -> Result, String> { } else { Parity::Uncertified }; - Ok(Certified { member: m, settle, sample, capacity }) + Ok(Certified { member: m, settle, sample, capacity, certified: true, demoted: RefCell::new(None) }) } fn check_cells(o: &Outcome, n: usize) -> Result<(), String> { @@ -334,6 +371,9 @@ 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 } diff --git a/wai/wai-quantum/src/quantum_landscape/run.rs b/wai/wai-quantum/src/quantum_landscape/run.rs index a5c3f7488..4175dcd74 100644 --- a/wai/wai-quantum/src/quantum_landscape/run.rs +++ b/wai/wai-quantum/src/quantum_landscape/run.rs @@ -317,17 +317,49 @@ fn add_reading(total: &mut Option>, reading: Option u64 { + let mut h = blake3::Hasher::new(); + h.update(job.program.source.as_bytes()); + h.update(&seed.to_le_bytes()); + h.update(&purpose.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. - c.capacity().admits_settle(w, sweeps).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: the whole settle again on the reference, within a budget of + // work, and past it one job in so many, chosen by the job's digest. + if audited { + let work = u64::from(o.sweeps).saturating_mul(w.nonzeros() + n as u64); + if (work <= member::AUDIT_SETTLE_WORK || audit_draw(job, seed, 0).is_multiple_of(member::AUDIT_SETTLE_ONE_IN)) && o != w.settle(&start, sweeps) { + 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,13 +385,18 @@ 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())); + 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(refused)?; + cap.admits(w, temperatures).map_err(declined)?; + // The audit of a sampling kernel graded `bit-exact`: the last chain of every + // call, and one chain of the run chosen by the job's digest, again on the reference. + 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 chosen = audit_draw(job, seeds.first().copied().unwrap_or(0), 1) % total.max(1); let mut found: Vec = vec![(None, 0, 0, None); seeds.len()]; let mut at = 0u64; while at < total { @@ -375,9 +412,24 @@ 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 { + for k in [end - 1, chosen] { + if (at..end).contains(&k) { + let (i, ch) = ((k - at) as usize, &chains[(k - at) as usize]); + if let Err(e) = member::check_chain(&outcomes[i], w, temperatures) { + return Err(demote(c, KERNEL_SAMPLE, format!("gave a chain the host's checks refuse ({e})"))); + } + if outcomes[i] != sample::anneal_with(w, &ch.start, temperatures, ch.seed) { + 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; 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/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index a94cb1038..4bc8adcdd 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -302,6 +302,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], @@ -310,9 +311,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) { @@ -399,6 +405,24 @@ enum Fault { OnlyTheFirstCells(usize), /// A settle that stops looking for its end after this many sweeps. SettleEndsLate(u32), + /// 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, + /// 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), /// 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. @@ -429,9 +453,18 @@ 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 +} + 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::(), + _ => w.field(s, i), + }; match self { Fault::Fields16 => i64::from(h as i16), Fault::Fields32 | Fault::Fields32CheckedWithoutCouplings => i64::from(h as i32), @@ -440,32 +473,65 @@ impl Fault { } fn sweep(self, w: &Workload, s: &[i8]) -> Vec { + self.sweep_in(w, s, false) + } + + /// A sweep at 0; `mixed` where the schedule has a sweep above 0 as well. + fn sweep_in(self, w: &Workload, s: &[i8], mixed: bool) -> Vec { let c = w.cost(); - (0..w.n()).map(|i| { + let n = w.n(); + (0..n).map(|i| { + if self == Fault::TileRemainder && 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::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 next = self.sweep_in(w, &s, mixed); + 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 { + 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 = (counter * n + i + 1) as u64; + 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 { @@ -475,6 +541,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 } @@ -522,7 +597,13 @@ impl Member for Fault { { 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::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 } @@ -587,6 +668,46 @@ impl Witness { 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 { @@ -649,6 +770,66 @@ 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. @@ -671,6 +852,21 @@ fn faults() -> Vec<(Fault, &'static str, Witness)> { (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() { @@ -694,9 +890,10 @@ fn holds(m: &dyn Member, kernel: &str) -> bool { } /// With no fault the stand-in is the reference, so each fault below is the only -/// difference. Each loses its kernel's grade twice: declaring no capacity, and -/// declaring the most hostile capacity it may, the least at or above the floor -/// that still takes a job it gets wrong. +/// 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); @@ -704,11 +901,13 @@ fn every_fault_loses_the_grade_of_the_kernel_it_is_in() { 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 cap = witness.hostile(); - assert!(cap.under(&floor()).is_empty()); - 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()); + 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()); + } } } @@ -977,3 +1176,286 @@ fn review_skeptic_a_device_that_restarts_its_draws_each_window() { 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); + +impl Member for Sleeper { + fn id(&self) -> String { + "gpu:sleeper".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 { + 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 (f, program) in jobs { + let sleeper = Sleeper(f, Cell::new(false)); + 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(Fault::DeclaresFieldBound(i64::MAX), Cell::new(false)); + 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()); +} -- 2.54.0 (Apple Git-157) From 21e2df5964308b7724a25f6f7b07018f17bada20 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 09:42:19 -0500 Subject: [PATCH 05/15] wai-quantum: the audit rests on what the host computes and remembers, and generation reaches within every capacity The third adversarial check of #678 found the audit trusting what a member reports or can predict, and generation falling short at small and declared capacities. - The audit checks one chain of every seed's run, and the last of every call, in run, 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 a 2^24 budget goes. It draws with a host key made at start and a count every draw moves on; each audited answer records what it checked and a commitment to the key (`audit`). - A demotion is kept by identity (Member::fingerprint and the id) in member::Demotions, in a file where the host names one: the FPGA host does. A second certification or another declared capacity does not clear it. - A kernel graded statistical is placed only where a job asks for it ("parity": "statistical"). - Generation: lane vectors packed to what the capacity takes; remainder-band ladder sizes quenched; a wave as long as a settle can be for every capacity; a corner that moves at 0 and reaches the field bound through a cell's every coupling; a coupling sum past a narrow lane; annealing-shaped vectors cold enough for a declared span; a battery of exactly the declared cells, and of 1,023 with none. The cost bounds are constants a test holds over random capacities. - The CUDA out-of-table mark is no longer claimed to mark the answer. - The round-3 probes are kept as tests; spec 7.1 and 7.3 follow. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-landscape.yml | 10 +- .../2026-11-07-landscape-declared-capacity.md | 3 + wai/extensions/quantum-landscape.md | 108 ++++--- wai/wai-quantum-landscape-fpga/README.md | 5 +- wai/wai-quantum-landscape-fpga/src/main.rs | 7 + wai/wai-quantum-landscape-fpga/src/member.rs | 6 + wai/wai-quantum/docs/landscape.md | 14 +- wai/wai-quantum/schemas/landscape.v1.json | 76 +++++ .../src/quantum_landscape/capacity.rs | 277 +++++++++++++----- .../src/quantum_landscape/conformance.rs | 11 +- wai/wai-quantum/src/quantum_landscape/cuda.rs | 6 +- wai/wai-quantum/src/quantum_landscape/job.rs | 26 +- .../src/quantum_landscape/member.rs | 102 ++++++- wai/wai-quantum/src/quantum_landscape/run.rs | 81 ++++- .../tests/landscape_conformance.rs | 201 ++++++++++++- 15 files changed, 782 insertions(+), 151 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape.yml b/.github/workflows/wai-quantum-landscape.yml index 6919a3e7e..76a496284 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -237,7 +237,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\. 25 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (25 expected)"; exit 1; } + grep -qE '^test result: ok\. 32 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (32 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 @@ -312,10 +312,10 @@ 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; } - # The reviews' probes and the audit's tests test certification and the audit, 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_ 2>&1 | tee "$RUNNER_TEMP/vectors.log" - grep -qE '^test result: ok\. 12 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out' "$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; 22 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } done wasip2: diff --git a/wai/changes/2026-11-07-landscape-declared-capacity.md b/wai/changes/2026-11-07-landscape-declared-capacity.md index 86195ff31..8ac16961a 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -14,3 +14,6 @@ - **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 and a commitment to the key (`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 reaches every cell a workload can hold (4,096 with none). 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. +- **Breaking:** `api:rust:wai_quantum::quantum_landscape::job::Prepared::statistical`: a new field, whether a job takes a kernel graded statistical. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 13bbcb0de..079a28d1e 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -636,21 +636,31 @@ below. - 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; + 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, as long as a settle can be; - 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, one workload with 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. 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 - where both sides take it, the shape an annealing job ends in; - - a corner on each side of 0: `C` cells with every coupling `P` allows, fields at the - largest field of both signs, a field bound reached exactly, and the largest cost; + - 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; - 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 @@ -662,17 +672,19 @@ below. 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; and a schedule of exactly that many sweeps; - - for the battery (§7.2), at a declared cell count, `C` alike cells in blocks of three, - read at the cell counts above; at declared couplings, alike triangles; at a declared + - for the battery (§7.2), where `C` is past 24: `C` alike cells where a cell count is + declared, and 1,023 where none is, read in blocks of three at the cell counts above + and at the last cell; at declared couplings, alike + triangles; at a declared schedule length, one alike triangle warm and then cold, to 1,024 sweeps; at a declared field bound, field, cost and span, the workloads above. Identical blocks cannot reach a table, so a table has none. - The rules bound what certification costs, whatever the capacity: at most 200 generated - vectors, at most 300 million cell sweeps hot or settled (nearly all of them battery + The rules bound what certification costs, whatever the capacity: at most 300 generated + vectors, at most 400 million cell sweeps hot or settled (nearly all of them battery workloads, run only for a kernel that is not `bit-exact`), and at most 20 million at - temperature 0 (the run past `2²⁴`, at a fixed point, which the reference skips). The - exact counts are pinned by a test. + temperature 0 (the run past `2²⁴`, at a fixed point, which the reference skips). A test + holds the bounds over the named capacities and over random ones. - `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 @@ -718,34 +730,56 @@ 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. 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: +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: the last chain of every call to the member, and one chain of each run chosen - by the job's digest (the low 64 bits of BLAKE3 over the program, the seed and a purpose - number), so that the choice can be repeated; -- settling: the whole settle, when its work, sweeps times couplings and cells, is at most - 2²⁴; past that, one job in 8, chosen by the job's digest. The checks of §5 hold as before. +- sampling: in every job (a run, a replay, a sweep or a comparison), one chain of each + seed's run, and the last chain of every call to the member; +- 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`: each +run's seed and its chains checked, or how much of a settle) and a commitment to the key, +its BLAKE3 digest, which stays the same for as long as the host runs. 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. +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. -Its cost: at most one chain for each call and one for each run, so for a run of `R` -chains in calls of `B` at most `(⌈R/B⌉ + 1)/R` of its sampling work again; for a settle -within the budget, that settle's work again, and past it one in 8. What it catches: a -fault that makes a fraction `q` of a run's chains wrong at places the digest does not -favour, on each such job with a chance of at least `q`; 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, on the first job that shows it within the -budget, and past the budget within `N` such jobs with a chance of `1 − (7/8)^N` (0.74 at -10 jobs, 0.99 at 35). 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. A fault that only ever touches chains the audit does -not pick, on settles past the budget, can go unseen. The audit is a check that runs as long as the -member does. It is not a proof. +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: one chain for each run and one for each call, so for a run of `R` chains in +calls of `B` at most `(⌈R/B⌉ + 1)/R` of its sampling work again; for a settle, at most +2²⁴ of work, and past it the whole settle again one job in 8. 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 sooner +than the reference does, on the first such job; and one in a settle past the budget, +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 diff --git a/wai/wai-quantum-landscape-fpga/README.md b/wai/wai-quantum-landscape-fpga/README.md index 5c0764617..1dd56552b 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -65,7 +65,10 @@ at the wrap points of 16-bit lanes, a run past draw index 2²⁴, and a corner w 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. +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. **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 diff --git a/wai/wai-quantum-landscape-fpga/src/main.rs b/wai/wai-quantum-landscape-fpga/src/main.rs index b0c30a5ba..b7b7faf50 100644 --- a/wai/wai-quantum-landscape-fpga/src/main.rs +++ b/wai/wai-quantum-landscape-fpga/src/main.rs @@ -297,6 +297,13 @@ 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. + if std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").is_none() + && let Some(home) = std::env::var_os("HOME") + { + wai_quantum::quantum_landscape::member::keep_demotions_in(std::path::PathBuf::from(home).join(".local/state/wai-quantum-landscape-fpga/demotions.json")); + } 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 c204b75e1..5e8405047 100644 --- a/wai/wai-quantum-landscape-fpga/src/member.rs +++ b/wai/wai-quantum-landscape-fpga/src/member.rs @@ -334,6 +334,12 @@ impl Member for BoardMember

{ 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 { let l = lowered_settle(w); self.engine.borrow_mut().load(&l).map_err(|e| e.0)?; diff --git a/wai/wai-quantum/docs/landscape.md b/wai/wai-quantum/docs/landscape.md index c02ed0662..781bc7362 100644 --- a/wai/wai-quantum/docs/landscape.md +++ b/wai/wai-quantum/docs/landscape.md @@ -246,11 +246,15 @@ grade, and one that declares less than the floor (`capacity::floor`, what the co 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 the last chain -of every call and one chain of each run chosen by the job's digest again on the reference, and the -whole settle where its work is at most 2²⁴ (past that, one job in 8). 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`). +Then the host audits it. For every answer from a kernel graded `bit-exact`, it runs one chain of +every seed's run and the last chain of every call again on the reference, drawn with a key the host +makes at start, 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), so certifying again does not clear it. A kernel graded +`statistical` is placed only where a job asks for it (`"parity": "statistical"`). ```rust norun=account use wai_quantum::quantum_landscape::capacity::{Capacity, Limits}; diff --git a/wai/wai-quantum/schemas/landscape.v1.json b/wai/wai-quantum/schemas/landscape.v1.json index 32865a288..ca9204022 100644 --- a/wai/wai-quantum/schemas/landscape.v1.json +++ b/wai/wai-quantum/schemas/landscape.v1.json @@ -707,6 +707,25 @@ "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "key", + "checked" + ], + "properties": { + "key": { + "type": "string" + }, + "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).", @@ -969,6 +988,25 @@ } } }, + "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "key", + "checked" + ], + "properties": { + "key": { + "type": "string" + }, + "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).", @@ -1215,6 +1253,25 @@ } } }, + "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "key", + "checked" + ], + "properties": { + "key": { + "type": "string" + }, + "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).", @@ -1349,6 +1406,25 @@ "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "required": [ + "key", + "checked" + ], + "properties": { + "key": { + "type": "string" + }, + "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/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs index 9d82038da..4cb6734ad 100644 --- a/wai/wai-quantum/src/quantum_landscape/capacity.rs +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -407,6 +407,13 @@ pub mod reachable { /// 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 = 400_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 @@ -770,7 +777,11 @@ impl Out<'_, '_> { 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), + _ => { + 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); + } } } } @@ -837,6 +848,18 @@ impl Capacity { 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); + } + } + // A wave settling down as many cells as a workload can hold, a cell a sweep: + // a settle as long as a settle within the capacity can be. + if o.wants(true, false) { + let n = r.cells.min(r.pairs + 1).max(2); + let l = banded(n, n - 1, 2); + o.settle(format!("wave/cells-{n}"), &l, &l.lower(), 2 * n as u32 + 2); } // 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 @@ -876,6 +899,25 @@ impl Capacity { 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. @@ -883,63 +925,130 @@ impl Capacity { wraps.sort_unstable(); wraps.dedup(); for (side, s) in [("at-0", &r.zero), ("above-0", &r.hot), ("both", &r.both)] { - let alone: Vec = wraps.iter().copied().filter(|&v| v <= s.field).collect(); - let coupled: Vec = wraps.iter().copied().filter(|&v| v <= s.bound && v - 2 <= s.field).collect(); - if alone.is_empty() && coupled.is_empty() { + if !o.wants(side == "at-0", true) { continue; } let c = s.cost.min(wraps[0] - 1); - for cost in [0, c] { - let edge = cost + 1 <= s.field; - let mut l = Landscape::new(2 * alone.len() + 6 * coupled.len() + if edge { 2 } else { 0 }); - let mut at = 0; - for &v in &alone { - l.set_field(at, v).set_field(at + 1, -v); - at += 2; + 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; } - for &v in &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; + // 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); } - } - // 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); - o.on_side(side, format!("lane/{side}/cost-{cost}"), &l, (1u64 << 15) as f64, 3); - if c == 0 { - break; } } } - // A corner: every number at its most at once, on each side of 0. + // 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 < 5 || !o.wants(side == "at-0", true) { + 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 fill = banded(n - 5, r.pairs.saturating_sub(2), (s.bound - 1).clamp(1, 1 << 20) as usize); - let mut l = Landscape::new(n); - for i in 0..n - 5 { - for j in (i + 1)..n - 5 { - let c = fill.coupling(i, j); - if c != 0 { - l.couple(5 + i, 5 + j, c); + 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(5 + 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); } - l.set_field(0, f0).set_field(1, -f0); - if let Some(p) = pinned(s.bound, 1, s.field, 0) { - l.couple(2, 3, 1).couple(2, 4, 1); - l.set_field(2, p.field()[0]).set_field(3, p.field()[1]).set_field(4, p.field()[2]); + 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); } - l.set_cost(s.cost.min(f0.max(1))); - o.on_side(side, format!("corner/{side}"), &l, f0.max(1) as f64, 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. @@ -966,13 +1075,6 @@ impl Capacity { 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(c) = below_u(self.cells, reachable::CELLS) { - let c = c as usize; - if o.wants(true, false) { - let l = banded(c, (c - 1).min(r.pairs), 2); - o.settle(format!("edge/cells-{c}"), &l, &l.lower(), 2 * c as u32 + 2); - } - } if let Some(p) = below_u(self.couplings, reachable::COUPLINGS) { let pairs = (p / 2) as usize; if pairs > 0 && o.wants(true, true) { @@ -1078,20 +1180,25 @@ impl Capacity { 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 at the declared cells, couplings and sweeps: blocks of - // three, each held to its law by itself, the blocks read at the ladder's sizes. + // The battery across the cells: as many alike cells as a declared count + // (with none, 1,023, so that a battery run stays a few seconds), blocks of + // three read at the ladder's sizes and at the last cell; at declared + // couplings and sweeps, alike triangles. let read = |blocks: usize, cells_each: usize| -> Vec { - let mut out: Vec = ladder_sizes(blocks * cells_each).into_iter().map(|c| (c - 1) / cells_each).collect(); - out.push(blocks / 2); - out.retain(|&b| b < blocks); + let mut out: Vec = ladder_sizes(blocks * cells_each).into_iter().map(|c| (c - 1) / cells_each * cells_each).collect(); + out.push(blocks / 2 * cells_each); + out.retain(|&b| b + cells_each <= blocks * cells_each); out.sort_unstable(); out.dedup(); out }; - if let Some(c) = below_u(self.cells, reachable::CELLS) { - let blocks = (c as usize / 3).max(1); - let l = singles(&vec![1; 3 * blocks], 0); - o.law(BatteryCase { name: "edge/cells", landscape: l, temperatures: vec![1.0], law: Law::Settled, block: 3, read: read(blocks, 3) }); + if r.cells > 24 { + let c = if self.cells.is_some_and(|c| c < reachable::CELLS) { r.cells } else { 1023 }; + let mut starts: Vec = ladder_sizes(c).into_iter().map(|k| (k - 1).saturating_sub(2).min(c - 3)).collect(); + starts.extend([c / 2, c - 3]); + starts.sort_unstable(); + starts.dedup(); + o.law(BatteryCase { name: "cells", landscape: singles(&vec![1; c], 0), temperatures: vec![1.0], law: Law::Settled, block: 3, read: starts }); } if let Some(p) = below_u(self.couplings, reachable::COUPLINGS) { let blocks = ((p / 6) as usize).min(r.cells / 3).min(341); @@ -1224,7 +1331,7 @@ mod tests { 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 == "edge/cells-299").expect("a settle at 299 cells"); + 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(); @@ -1244,7 +1351,7 @@ mod tests { 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 ["edge/cells-64", "edge/field-524287-above-0/524287", "edge/cost-1048575-above-0/hot", "edge/uncertain-span-8192"] { + 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 { @@ -1296,15 +1403,16 @@ mod tests { } 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/cold", "both"] { + for side in ["at-0", "above-0", "both"] { for cost in [0, 32_766] { - let name = format!("lane/{}/cost-{cost}", side.split('/').next().unwrap()); - let c = e.chains.iter().find(|c| c.name.starts_with(&name) && c.name.contains(side.split('/').next_back().unwrap())).unwrap_or_else(|| panic!("{name} {side}")); - let w = c.landscape.lower(); + 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 zero = vec![0i8; w.n()]; - assert!((0..w.n()).any(|i| w.field(&zero, i) == v) && (0..w.n()).any(|i| w.field(&zero, i) == -v), "{name}: a cell at ±{v}"); - assert!((0..w.n()).any(|i| w.field(&zero, i) == v - 2 && w.degree(i) == 2), "{name}: ±{v} through couplings"); + 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"); } } } @@ -1321,12 +1429,37 @@ mod tests { } /// What certification generates costs a bounded amount, whatever the - /// capacity: the numbers §7.1 states. + /// 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 }; - for (name, cap) in [("none", Capacity::NONE), ("floor", floor()), ("fpga", fpga), ("biggest", biggest)] { + 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 }), + ("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) = (0u64, 0u64, 0u64); cap.generate(Want::ALL, &mut |g| { vectors += 1; @@ -1343,9 +1476,11 @@ mod tests { true }); eprintln!("generated for {name}: {vectors} vectors, {work} cell-sweeps hot or settled, {cold} at temperature 0"); - assert!(vectors <= 200, "{name}: {vectors}"); - assert!(work <= 300_000_000, "{name}: {work}"); - assert!(cold <= 20_000_000, "{name}: {cold}"); + (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 eaa65a084..5ae1106ec 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -540,9 +540,10 @@ pub struct BatteryCase { pub temperatures: Vec, pub law: Law, pub block: usize, - /// The blocks whose final states are counted, in order; empty, every block. - /// A workload of many blocks reads a few, so that so many tests at once do - /// not fail a correct sampler by chance. + /// 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, } @@ -785,7 +786,7 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec return BatteryResult { name: case.name, chi_square: 0.0, degrees: 0, critical: 0.0, passed: true, declined: true }; } let exact = case.exact(); - let blocks: Vec = if case.read.is_empty() { (0..case.landscape.len() / case.block).collect() } else { case.read.clone() }; + 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; let mut at = 0u64; @@ -796,7 +797,7 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec Ok(got) if got.len() == chunk.len() && got.iter().all(|o| check_chain(o, &w, &case.temperatures).is_ok()) => { for o in got { for (c, &b) in counts.iter_mut().zip(&blocks) { - c[index_of(&o.state[b * case.block..(b + 1) * case.block])] += 1; + c[index_of(&o.state[b..b + case.block])] += 1; } } } diff --git a/wai/wai-quantum/src/quantum_landscape/cuda.rs b/wai/wai-quantum/src/quantum_landscape/cuda.rs index a66ab137d..cf5e149d3 100644 --- a/wai/wai-quantum/src/quantum_landscape/cuda.rs +++ b/wai/wai-quantum/src/quantum_landscape/cuda.rs @@ -83,8 +83,10 @@ extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* i64 h = field_of(rows, cols, vals, fields, s, i); u64 u = splitmix(seed, (u64)k * n + i + 1) >> 11; u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i]; - // A field past the table can only come of a fault: the cell is left at a - // state no cell has, which the host refuses, and nothing past it is read. + // A field past the table can only come of a fault. Nothing past the table is + // read, so no illegal address poisons the context; the cell is left at a state + // no cell has, which a later sweep may overwrite, so the wrong answer is caught + // by the vectors and the audit, not by this mark. if (e >= entries) { s[i] = 2; continue; } s[i] = u < table[2 * e] ? -1 : (u < table[2 * e + 1] ? 0 : 1); } diff --git a/wai/wai-quantum/src/quantum_landscape/job.rs b/wai/wai-quantum/src/quantum_landscape/job.rs index a103008f7..ef8ab41ed 100644 --- a/wai/wai-quantum/src/quantum_landscape/job.rs +++ b/wai/wai-quantum/src/quantum_landscape/job.rs @@ -108,6 +108,10 @@ pub struct Prepared { 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: @@ -125,6 +129,11 @@ 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)?; @@ -188,11 +197,22 @@ pub fn prepare(op: &str, request: &Value, max_bytes: u64) -> Result = 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, schedules, settles, 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!({ "key": run::audit_commitment(), "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() { @@ -265,6 +285,8 @@ 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 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 { diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index 8c825661e..55cf9fe21 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -41,6 +41,9 @@ 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, /// its sweeps times its couplings and cells, is at most this is run again on @@ -152,6 +155,72 @@ pub trait Member { 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() + } +} + +/// 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. +pub struct Demotions { + path: Option, + records: Mutex>, +} + +impl Demotions { + /// A store kept in `path` (read now, written on every demotion), or in + /// memory alone with none. + pub fn open(path: Option) -> Demotions { + let records = path.as_ref().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|t| serde_json::from_str::>(&t).ok()).unwrap_or_default(); + Demotions { path, records: Mutex::new(records) } + } + + /// Why an identity was demoted, if it was. + pub fn get(&self, identity: &str) -> Option { + self.records.lock().ok()?.get(identity).and_then(|r| r["why"].as_str().map(String::from)) + } + + /// Remember a demotion, and write the store where it has a file. The first reason stands. + pub fn record(&self, identity: &str, why: &str, capacity: &Capacity) { + let Ok(mut r) = self.records.lock() else { return }; + r.entry(identity.to_string()).or_insert_with(|| json!({ "why": why, "capacity": capacity.json() })); + if let Some(p) = &self.path { + if let Some(dir) = p.parent() { + let _ = std::fs::create_dir_all(dir); + } + let tmp = p.with_extension("tmp"); + if std::fs::write(&tmp, serde_json::to_string_pretty(&*r).unwrap_or_default()).is_ok() { + let _ = std::fs::rename(&tmp, p); + } + } + } +} + +static DEMOTIONS: OnceLock = OnceLock::new(); + +/// The host's store of demotions: the file `WAI_LANDSCAPE_DEMOTIONS` names, or +/// one a host chose with [`keep_demotions_in`] 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").map(PathBuf::from))) +} + +/// Keep the host's demotions in `path` (a host that runs members does so at +/// start). False where the store was already in use. +pub fn keep_demotions_in(path: PathBuf) -> bool { + DEMOTIONS.set(Demotions::open(Some(path))).is_ok() +} + +/// 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. @@ -188,17 +257,22 @@ pub struct Certified<'a> { 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). 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, capacity: Capacity::NONE, certified: true, demoted: RefCell::new(None) } + 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()) } } } @@ -214,6 +288,17 @@ impl<'a> Certified<'a> { 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() @@ -225,6 +310,7 @@ impl<'a> Certified<'a> { pub fn demote(&self, why: String) { let mut d = self.demoted.borrow_mut(); if d.is_none() { + self.store.record(&identity_of(self.member), &why, &self.capacity); *d = Some(why); } } @@ -273,7 +359,7 @@ 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, capacity: m.capacity(), certified: false, demoted: RefCell::new(None) }) + Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified, capacity: m.capacity(), certified: false, store: demotions(), demoted: RefCell::new(demotions().get(&identity_of(m))), audit_log: RefCell::new(Vec::new()) }) } /// Certify a member within its declared capacity: its settle kernel on every @@ -282,12 +368,22 @@ pub fn named(m: &dyn Member) -> Result, String> { /// 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 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("; "))); } + if let Some(why) = store.get(&identity_of(m)) { + 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 }; @@ -298,7 +394,7 @@ pub fn certify(m: &dyn Member) -> Result, String> { } else { Parity::Uncertified }; - Ok(Certified { member: m, settle, sample, capacity, certified: true, demoted: RefCell::new(None) }) + 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> { diff --git a/wai/wai-quantum/src/quantum_landscape/run.rs b/wai/wai-quantum/src/quantum_landscape/run.rs index 4175dcd74..ccf52e269 100644 --- a/wai/wai-quantum/src/quantum_landscape/run.rs +++ b/wai/wai-quantum/src/quantum_landscape/run.rs @@ -317,13 +317,41 @@ 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 commitment to the host's audit key, which every audited answer carries: +/// the key's BLAKE3 digest. +pub fn audit_commitment() -> String { + blake3::hash(audit_key()).to_hex().to_string() +} + +/// 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 { - let mut h = blake3::Hasher::new(); + 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")) } @@ -352,11 +380,32 @@ fn execute(job: &Job, plan: Plan, seed: u64, c: &Certified<'_>) -> Result 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"))); } } @@ -396,7 +445,9 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 let r = u64::from(restarts); // Every (seed, start) in order: seed after seed, start after start. let total = seeds.len() as u64 * r; - let chosen = audit_draw(job, seeds.first().copied().unwrap_or(0), 1) % total.max(1); + // One chain of each seed's run, drawn with the host's key, and the last chain of every call. + let chosen: Vec = if audited { seeds.iter().enumerate().map(|(s, &seed)| s as u64 * r + audit_draw(job, seed, 1) % r.max(1)).collect() } else { Vec::new() }; + 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 { @@ -413,8 +464,13 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 } let reading = m.meter(); if audited { - for k in [end - 1, chosen] { - if (at..end).contains(&k) { + let mut these: Vec = chosen.iter().copied().filter(|k| (at..end).contains(k)).collect(); + these.push(end - 1); + these.sort_unstable(); + these.dedup(); + for k in these { + checked[(k / r) as usize].push(k % r); + { let (i, ch) = ((k - at) as usize, &chains[(k - at) as usize]); if let Err(e) = member::check_chain(&outcomes[i], w, temperatures) { return Err(demote(c, KERNEL_SAMPLE, format!("gave a chain the host's checks refuse ({e})"))); @@ -443,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)| { diff --git a/wai/wai-quantum/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index 4bc8adcdd..e03440471 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -417,6 +417,16 @@ enum Fault { 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. @@ -463,6 +473,7 @@ impl Fault { fn field(self, w: &Workload, s: &[i8], i: usize) -> i64 { 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 { @@ -473,19 +484,24 @@ impl Fault { } fn sweep(self, w: &Workload, s: &[i8]) -> Vec { - self.sweep_in(w, s, false) + self.sweep_in(w, s, false, false) } - /// A sweep at 0; `mixed` where the schedule has a sweep above 0 as well. - fn sweep_in(self, w: &Workload, s: &[i8], mixed: bool) -> Vec { + /// 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(); let n = w.n(); (0..n).map(|i| { - if self == Fault::TileRemainder && in_the_tile_gap(n, 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); 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 } @@ -511,7 +527,7 @@ impl Fault { } if t <= 0.0 { if !rests { - let next = self.sweep_in(w, &s, mixed); + let next = self.sweep_in(w, &s, mixed, true); rests = next == s; s = next; } @@ -572,6 +588,10 @@ impl Member for Fault { { 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 }); @@ -600,6 +620,7 @@ impl Member for Fault { 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 } @@ -1340,11 +1361,17 @@ fn review2_the_battery_at_the_declared_cells() { /// 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); +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 { - "gpu:sleeper".into() + self.2.clone() } fn class(&self) -> &'static str { "gpu" @@ -1406,8 +1433,8 @@ fn audit_demotes_a_member_on_the_first_job_it_gets_wrong() { (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 (f, program) in jobs { - let sleeper = Sleeper(f, Cell::new(false)); + 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); @@ -1450,7 +1477,7 @@ fn audit_demotes_a_member_on_the_first_job_it_gets_wrong() { /// 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(Fault::DeclaresFieldBound(i64::MAX), Cell::new(false)); + 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")] { @@ -1459,3 +1486,157 @@ fn audit_leaves_a_correct_member_alone() { } 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 under a commitment to the key. +#[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_eq!(a["audit"]["key"].as_str(), Some(wai_quantum::quantum_landscape::run::audit_commitment().as_str())); + 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), + ]); +} -- 2.54.0 (Apple Git-157) From f290012f4ef0d3ca31c22a21c3a70c2c6894bdc2 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 10:15:08 -0500 Subject: [PATCH 06/15] wai-quantum: the change entry names only the breaks the gate finds Prepared::statistical is a new field of a struct with private fields, which the API gate does not count as a break. Co-Authored-By: Claude Opus 5.5 --- wai/changes/2026-11-07-landscape-declared-capacity.md | 1 - 1 file changed, 1 deletion(-) diff --git a/wai/changes/2026-11-07-landscape-declared-capacity.md b/wai/changes/2026-11-07-landscape-declared-capacity.md index 8ac16961a..3c7010a35 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -16,4 +16,3 @@ - **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 and a commitment to the key (`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 reaches every cell a workload can hold (4,096 with none). 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. -- **Breaking:** `api:rust:wai_quantum::quantum_landscape::job::Prepared::statistical`: a new field, whether a job takes a kernel graded statistical. -- 2.54.0 (Apple Git-157) From 5aa35b07e1ab4bbbadc1fff97fa6ca429b2efc11 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 11:39:44 -0500 Subject: [PATCH 07/15] wai-quantum: the landscape's WebGPU members come from one instance, so a card is found every time With the CUDA fault fixed, jetson-hub's leg still lost a member: the RTX 4050 through Vulkan, from the fourth fault on. The disk was not the cause. The leg compiled and wrote its files, and the same tests failed twice more with 248 GB free. Nor was the card's memory: at most 630 of 6,141 MiB was in use. Each discovery made its own wgpu instance. That host's NVIDIA Vulkan driver (595.99) lists its card to a process's first ten instances and not after, even with every one dropped. At that moment it logs NV_ERR_NO_MEMORY for a host-memory allocation. A process that looks again, as the device tests do for every test and every faulted kernel, found the card gone; the order of the tests only decided where. On jetson-hub, one process discovering 60 times: - an instance a discovery: the card is found 10 times, then not again; - one instance for the process: the card is found 60 times. Discovery now takes the process's one instance. a_member_is_found_however_often_the_host_looks discovers twelve times in one process and wants the same members each time. On the Framework (AMD, through Vulkan) and Thor (NVIDIA), the device tests, it among them, pass twice each. The workflow expects 9 device tests where it expected 8. Co-Authored-By: Claude Opus 5.5 --- .../wai-quantum-landscape-devices.yml | 2 +- .../src/quantum_landscape/webgpu.rs | 22 ++++++-- wai/wai-quantum/tests/landscape_devices.rs | 50 +++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape-devices.yml b/.github/workflows/wai-quantum-landscape-devices.yml index ab8544573..9eb378574 100644 --- a/.github/workflows/wai-quantum-landscape-devices.yml +++ b/.github/workflows/wai-quantum-landscape-devices.yml @@ -140,7 +140,7 @@ jobs: rm -f "$report" WAI_LANDSCAPE_REPORT="$report" nice -n 10 \ cargo test --locked --release --features "${{ matrix.features }}" --test landscape_devices -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/devices.log" - grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (7 expected)"; exit 1; } + grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (9 expected)"; exit 1; } cat "$report" IFS=',' read -ra want <<< "$WAI_LANDSCAPE_EXPECT" for p in "${want[@]}"; do diff --git a/wai/wai-quantum/src/quantum_landscape/webgpu.rs b/wai/wai-quantum/src/quantum_landscape/webgpu.rs index 3c9cec53e..c60c54dd8 100644 --- a/wai/wai-quantum/src/quantum_landscape/webgpu.rs +++ b/wai/wai-quantum/src/quantum_landscape/webgpu.rs @@ -134,14 +134,26 @@ pub fn discover_with(source: &str) -> Vec> { if wgpu::Instance::enabled_backend_features().is_empty() { return Vec::new(); } - let mut d = wgpu::InstanceDescriptor::new_without_display_handle(); - d.backends = wgpu::Backends::PRIMARY; - d.flags = wgpu::InstanceFlags::empty(); - let instance = wgpu::Instance::new(d); - let Ok(adapters) = resolve(instance.enumerate_adapters(wgpu::Backends::PRIMARY)) else { return Vec::new() }; + let Ok(adapters) = resolve(instance().enumerate_adapters(wgpu::Backends::PRIMARY)) else { return Vec::new() }; adapters.into_iter().map(|a| open(a, source)).collect() } +/// The process's one instance of the platform's GPU interfaces, which every +/// discovery enumerates. A discovery that made its own lost a GPU after ten: +/// an NVIDIA Vulkan driver (595.99, an RTX 4050) lists its card to a process's +/// first ten instances and not after, even with every one dropped, so a host +/// that discovers its members again (each test, each faulted kernel) found +/// that card gone. +fn instance() -> &'static wgpu::Instance { + static INSTANCE: std::sync::OnceLock = std::sync::OnceLock::new(); + INSTANCE.get_or_init(|| { + let mut d = wgpu::InstanceDescriptor::new_without_display_handle(); + d.backends = wgpu::Backends::PRIMARY; + d.flags = wgpu::InstanceFlags::empty(); + wgpu::Instance::new(d) + }) +} + fn open(adapter: wgpu::Adapter, source: &str) -> Result { let info = adapter.get_info(); let backend = backend_label(info.backend).ok_or_else(|| format!("{}: backend {:?} is not one this member runs on", info.name, info.backend))?; diff --git a/wai/wai-quantum/tests/landscape_devices.rs b/wai/wai-quantum/tests/landscape_devices.rs index 67d617821..7c5b1de2c 100644 --- a/wai/wai-quantum/tests/landscape_devices.rs +++ b/wai/wai-quantum/tests/landscape_devices.rs @@ -330,6 +330,56 @@ fn every_fault_in_a_device_s_kernels_loses_its_grade() { } } +/// A host finds the same members however often it looks. Each discovery once +/// made its own instance of the GPU interfaces, and an NVIDIA Vulkan driver +/// lists its card to a process's first ten instances and not after: by a test +/// late in this file, that card was gone. Twelve discoveries, one process. +#[test] +fn a_member_is_found_however_often_the_host_looks() { + let ids = |found: Found| -> Vec { found.into_iter().filter_map(Result::ok).map(|m| m.id()).collect() }; + let first: Vec> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect(); + for look in 2..=12 { + let again: Vec> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect(); + assert_eq!(again, first, "discovery {look} found other members than the first"); + } +} + +/// A port whose sampling kernel looks a field up past its table (by 2⁴⁰ +/// entries on CUDA, so the address is never mapped) loses its grade and not +/// the device: the clean kernels then open on as many devices as before and +/// are certified bit-exact on each. A CUDA kernel that read there would lose +/// its context to every job after it in the process; WebGPU's kernels are +/// bounds-checked. +#[test] +fn a_kernel_that_looks_past_its_table_loses_its_grade_and_not_the_device() { + let present: Vec = members().iter().map(|m| m.id()).collect(); + let have = built(); + let past: Vec<(&str, &str, &str)> = [ + ("cuda", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i];", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i] + (1ull << 40);"), + ("webgpu", "return kind - 1u + bitcast(h) + graph[shape.offsets + i];", "return kind - 1u + bitcast(h) + graph[shape.offsets + i] + 0x1000000u;"), + ] + .into_iter() + .filter(|(b, _, _)| have.contains(b)) + .collect(); + for (backend, found) in backends(&past) { + if !past.iter().any(|(b, _, _)| *b == backend) { + continue; + } + for m in found.into_iter().filter_map(Result::ok) { + let c = member::certify(m.as_ref()).unwrap(); + report(&format!("fault a lookup past the table in {}: settle {}, sample {}", m.id(), c.parity(KERNEL_SETTLE).label(), c.parity(KERNEL_SAMPLE).label())); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact, "{}: a lookup past the table kept the sampler bit-exact", m.id()); + } + let again: Vec> = backends(&[]).into_iter().filter(|(b, _)| *b == backend).flat_map(|(_, f)| f).filter_map(Result::ok).collect(); + let before = present.iter().filter(|id| backend_of(id) == backend).count(); + assert_eq!(again.len(), before, "{backend}: after the fault the clean kernels opened on {} devices, before it on {before}", again.len()); + for m in again { + let c = member::certify(m.as_ref()).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact), "{}: the device lost its grade with the faulted kernels", m.id()); + } + } +} + /// A WebGPU member waits once a window, so a run longer than one wait but made /// of windows shorter than it finishes; and a job a member cannot finish in its /// wait goes to the next member, the answer naming the one that failed. -- 2.54.0 (Apple Git-157) From 294d19a961d1e0950a8bab23fb013de2300ec4a8 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 14:20:16 -0500 Subject: [PATCH 08/15] wai-quantum: the audit draws in every call, the store fails closed, and generation reaches long settles and spans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fourth adversarial check of the landscape capacity rule (#678) confirmed 22 findings. This commit fixes them and keeps each probe as a test. Generation (spec §7.1): - Ladder sizes in a remainder band also run hot and then one sweep at 0, the final sweep an engine runs after hot ones. - The wave comes to rest within a declared sweep limit, where it was dropped before. - The widest wave is also stopped at its bound while it still moves: past 2^9, 2^10 and 2^11 sweeps, and one sweep before it would rest. - The battery's alike cells follow a declared cell count, up to 4,096. - The uncertain span is reached where a table limit is the narrower, and where no span is declared. - The workloads a capacity adds to the battery hold each block at a 10^-8 chance of failing a correct sampler, and read only a few blocks. The audit (spec §7.3): - Every call checks its last chain and one chain of each run it holds, drawn afresh, so timing the host tells a member nothing about the next call. - The re-runs are spread across the host's cores. - An answer no longer carries a key digest that nothing could open. - The text states what the settle audit catches past its budget, and the wall-time cost. The demotion store reads its file at every certification. It merges each demotion under a file lock, writing it to a file of its own and renaming that into place. It fails closed: where the file is corrupt, unreadable or unwritable, or the FPGA host has no HOME, it logs the reason and grades no kernel bit-exact. "parity": null is the default. §2 and §3 list parity, audit and member.demoted. This also carries #673's one WebGPU instance per process, so the NVIDIA driver keeps listing its Vulkan adapter. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-landscape.yml | 10 +- .../2026-11-07-landscape-declared-capacity.md | 5 +- wai/extensions/quantum-landscape.md | 113 ++++-- wai/wai-quantum-landscape-fpga/README.md | 6 +- wai/wai-quantum-landscape-fpga/src/main.rs | 12 +- wai/wai-quantum/docs/landscape.md | 21 +- wai/wai-quantum/schemas/landscape.v1.json | 32 +- .../src/quantum_landscape/capacity.rs | 162 ++++++-- .../src/quantum_landscape/conformance.rs | 71 +++- wai/wai-quantum/src/quantum_landscape/job.rs | 5 +- .../src/quantum_landscape/member.rs | 169 ++++++-- wai/wai-quantum/src/quantum_landscape/run.rs | 67 +++- .../tests/landscape_conformance.rs | 375 +++++++++++++++++- wai/wai-quantum/tests/landscape_devices.rs | 25 ++ 14 files changed, 906 insertions(+), 167 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape.yml b/.github/workflows/wai-quantum-landscape.yml index 76a496284..d5ef68cca 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -125,7 +125,7 @@ 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\. 103 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (103 expected)"; exit 1; } + grep -qE '^test result: ok\. 105 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (105 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 \ @@ -133,6 +133,8 @@ jobs: 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_chi_square_tail_is_the_closed_form \ 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 \ @@ -237,7 +239,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\. 32 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (32 expected)"; exit 1; } + grep -qE '^test result: ok\. 39 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (39 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 @@ -315,7 +317,7 @@ jobs: # 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; 22 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } + grep -qE '^test result: ok\. 10 passed; 0 failed; 0 ignored; 0 measured; 29 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } done wasip2: @@ -364,7 +366,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\. 102 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (102 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 index 3c7010a35..85e3c6d0c 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -6,7 +6,7 @@ - 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 chosen by the job's digest 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 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`). @@ -14,5 +14,6 @@ - **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 and a commitment to the key (`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 reaches every cell a workload can hold (4,096 with none). The bound on what generation costs holds over random capacities, and the text states the bound the test holds. +- 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. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 079a28d1e..b0c246643 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`, `parity`, and the `capacity` that parity holds within, absent where none is declared (§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,13 +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`, `capacity`), `seeds` and `lines`. A `sweep` and a `compare` give `member` as -`id`, `class`, `capacity` 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 @@ -638,8 +643,13 @@ below. 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, as long as a settle can be; + quenched at 0 for 3 sweeps, and held at 1.5 for 2 and then at 0 for one, the final + sweep an engine runs after hot ones; 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 settle can be; and the same wave stopped at its bound while it still moves, past + 2⁹, 2¹⁰ and 2¹¹ sweeps and one sweep before it would rest, so that a settle that ends + at its bound, its last sweep moving a cell, is as long as one can be; - 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, @@ -661,6 +671,9 @@ below. 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 @@ -670,21 +683,26 @@ below. 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; and a - schedule of exactly that many sweeps; + 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 where a cell count is - declared, and 1,023 where none is, read in blocks of three at the cell counts above - and at the last cell; at declared couplings, alike - triangles; at a declared - schedule length, one alike triangle warm and then cold, to 1,024 sweeps; at a - declared field bound, field, cost and span, the workloads above. Identical blocks - cannot reach a table, so a table has none. + declared (so to 4,096), and 1,023 where none is, read in blocks of three: the first + and the last three cells, and three across each power of two from 32; at declared + couplings, alike triangles, read at the first, the last, and each side of each power + of two; 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 400 million cell sweeps hot or settled (nearly all of them battery - workloads, run only for a kernel that is not `bit-exact`), and at most 20 million at - temperature 0 (the run past `2²⁴`, at a fixed point, which the reference skips). A test - holds the bounds over the named capacities and over random ones. + vectors, at most 400 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 100 million cell sweeps, a fifth of them battery workloads, + which run only for a kernel that is not `bit-exact`; the most, for a capacity that + declares every number below what a job can reach, is about 330 million, three fifths 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 @@ -712,7 +730,10 @@ are all alike; there the counts are read over how many cells of the block hold e state, which any order of the cells leaves the same. The statistic is Pearson's, every bin expected fewer than `statistical.pool_below` (5) times pooled into one; with `df` the bins less one, a block passes when it is at most `df + 5·√(2·df)`, and a workload when -every block does. +every block does. The workloads a capacity adds (§7.1) are held wider, so that they fail +a correct sampler, whatever its draws, less than once in a million between them: each +block at the value its statistic passes with chance 10⁻⁸ for its `df`. A sampler that +leaves a cell unmoved, or draws one from another law, fails them all the same. 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 @@ -734,8 +755,12 @@ as well as on certification, and the check rests only on what the host computes 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), one chain of each - seed's run, and the last chain of every call to the member; +- 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 across its cores after + the call returns and before it makes the next; - 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 @@ -746,9 +771,11 @@ of the job again on the reference and compares the bits: 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`: each -run's seed and its chains checked, or how much of a settle) and a commitment to the key, -its BLAKE3 digest, which stays the same for as long as the host runs. +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, @@ -758,7 +785,14 @@ places it again. The host keeps the demotion under the member's identity, its id 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. +afresh. Hosts and jobs may share the file: the host reads it afresh whenever it certifies a +member, and merges a demotion into it under a lock (the file of its name with `.lock` added), +writing it whole to a file of its own and renaming that into place, so that none loses +another's. The store fails closed: where the file cannot be read, does not parse or +cannot be written, or a host that keeps one has no file to name, the host says so on its +standard error and grades no kernel `bit-exact`, holding the member at `uncertified` and +saying why in `member.demoted`; a demotion it made and could not write it keeps for as +long as it runs, and says so. 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 @@ -766,17 +800,22 @@ reference's bits. A host placing jobs places it only where the job asks for it 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: one chain for each run and one for each call, so for a run of `R` chains in -calls of `B` at most `(⌈R/B⌉ + 1)/R` of its sampling work again; for a settle, at most -2²⁴ of work, and past it the whole settle again one job in 8. 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 sooner -than the reference does, on the first such job; and one in a settle past the budget, -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 +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. That is +work on the CPU, which a device is faster than, so in wall time it costs more than its +share of the work: WALLTIME_PLACEHOLDER. 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. diff --git a/wai/wai-quantum-landscape-fpga/README.md b/wai/wai-quantum-landscape-fpga/README.md index 1dd56552b..6ec02b477 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -68,7 +68,11 @@ sampling kernel that misses, the host tries the statistical battery within it. A 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. +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, and merges a demotion into it under the file's own lock, +so jobs and hosts sharing it keep one another's. Where the file cannot be read, parsed or written, +or `HOME` is unset and no file is named, it says so on standard error and grades no kernel +`bit-exact`. **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 diff --git a/wai/wai-quantum-landscape-fpga/src/main.rs b/wai/wai-quantum-landscape-fpga/src/main.rs index b7b7faf50..dfc3d1755 100644 --- a/wai/wai-quantum-landscape-fpga/src/main.rs +++ b/wai/wai-quantum-landscape-fpga/src/main.rs @@ -299,10 +299,14 @@ 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. - if std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").is_none() - && let Some(home) = std::env::var_os("HOME") - { - wai_quantum::quantum_landscape::member::keep_demotions_in(std::path::PathBuf::from(home).join(".local/state/wai-quantum-landscape-fpga/demotions.json")); + // The file is read when the board is certified, which is after its lock is taken, and + // merged under a lock of its own; with no file to name, no kernel is graded bit-exact. + if std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").is_none() { + 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 { diff --git a/wai/wai-quantum/docs/landscape.md b/wai/wai-quantum/docs/landscape.md index 781bc7362..debb9937f 100644 --- a/wai/wai-quantum/docs/landscape.md +++ b/wai/wai-quantum/docs/landscape.md @@ -246,15 +246,18 @@ grade, and one that declares less than the floor (`capacity::floor`, what the co 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 one chain of -every seed's run and the last chain of every call again on the reference, drawn with a key the host -makes at start, 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), so certifying again does not clear it. A kernel graded -`statistical` is placed only where a job asks for it (`"parity": "statistical"`). +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, across the host's cores before the next call; 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, read at every certification and merged under a lock), so +certifying again does not clear it; where that file cannot be read or written, no kernel is graded +`bit-exact`. A kernel graded `statistical` is placed only where a job asks for it +(`"parity": "statistical"`). ```rust norun=account use wai_quantum::quantum_landscape::capacity::{Capacity, Limits}; diff --git a/wai/wai-quantum/schemas/landscape.v1.json b/wai/wai-quantum/schemas/landscape.v1.json index ca9204022..6008c954d 100644 --- a/wai/wai-quantum/schemas/landscape.v1.json +++ b/wai/wai-quantum/schemas/landscape.v1.json @@ -130,7 +130,7 @@ "description": "What the member has earned for this kernel." }, "demoted": { - "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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": { @@ -709,15 +709,11 @@ }, "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "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": [ - "key", "checked" ], "properties": { - "key": { - "type": "string" - }, "checked": { "type": "array", "items": { @@ -831,7 +827,7 @@ } }, "demoted": { - "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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": { @@ -990,15 +986,11 @@ }, "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "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": [ - "key", "checked" ], "properties": { - "key": { - "type": "string" - }, "checked": { "type": "array", "items": { @@ -1109,7 +1101,7 @@ } }, "demoted": { - "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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": { @@ -1255,15 +1247,11 @@ }, "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "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": [ - "key", "checked" ], "properties": { - "key": { - "type": "string" - }, "checked": { "type": "array", "items": { @@ -1343,7 +1331,7 @@ "description": "What the member has earned for this kernel." }, "demoted": { - "description": "Why the host's audit demoted the member to uncertified (extensions/quantum-landscape.md §7.3): a kernel graded bit-exact gave other bits than the reference's. Absent until it has.", + "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": { @@ -1408,15 +1396,11 @@ }, "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): the commitment to its key, and each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.", + "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": [ - "key", "checked" ], "properties": { - "key": { - "type": "string" - }, "checked": { "type": "array", "items": { diff --git a/wai/wai-quantum/src/quantum_landscape/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs index 4cb6734ad..3ec7aaf9e 100644 --- a/wai/wai-quantum/src/quantum_landscape/capacity.rs +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -421,8 +421,7 @@ pub const MOST_GENERATED_COLD: u64 = 20_000_000; /// 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 and the blocks a battery twin reads: -/// every power of two, one less and one more, from 1 to `most`, and `most` +/// 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(); @@ -526,6 +525,13 @@ fn pinned(b: i64, sign: i64, most: i64, cost: i64) -> Option { (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() } +} + /// 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(); @@ -852,14 +858,31 @@ impl Capacity { // 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); + // A sweep at 0 straight after hot ones: an engine's final sweep. + o.chain(format!("ladder/cells-{n}/hot-then-0"), &l, &w, Held(vec![(1.5, 2), (0.0, 1)]), 4); } } // A wave settling down as many cells as a workload can hold, a cell a sweep: - // a settle as long as a settle within the capacity can be. + // a settle as long as a settle within the capacity can be, coming to rest + // within the sweeps a run may have (of fewer cells where those are fewer). + // And the widest wave stopped at its bound while it still moves: past 2⁹, + // 2¹⁰ and 2¹¹ sweeps, where a narrow sweep counter wraps, and at the most + // sweeps it can move for, so that a settle ending at its bound is held as + // long as one can be. 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); - o.settle(format!("wave/cells-{n}"), &l, &l.lower(), 2 * n as u32 + 2); + let w = l.lower(); + let mut bounds = vec![513u32, 1025, 2049, (n as u32 - 1).min(r.sweeps)]; + bounds.retain(|&b| b >= 2 && (b as usize) < n); + bounds.sort_unstable(); + bounds.dedup(); + for b in bounds { + o.settle(format!("wave/cells-{n}/to-{b}"), &l, &w, b); + } } // 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 @@ -1081,7 +1104,7 @@ impl Capacity { 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); + 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); } @@ -1128,7 +1151,7 @@ impl Capacity { o.every_side(&format!("edge/field-bound-{b}-above-0/pinned/{sign}"), &l, 1.0, 3, 4); } } - o.law(BatteryCase::of("edge/field-bound-above-0", singles(&[f], 0), vec![f.max(1) as f64; 32])); + 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) { @@ -1136,7 +1159,7 @@ impl Capacity { 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(BatteryCase::of("edge/field-above-0", singles(&[f], 0), vec![f.max(1) as f64; 32])); + 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; @@ -1145,7 +1168,7 @@ impl Capacity { 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(BatteryCase::of("edge/cost-above-0", singles(&[0], c), vec![t; 32])); + 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 @@ -1163,14 +1186,37 @@ impl Capacity { 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); } - if let Some(s) = below_u(self.uncertain_span, reachable::UNCERTAIN_SPAN) { - let l = span_reaching(s, r.cells, r.pairs, fa.min(ba), ba); - let w = l.lower(); - let t = s.max(1) as f64; - o.chain(format!("edge/uncertain-span-{s}"), &l, &w, Held(vec![(t, 3)]), 8); - o.chain(format!("edge/uncertain-span-{s}/both"), &l, &w, Held(vec![(t, 2), (COLD, 1), (0.0, 1)]), 8); - if l.len() <= 2 { - o.law(BatteryCase::of("edge/uncertain-span", l, vec![t; 32])); + // 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. @@ -1182,20 +1228,26 @@ impl Capacity { } // The battery across the cells: as many alike cells as a declared count // (with none, 1,023, so that a battery run stays a few seconds), blocks of - // three read at the ladder's sizes and at the last cell; at declared - // couplings and sweeps, alike triangles. - let read = |blocks: usize, cells_each: usize| -> Vec { - let mut out: Vec = ladder_sizes(blocks * cells_each).into_iter().map(|c| (c - 1) / cells_each * cells_each).collect(); - out.push(blocks / 2 * cells_each); - out.retain(|&b| b + cells_each <= blocks * cells_each); + // three read across each power of two and at the first and last cells; at + // declared couplings and sweeps, alike triangles. + // The blocks of a workload of `blocks` blocks that a battery twin reads: the + // first, the last, and those on each side of each power of two from 32. + let read = |blocks: usize, each: usize| -> Vec { + let cells = blocks * each; + let mut out = vec![0, cells - each]; + out.extend((5..usize::BITS).map(|b| 1usize << b).take_while(|&p| p < cells).flat_map(|p| [(p - 1) / each * each, p / each * each])); + out.retain(|&b| b + each <= cells); out.sort_unstable(); out.dedup(); out }; if r.cells > 24 { - let c = if self.cells.is_some_and(|c| c < reachable::CELLS) { r.cells } else { 1023 }; - let mut starts: Vec = ladder_sizes(c).into_iter().map(|k| (k - 1).saturating_sub(2).min(c - 3)).collect(); - starts.extend([c / 2, c - 3]); + let c = if self.cells.is_some() { r.cells } else { 1023 }; + // Few blocks, so that so many tests at once do not fail a correct sampler + // by chance: the first three cells, the last three, and three across each + // power of two from 32 (two cells below it and one past). + let mut starts: Vec = vec![0, c - 3]; + starts.extend((5..usize::BITS).map(|b| 1usize << b).take_while(|&p| p < c).map(|p| (p - 2).min(c - 3))); starts.sort_unstable(); starts.dedup(); o.law(BatteryCase { name: "cells", landscape: singles(&vec![1; c], 0), temperatures: vec![1.0], law: Law::Settled, block: 3, read: starts }); @@ -1389,6 +1441,58 @@ mod tests { } } + /// 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, 31, 33, 63, 4095] { + assert!(n.contains(&format!("ladder/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-513", 513), ("wave/cells-4096/to-1025", 1025), ("wave/cells-4096/to-2049", 2049), ("wave/cells-4096/to-4095", 4095)] { + 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:?}"); + let s1000 = settles(Capacity { sweeps: Some(1000), ..Capacity::NONE }); + assert!(s1000.contains(&("wave/cells-999".to_string(), 1000)) && s1000.contains(&("wave/cells-4096/to-513".to_string(), 513)), "{s1000:?}"); + // The wave rests where its bound lets it, and stopped 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, 4094).state, w.settle(&start, 4095).state, "the last sweep of a stopped wave moves a cell"); + // The battery's alike cells. + let cells = |cap: Capacity| cap.edges_of(Want::BATTERY).battery.into_iter().find(|c| c.name == "cells").map(|c| (c.landscape.len(), c.read)); + let wide = Limits { field_bound: Some(i64::from(i32::MAX)), ..Limits::NONE }; + for (cap, want) in [(Capacity::NONE, 1023), (Capacity { cells: Some(4096), ..Capacity::NONE }, 4096), (Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: wide, above_zero: wide, table_entries: Some(1 << 22), ..Capacity::NONE }, 4096), (Capacity { cells: Some(2000), ..Capacity::NONE }, 2000)] { + let (got, read) = cells(cap).unwrap(); + assert_eq!(got, want, "{}", cap.json()); + assert!(read.len() <= 12 && read.contains(&0) && read.contains(&(want - 3)) && read.contains(&1022.min(want - 3)), "{read:?}"); + } + // 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 @@ -1460,7 +1564,7 @@ mod tests { } 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) = (0u64, 0u64, 0u64); + let (mut vectors, mut cold, mut work, mut battery) = (0u64, 0u64, 0u64, 0u64); cap.generate(Want::ALL, &mut |g| { vectors += 1; match g { @@ -1471,11 +1575,13 @@ mod tests { 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) => work += u64::from(conformance::BATTERY_CHAINS) * c.landscape.len() as u64 * c.temperatures.len() as u64, + Generated::Battery(c) => battery += u64::from(conformance::BATTERY_CHAINS) * c.landscape.len() as u64 * c.temperatures.len() as u64, } true }); - eprintln!("generated for {name}: {vectors} vectors, {work} cell-sweeps hot or settled, {cold} at temperature 0"); + 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()); diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index 5ae1106ec..c5639938c 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -770,11 +770,64 @@ pub fn run_battery(m: &dyn Member) -> Vec { run_battery_within(m, &m.capacity()) } +/// The chance that a correct sampler fails one block of a generated battery +/// workload: each block is held at the chi-square value of its degrees that +/// chance leaves, so that the workloads a capacity adds, some dozens of blocks +/// between them, fail a correct sampler, whatever its draws, less than once in +/// a million (§7.2). A wrong cell fails either. +pub const GENERATED_FALSE: f64 = 1e-8; + +/// The chance that Pearson's statistic of `df` degrees exceeds `x`, a correct +/// sampler's: the upper regularized gamma function `Q(df/2, x/2)`, in closed +/// form for whole degrees. +pub fn chi_square_tail(x: f64, df: usize) -> f64 { + if x <= 0.0 { + return 1.0; + } + let z = x / 2.0; + if df.is_multiple_of(2) { + // e^{−z} Σ_{i f64 { + let t = 1.0 / (1.0 + 0.5 * x.abs()); + let y = t * (-x * x - 1.265_512_23 + t * (1.000_023_68 + t * (0.374_091_96 + t * (0.096_784_18 + t * (-0.186_288_06 + t * (0.278_868_07 + t * (-1.135_203_98 + t * (1.488_515_87 + t * (-0.822_152_23 + t * 0.170_872_77))))))))).exp(); + if x >= 0.0 { y } else { 2.0 - y } +} + +/// The value Pearson's statistic of `df` degrees exceeds with chance `p`. +pub fn chi_square_critical(df: usize, p: f64) -> f64 { + let (mut lo, mut hi) = (0.0f64, df as f64 + 100.0 * (2.0 * df as f64).sqrt() + 200.0); + for _ in 0..200 { + let mid = 0.5 * (lo + hi); + if chi_square_tail(mid, df) > p { lo = mid } else { hi = mid } + } + hi +} + /// [`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 per_call = cap.batch(4096); + let fixed = battery().len(); battery() .into_iter() .chain(cap.edges_of(super::capacity::Want::BATTERY).battery) @@ -811,7 +864,8 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec .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)) + let (chi, df, critical) = chi_square(&got, &expected, u64::from(BATTERY_CHAINS)); + (chi, df, if k < fixed { critical } else { chi_square_critical(df, GENERATED_FALSE) }) }) .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; @@ -826,6 +880,7 @@ fn battery_json() -> Value { "seed": BATTERY_SEED.to_string(), "pool_below": BATTERY_POOL_BELOW, "critical": "df + 5·sqrt(2·df), df the bins after pooling, less one", + "critical_generated": "the value a correct sampler's statistic passes with chance 1e-8, for the workloads a capacity adds (§7.2)", "workloads": battery().iter().map(|c| json!({ "name": c.name, "landscape": landscape_json(&c.landscape), @@ -913,6 +968,20 @@ mod tests { } } + /// The tail a generated workload's critical value comes from is the closed + /// form, odd degrees and even, and the critical value leaves the chance asked. + #[test] + fn the_chi_square_tail_is_the_closed_form() { + for (x, df, want) in [(12.0, 2, 0.002_478_752_176_666_357), (62.0, 26, 8.977_207_619_300_019e-5), (15.0, 1, 1.075_111_767_295_005_4e-4), (50.0, 9, 1.077_238_202_257_469_3e-7), (30.0, 3, 1.380_057_031_293_254_7e-6), (100.0, 80, 0.064_570_368_921_131_95)] { + let got = chi_square_tail(x, df); + assert!((got / want - 1.0).abs() < 1e-5, "{x} at {df}: {got} against {want}"); + } + for (df, want) in [(1, 32.841_253), (2, 36.841_361), (8, 53.169_478), (9, 55.449_105), (26, 88.411_594), (80, 172.346_607)] { + let c = chi_square_critical(df, GENERATED_FALSE); + assert!((c - want).abs() < 1e-3, "{df}: {c} against {want}"); + } + } + #[test] fn a_state_s_index_is_its_digits() { for k in 0..81 { diff --git a/wai/wai-quantum/src/quantum_landscape/job.rs b/wai/wai-quantum/src/quantum_landscape/job.rs index ef8ab41ed..33ea73512 100644 --- a/wai/wai-quantum/src/quantum_landscape/job.rs +++ b/wai/wai-quantum/src/quantum_landscape/job.rs @@ -129,7 +129,8 @@ 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.")), @@ -207,7 +208,7 @@ pub fn answer(c: &Certified<'_>, prepared: &Prepared) -> Result let mut a = answer_unaudited(c, prepared)?; let checked = c.take_audit(); if !checked.is_empty() { - a["audit"] = json!({ "key": run::audit_commitment(), "checked": checked }); + a["audit"] = json!({ "checked": checked }); } Ok(a) } diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index 55cf9fe21..d84f34ff9 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -46,12 +46,13 @@ 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, -/// its sweeps times its couplings and cells, is at most this is run again on -/// the reference whole, every time. +/// 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, chosen by -/// the job's digest so that the choice can be repeated. +/// 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. @@ -169,53 +170,152 @@ pub trait Member { /// 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 reads it afresh at every certification, under a shared +/// lock, and records a demotion by reading, merging and writing it under an +/// exclusive one (on the file of its name with `.lock` added), so that hosts and jobs +/// sharing it never lose one another's demotions. It fails closed: where the +/// file cannot be read, does not parse or cannot be written, 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`]). 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>, } impl Demotions { - /// A store kept in `path` (read now, written on every demotion), or in - /// memory alone with none. + /// A store kept in `path`, or in memory alone with none (for this process). pub fn open(path: Option) -> Demotions { - let records = path.as_ref().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|t| serde_json::from_str::>(&t).ok()).unwrap_or_default(); - Demotions { path, records: Mutex::new(records) } + Demotions { path, records: Mutex::new(BTreeMap::new()), broken: Mutex::new(None) } } - /// Why an identity was demoted, if it was. - pub fn get(&self, identity: &str) -> Option { - self.records.lock().ok()?.get(identity).and_then(|r| r["why"].as_str().map(String::from)) + /// 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::open(None); + d.fail(why); + d } - /// Remember a demotion, and write the store where it has a file. The first reason stands. - pub fn record(&self, identity: &str, why: &str, capacity: &Capacity) { - let Ok(mut r) = self.records.lock() else { return }; - r.entry(identity.to_string()).or_insert_with(|| json!({ "why": why, "capacity": capacity.json() })); + /// 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 + } + + /// The lock beside the file, held shared or exclusive until it is dropped; + /// its folder made where it is missing. + 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| 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 = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&at).map_err(|e| format!("its lock {} cannot be opened: {e}", at.display()))?; + if exclusive { f.lock() } else { f.lock_shared() }.map_err(|e| format!("its lock {} cannot be taken: {e}", at.display()))?; + Ok(f) + } + + /// The file's records: none where it does not exist yet. + fn read(path: &std::path::Path) -> Result, String> { + match std::fs::read(path) { + Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| format!("{} does not parse as demotions: {e}", path.display())), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(BTreeMap::new()), + Err(e) => Err(format!("{} cannot be read: {e}", path.display())), + } + } + + /// Why an identity was demoted, if it was, its file read afresh; 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 mut records = self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?; if let Some(p) = &self.path { - if let Some(dir) = p.parent() { - let _ = std::fs::create_dir_all(dir); - } - let tmp = p.with_extension("tmp"); - if std::fs::write(&tmp, serde_json::to_string_pretty(&*r).unwrap_or_default()).is_ok() { - let _ = std::fs::rename(&tmp, p); + let read = Demotions::lock(p, false).and_then(|_held| Demotions::read(p)); + match read { + Ok(on_file) => { + for (k, v) in on_file { + records.entry(k).or_insert(v); + } + } + Err(e) => return Err(self.fail(e)), } } + Ok(records.get(identity).map(|r| r["why"].as_str().map_or_else(|| r.to_string(), String::from))) + } + + /// Remember a demotion, and where the store has a file, merge it into the + /// file under its exclusive lock: written whole to a file of this process's + /// own, synced, and renamed into place. The first reason stands. An error + /// where it could not be kept past this process. + pub fn record(&self, identity: &str, why: &str, capacity: &Capacity) -> Result<(), String> { + static WRITES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let entry = json!({ "why": why, "capacity": capacity.json() }); + let mut records = self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?; + records.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 write = || -> Result<(), String> { + let _held = Demotions::lock(p, true)?; + let mut on_file = Demotions::read(p)?; + on_file.entry(identity.to_string()).or_insert(entry); + for (k, v) in on_file.iter() { + records.entry(k.clone()).or_insert_with(|| v.clone()); + } + let text = serde_json::to_string_pretty(&on_file).map_err(|e| e.to_string())?; + let name = p.file_name().map_or_else(|| "demotions".into(), |n| n.to_string_lossy().into_owned()); + let tmp = p.with_file_name(format!(".{name}.{}.{}.tmp", std::process::id(), WRITES.fetch_add(1, std::sync::atomic::Ordering::Relaxed))); + let put = || -> std::io::Result<()> { + use std::io::Write; + let mut f = std::fs::File::create(&tmp)?; + f.write_all(text.as_bytes())?; + f.sync_all()?; + std::fs::rename(&tmp, p) + }; + put().map_err(|e| { + let _ = std::fs::remove_file(&tmp); + format!("{} cannot be written: {e}", p.display()) + }) + }; + write().map_err(|e| self.fail(e)) } } static DEMOTIONS: OnceLock = OnceLock::new(); /// The host's store of demotions: the file `WAI_LANDSCAPE_DEMOTIONS` names, or -/// one a host chose with [`keep_demotions_in`] before its first use; in memory -/// alone otherwise, for this process. +/// 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").map(PathBuf::from))) } -/// Keep the host's demotions in `path` (a host that runs members does so at +/// 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_in(path: PathBuf) -> bool { - DEMOTIONS.set(Demotions::open(Some(path))).is_ok() +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. @@ -260,7 +360,8 @@ pub struct Certified<'a> { /// 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). From + /// `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 @@ -310,8 +411,11 @@ impl<'a> Certified<'a> { pub fn demote(&self, why: String) { let mut d = self.demoted.borrow_mut(); if d.is_none() { - self.store.record(&identity_of(self.member), &why, &self.capacity); - *d = Some(why); + 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})"), + }); } } @@ -359,7 +463,8 @@ 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, capacity: m.capacity(), certified: false, store: demotions(), demoted: RefCell::new(demotions().get(&identity_of(m))), audit_log: RefCell::new(Vec::new()) }) + 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 within its declared capacity: its settle kernel on every @@ -381,7 +486,9 @@ pub fn certify_with<'a>(m: &'a dyn Member, store: &'a Demotions) -> Result>, reading: Option &'static [u8; 32] { static KEY: std::sync::OnceLock<[u8; 32]> = std::sync::OnceLock::new(); KEY.get_or_init(|| { @@ -336,12 +338,6 @@ fn audit_key() -> &'static [u8; 32] { }) } -/// A commitment to the host's audit key, which every audited answer carries: -/// the key's BLAKE3 digest. -pub fn audit_commitment() -> String { - blake3::hash(audit_key()).to_hex().to_string() -} - /// 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. @@ -437,16 +433,17 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 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`: the last chain of every - // call, and one chain of the run chosen by the job's digest, again on the reference. + // 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, across the host's cores, + // 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; - // One chain of each seed's run, drawn with the host's key, and the last chain of every call. - let chosen: Vec = if audited { seeds.iter().enumerate().map(|(s, &seed)| s as u64 * r + audit_draw(job, seed, 1) % r.max(1)).collect() } else { Vec::new() }; let mut checked: Vec> = vec![Vec::new(); seeds.len()]; let mut found: Vec = vec![(None, 0, 0, None); seeds.len()]; let mut at = 0u64; @@ -464,22 +461,25 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 } let reading = m.meter(); if audited { - let mut these: Vec = chosen.iter().copied().filter(|k| (at..end).contains(k)).collect(); + 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 { + for &k in &these { checked[(k / r) as usize].push(k % r); - { - let (i, ch) = ((k - at) as usize, &chains[(k - at) as usize]); - if let Err(e) = member::check_chain(&outcomes[i], w, temperatures) { - return Err(demote(c, KERNEL_SAMPLE, format!("gave a chain the host's checks refuse ({e})"))); - } - if outcomes[i] != sample::anneal_with(w, &ch.start, temperatures, ch.seed) { - return Err(demote(c, KERNEL_SAMPLE, format!("gave chain {} of a call of {} other bits than the reference's", i + 1, chains.len()))); - } + 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) { @@ -514,6 +514,31 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 .collect()) } +/// The first of a call's chains at `places` whose outcome is not the +/// reference's, each run again on the reference, side by side over the host's +/// cores (on one where it can start no thread). +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)); + let cores = std::thread::available_parallelism().map_or(1, |p| p.get()).clamp(1, 16).min(places.len().max(1)); + let parts: Vec<&[usize]> = places.chunks(places.len().div_ceil(cores).max(1)).collect(); + std::thread::scope(|s| { + let (first, rest) = parts.split_first()?; + let others: Vec<_> = rest.iter().map(|&part| (part, std::thread::Builder::new().name("wai-audit".into()).spawn_scoped(s, move || wrong(part)))).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> { diff --git a/wai/wai-quantum/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index e03440471..bee6cc38f 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -433,6 +433,18 @@ enum Fault { 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, + /// 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), /// 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. @@ -469,6 +481,12 @@ fn in_the_tile_gap(n: usize, i: usize) -> bool { 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 = match self { @@ -527,7 +545,14 @@ impl Fault { } if t <= 0.0 { if !rests { - let next = self.sweep_in(w, &s, mixed, true); + let mut next = self.sweep_in(w, &s, mixed, true); + if k > 0 && temperatures[k - 1] > 0.0 { + for i in 0..n { + if (self == Fault::Final64 && in_the_tile_gap(n, i)) || (self == Fault::Final32 && in_the_warp_gap(n, i)) { + next[i] = s[i]; + } + } + } rests = next == s; s = next; } @@ -596,6 +621,20 @@ impl Member for Fault { 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::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::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 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 { @@ -1520,7 +1559,8 @@ fn review3_a_settle_audit_the_member_cannot_opt_out_of() { } /// 16, 17: the audit draws with the host's key and afresh each time, and every -/// audited answer says what it checked under a commitment to the key. +/// 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); @@ -1529,7 +1569,7 @@ fn review3_the_audit_is_keyed_and_drawn_afresh() { let mut seen = Vec::new(); for _ in 0..8 { let a = job::call_certified(&c, "run", &req, MAX_JOB_BYTES).unwrap(); - assert_eq!(a["audit"]["key"].as_str(), Some(wai_quantum::quantum_landscape::run::audit_commitment().as_str())); + 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:?}"); @@ -1640,3 +1680,332 @@ fn review3_generation_reaches_within_the_capacity() { ("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}; + let program = "vars 6\nagree 0 1\noppose 1 2\nagree 2 3\noppose 3 4\nagree 4 5\nfield 0 1\nsolve t0=2 restarts=128 steps=4\n"; + 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), + ]); +} diff --git a/wai/wai-quantum/tests/landscape_devices.rs b/wai/wai-quantum/tests/landscape_devices.rs index 7c5b1de2c..8ab82aa9b 100644 --- a/wai/wai-quantum/tests/landscape_devices.rs +++ b/wai/wai-quantum/tests/landscape_devices.rs @@ -501,4 +501,29 @@ 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), + ]; + 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(); + 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); + assert!(c.demoted().is_none(), "{}", m.id()); + println!(" {} audited {audited:.3} s, unaudited {unaudited:.3} s: {:+.0}% for the audit, {:.2} of the reference's time", m.id(), 100.0 * (audited / unaudited - 1.0), audited / t_cpu); + } + } } -- 2.54.0 (Apple Git-157) From 0fe0fe8dad638462b5fbb7ddc8f173caad94c424 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 14:31:52 -0500 Subject: [PATCH 09/15] =?UTF-8?q?wai-quantum:=20the=20published=20conforma?= =?UTF-8?q?nce=20set=20keeps=20its=20bytes;=20the=20generated=20battery's?= =?UTF-8?q?=20critical=20value=20is=20in=20the=20code=20and=20=C2=A77.2=20?= =?UTF-8?q?alone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/src/quantum_landscape/conformance.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index c5639938c..1ea45954a 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -880,7 +880,6 @@ fn battery_json() -> Value { "seed": BATTERY_SEED.to_string(), "pool_below": BATTERY_POOL_BELOW, "critical": "df + 5·sqrt(2·df), df the bins after pooling, less one", - "critical_generated": "the value a correct sampler's statistic passes with chance 1e-8, for the workloads a capacity adds (§7.2)", "workloads": battery().iter().map(|c| json!({ "name": c.name, "landscape": landscape_json(&c.landscape), -- 2.54.0 (Apple Git-157) From a4f5b2dd617341577191012d887456d88bddceef Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 14:36:57 -0500 Subject: [PATCH 10/15] =?UTF-8?q?wai-quantum:=20=C2=A77.3=20states=20the?= =?UTF-8?q?=20audit's=20wall-time=20cost,=20measured=20on=20Metal=20and=20?= =?UTF-8?q?CUDA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- wai/extensions/quantum-landscape.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index b0c246643..8f8b78d3b 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -804,8 +804,14 @@ Its cost: in every call, its last chain and one chain of each run it holds chain 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. That is -work on the CPU, which a device is faster than, so in wall time it costs more than its -share of the work: WALLTIME_PLACEHOLDER. What it catches: a fault that makes a fraction `q` +work on the host's CPU, which a device outruns, so in wall time the audit costs more than +its share of the work, and most where a job is many short runs. Measured by the `timings` +test of `landscape_devices`, audited against the same member run by name: on an M3 +Ultra's GPU through Metal, a replay of 256 seeds of 8 chains of 64 cells for 500 sweeps took +41% longer, and of 256 seeds of one chain 32% longer; on an RTX 4050 laptop GPU through +CUDA, 122% and 178% longer. A run of 4,096 chains of 64 cells, or of 1,024 chains of 1,024 +cells, took at most 3% longer on either. Audited, each of these jobs still took at most +half the reference's time alone. 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 -- 2.54.0 (Apple Git-157) From 054017bc82b8bd8a1b093906bb0c9bcceddae730 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 14:54:04 -0500 Subject: [PATCH 11/15] =?UTF-8?q?wai-quantum:=20the=20per-call=20draw's=20?= =?UTF-8?q?probe=20runs=20chains=20that=20come=20to=20rest=20apart,=20and?= =?UTF-8?q?=20=C2=A77.3=20gives=20the=20wall=20time=20over=20two=20runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cheat that copies a call's last chain went unseen on a six-cell job whose chains often came to rest in the same state; on a ring of 32 cells they do not. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/quantum-landscape.md | 14 ++++++++------ wai/wai-quantum/tests/landscape_conformance.rs | 12 +++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 8f8b78d3b..d7b8d89ee 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -806,12 +806,14 @@ and for a job of many seeds' runs in one call, one chain of each and one more; f settle, at most 2²⁴ of work, and past it the whole settle again one job in 8. That 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, and most where a job is many short runs. Measured by the `timings` -test of `landscape_devices`, audited against the same member run by name: on an M3 -Ultra's GPU through Metal, a replay of 256 seeds of 8 chains of 64 cells for 500 sweeps took -41% longer, and of 256 seeds of one chain 32% longer; on an RTX 4050 laptop GPU through -CUDA, 122% and 178% longer. A run of 4,096 chains of 64 cells, or of 1,024 chains of 1,024 -cells, took at most 3% longer on either. Audited, each of these jobs still took at most -half the reference's time alone. What it catches: a fault that makes a fraction `q` +test of `landscape_devices` in two runs on shared hosts, audited against the same member run +by name: on an M3 Ultra's GPU through Metal, a replay of 256 seeds of 8 chains of 64 cells +for 500 sweeps took 39% to 41% longer, and of 256 seeds of one chain 21% to 32% longer; on +an RTX 4050 laptop GPU through CUDA, 122% to 254% and 178% to 251% longer. A run of 4,096 +chains of 64 cells took at most 3% longer through Metal and 3% to 36% through CUDA (a few +milliseconds), and one of 1,024 chains of 1,024 cells at most 2% longer on either. Audited, +each of these jobs still took at most two thirds of the reference's time alone. 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 diff --git a/wai/wai-quantum/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index bee6cc38f..3e89fb2f0 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -1781,7 +1781,17 @@ impl Member for AfterTheFirstCall { #[test] fn review4_the_audit_draws_in_every_call() { use wai_quantum::quantum_landscape::member::{Demotions, certify_with}; - let program = "vars 6\nagree 0 1\noppose 1 2\nagree 2 3\noppose 3 4\nagree 4 5\nfield 0 1\nsolve t0=2 restarts=128 steps=4\n"; + // 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); -- 2.54.0 (Apple Git-157) From cc0f809dd7525232587abd43541c9fcde4618ac6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 9 Nov 2026 08:28:57 -0500 Subject: [PATCH 12/15] wai-quantum: the store proves its folder takes a rewrite, and generation and the battery reach the longest settles, every remainder and every cell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fifth adversarial check of the landscape capacity rule (#678) confirmed 15 findings. This commit fixes them and keeps each probe as a test (review5_* in landscape_conformance). The demotion store: - At every certification it writes, syncs and removes a file as large as its next rewrite, so a folder that stopped taking writes, 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. - Its lock is waited for at most 5 s; past that, that certification grades nothing bit-exact, and the next waits again. - A rewrite keeps the file's mode and removes files a crashed writer left. Generation: - The wave is stopped one sweep before its rest. - A settle of two-cell units that moves for 2n - 10 sweeps is stopped past 2^9 to 2^12 sweeps and at its last moving sweep. - The final sweep at 0 runs at odd, even and middle remainders of a partial warp and workgroup, after two, three and a cold sweep above 0, on cells tied at the cost so that it moves them. The battery: - Its 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, held by a likelihood-ratio bound from the method of types, which holds whatever the law. The audit's re-runs take at most a quarter of the host's cores, 4 at most over the process, at a low priority. §7.3 states its cost as work. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-landscape.yml | 6 +- .../2026-11-07-landscape-declared-capacity.md | 1 + wai/extensions/quantum-landscape.md | 128 ++++-- wai/wai-quantum-landscape-fpga/README.md | 8 +- wai/wai-quantum-landscape-fpga/src/main.rs | 6 +- wai/wai-quantum/docs/landscape.md | 9 +- .../src/quantum_landscape/capacity.rs | 170 +++++--- .../src/quantum_landscape/conformance.rs | 135 +++--- .../src/quantum_landscape/member.rs | 193 ++++++--- wai/wai-quantum/src/quantum_landscape/run.rs | 79 +++- .../tests/landscape_conformance.rs | 399 +++++++++++++++++- wai/wai-quantum/tests/landscape_devices.rs | 16 +- 12 files changed, 923 insertions(+), 227 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape.yml b/.github/workflows/wai-quantum-landscape.yml index d5ef68cca..048de75e6 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -134,7 +134,7 @@ jobs: 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_chi_square_tail_is_the_closed_form \ + 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 \ @@ -239,7 +239,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\. 39 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (39 expected)"; exit 1; } + grep -qE '^test result: ok\. 44 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (44 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 @@ -317,7 +317,7 @@ jobs: # 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; 29 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } + grep -qE '^test result: ok\. 10 passed; 0 failed; 0 ignored; 0 measured; 35 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } done wasip2: diff --git a/wai/changes/2026-11-07-landscape-declared-capacity.md b/wai/changes/2026-11-07-landscape-declared-capacity.md index 85e3c6d0c..319a3bb03 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -17,3 +17,4 @@ - 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 take at most a quarter of the host's cores, 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. diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index d7b8d89ee..30be5e791 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -643,13 +643,28 @@ below. 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 held at 1.5 for 2 and then at 0 for one, the final - sweep an engine runs after hot ones; 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 settle can be; and the same wave stopped at its bound while it still moves, past - 2⁹, 2¹⁰ and 2¹¹ sweeps and one sweep before it would rest, so that a settle that ends - at its bound, its last sweep moving a cell, is as long as one can be; + 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 settle 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; + - the longest-moving settle the rules know: `n` cells (`C`, or as many as `P` lets) in + 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 from every cell at 0 moves a cell in every sweep to sweep `2n − 10` and rests at + `2n − 9`: stopped at its bound just past 2⁹, 2¹⁰, 2¹¹ and 2¹² sweeps, where a narrow + sweep counter wraps, and at its last moving sweep (or the most a run may have), so + that a settle ending at its bound, its last sweep moving, is held as long as the rules + know one to be; - 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, @@ -686,23 +701,24 @@ below. 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 where a cell count is - declared (so to 4,096), and 1,023 where none is, read in blocks of three: the first - and the last three cells, and three across each power of two from 32; at declared - couplings, alike triangles, read at the first, the last, and each side of each power - of two; 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. + - 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 400 million cell sweeps hot or settled, and at most 20 million at + The rules bound what certification costs, whatever the capacity: at most MOSTV generated + vectors, at most MOSTW 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 100 million cell sweeps, a fifth of them battery workloads, - which run only for a kernel that is not `bit-exact`; the most, for a capacity that - declares every number below what a job can reach, is about 330 million, three fifths of - them battery workloads. A test holds the bounds over the named capacities and over - random ones, and prints each one's share. + capacity they come to about NONEW million cell sweeps, NONESHARE of them battery + workloads, which run only for a kernel that is not `bit-exact`; the most, for a + capacity that declares every number below what a job can reach, is about MAXW million, + MAXSHARE 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 @@ -730,10 +746,24 @@ are all alike; there the counts are read over how many cells of the block hold e state, which any order of the cells leaves the same. The statistic is Pearson's, every bin expected fewer than `statistical.pool_below` (5) times pooled into one; with `df` the 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 wider, so that they fail -a correct sampler, whatever its draws, less than once in a million between them: each -block at the value its statistic passes with chance 10⁻⁸ for its `df`. A sampler that -leaves a cell unmoved, or draws one from another law, fails them all the same. +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 and of 31 chains in turn (no +more than the member takes), so that every call ends in a partial warp and workgroup. 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 from another law, 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 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 @@ -759,8 +789,11 @@ of the job again on the reference and compares the bits: 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 across its cores after - the call returns and before it makes the next; + 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 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, each at a low priority (a nice value 10 higher on Linux, the background band + on macOS), so that one audited job does not crowd out another's work; - 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 @@ -787,12 +820,21 @@ in a file where it names one, so that it outlives the process: certifying the me or its declaring another capacity, does not clear it. Only another identity is certified afresh. Hosts and jobs may share the file: the host reads it afresh whenever it certifies a member, and merges a demotion into it under a lock (the file of its name with `.lock` added), -writing it whole to a file of its own and renaming that into place, so that none loses -another's. The store fails closed: where the file cannot be read, does not parse or -cannot be written, or a host that keeps one has no file to name, the host says so on its -standard error and grades no kernel `bit-exact`, holding the member at `uncertified` and -saying why in `member.demoted`; a demotion it made and could not write it keeps for as -long as it runs, and says so. +writing it whole to a file of its own with the file's mode and renaming that into place, +so that none loses another's; a writer's file that a crash left behind it removes under +that lock. Where the path is a link, the store is the link's target: its lock, its files +and its rename. 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, where the file cannot be read or does not parse, +where its folder does not take a file as large as the file's next rewrite (written, +synced and removed at every certification, so that a folder no longer written to, a full +disk or a limit on a file's size is found before a member is graded on it), where a write +fails, or where a host that keeps one has no file to name. A demotion it made and could +not write it keeps for as long as it runs, and says so. The host waits for the lock at +most 5 s (`member::STORE_LOCK_WAIT`); past that, 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 @@ -803,17 +845,13 @@ 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. That 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, and most where a job is many short runs. Measured by the `timings` -test of `landscape_devices` in two runs on shared hosts, audited against the same member run -by name: on an M3 Ultra's GPU through Metal, a replay of 256 seeds of 8 chains of 64 cells -for 500 sweeps took 39% to 41% longer, and of 256 seeds of one chain 21% to 32% longer; on -an RTX 4050 laptop GPU through CUDA, 122% to 254% and 178% to 251% longer. A run of 4,096 -chains of 64 cells took at most 3% longer through Metal and 3% to 36% through CUDA (a few -milliseconds), and one of 1,024 chains of 1,024 cells at most 2% longer on either. Audited, -each of these jobs still took at most two thirds of the reference's time alone. What it -catches: a fault that makes a fraction `q` +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). +WALLTIME_RANGES 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 diff --git a/wai/wai-quantum-landscape-fpga/README.md b/wai/wai-quantum-landscape-fpga/README.md index 6ec02b477..32fd2a116 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -70,9 +70,11 @@ the demotion in `~/.local/state/wai-quantum-landscape-fpga/demotions.json` (or t `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, and merges a demotion into it under the file's own lock, -so jobs and hosts sharing it keep one another's. Where the file cannot be read, parsed or written, -or `HOME` is unset and no file is named, it says so on standard error and grades no kernel -`bit-exact`. +waited for at most 5 s, so jobs and hosts sharing it keep one another's; a link is followed to its +target. Where the file cannot be read, parsed or written, its folder does not take a rewrite of it +(a full disk, a read-only folder), 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`. **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 diff --git a/wai/wai-quantum-landscape-fpga/src/main.rs b/wai/wai-quantum-landscape-fpga/src/main.rs index dfc3d1755..bfb357c57 100644 --- a/wai/wai-quantum-landscape-fpga/src/main.rs +++ b/wai/wai-quantum-landscape-fpga/src/main.rs @@ -300,8 +300,10 @@ fn main() { // 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, and - // merged under a lock of its own; with no file to name, no kernel is graded bit-exact. - if std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").is_none() { + // merged under a lock of its own, waited for at most `member::STORE_LOCK_WAIT` while + // the board is held; 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"))), diff --git a/wai/wai-quantum/docs/landscape.md b/wai/wai-quantum/docs/landscape.md index debb9937f..a284aa100 100644 --- a/wai/wai-quantum/docs/landscape.md +++ b/wai/wai-quantum/docs/landscape.md @@ -248,15 +248,16 @@ 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, across the host's cores before the next call; and the +with a key the host makes at start, before the next call, on at most a quarter of the host's cores +(4 at most, over the whole process) at a low priority; 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, read at every certification and merged under a lock), so -certifying again does not clear it; where that file cannot be read or written, no kernel is graded -`bit-exact`. A kernel graded `statistical` is placed only where a job asks for it +`WAI_LANDSCAPE_DEMOTIONS` names, or a link's target, read at every certification with a check that +its folder takes a rewrite, and merged under a lock waited for at most 5 s), so certifying again +does not clear it; where that file cannot be read or written, no kernel is graded `bit-exact`. A kernel graded `statistical` is placed only where a job asks for it (`"parity": "statistical"`). ```rust norun=account diff --git a/wai/wai-quantum/src/quantum_landscape/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs index 3ec7aaf9e..2088290c6 100644 --- a/wai/wai-quantum/src/quantum_landscape/capacity.rs +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -410,7 +410,7 @@ 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 = 400_000_000; +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; @@ -532,6 +532,23 @@ fn alike3(name: &'static str, f: i64, c: i64, t: f64) -> conformance::BatteryCas 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`, and rests at `2n − 9`. +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 +} + /// 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(); @@ -858,30 +875,61 @@ impl Capacity { // 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); - // A sweep at 0 straight after hot ones: an engine's final sweep. - o.chain(format!("ladder/cells-{n}/hot-then-0"), &l, &w, Held(vec![(1.5, 2), (0.0, 1)]), 4); + } + } + // 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: // a settle as long as a settle within the capacity can be, coming to rest - // within the sweeps a run may have (of fewer cells where those are fewer). - // And the widest wave stopped at its bound while it still moves: past 2⁹, - // 2¹⁰ and 2¹¹ sweeps, where a narrow sweep counter wraps, and at the most - // sweeps it can move for, so that a settle ending at its bound is held as - // long as one can be. + // 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 w = l.lower(); - let mut bounds = vec![513u32, 1025, 2049, (n as u32 - 1).min(r.sweeps)]; - bounds.retain(|&b| b >= 2 && (b as usize) < n); - bounds.sort_unstable(); - bounds.dedup(); - for b in bounds { - o.settle(format!("wave/cells-{n}/to-{b}"), &l, &w, b); + let b = (n as u32).min(r.sweeps); + o.settle(format!("wave/cells-{n}/to-{b}"), &l, &l.lower(), b); + } + // The longest-moving settle the rules know: units of two cells that move + // for 2n − 10 sweeps from every cell at 0, twice a wave's; stopped at its + // bound while it still moves, just past 2⁹, 2¹⁰, 2¹¹ and 2¹² sweeps, where a + // narrow sweep counter wraps, and at its last moving sweep, so that a settle + // ending at its bound is held as long as the rules know one to be. + if o.wants(true, false) { + let units = (r.cells / 2).min((r.pairs + 2) / 3); + if units >= 8 { + let l = slow(units); + let w = l.lower(); + let last = (4 * units as u32 - 10).min(r.sweeps); + let mut bounds = vec![513u32, 1025, 2049, 4097, last]; + bounds.retain(|&b| b <= last); + bounds.dedup(); + for b in bounds { + o.settle(format!("slow/cells-{}/to-{b}", 2 * units), &l, &w, b); + } } } // Calls: every number of chains from 1 to 32 in one call, so that every @@ -1226,31 +1274,26 @@ impl Capacity { 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 declared count - // (with none, 1,023, so that a battery run stays a few seconds), blocks of - // three read across each power of two and at the first and last cells; at - // declared couplings and sweeps, alike triangles. - // The blocks of a workload of `blocks` blocks that a battery twin reads: the - // first, the last, and those on each side of each power of two from 32. - let read = |blocks: usize, each: usize| -> Vec { - let cells = blocks * each; - let mut out = vec![0, cells - each]; - out.extend((5..usize::BITS).map(|b| 1usize << b).take_while(|&p| p < cells).flat_map(|p| [(p - 1) / each * each, p / each * each])); - out.retain(|&b| b + each <= cells); - out.sort_unstable(); - out.dedup(); + // 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 = if self.cells.is_some() { r.cells } else { 1023 }; - // Few blocks, so that so many tests at once do not fail a correct sampler - // by chance: the first three cells, the last three, and three across each - // power of two from 32 (two cells below it and one past). - let mut starts: Vec = vec![0, c - 3]; - starts.extend((5..usize::BITS).map(|b| 1usize << b).take_while(|&p| p < c).map(|p| (p - 2).min(c - 3))); - starts.sort_unstable(); - starts.dedup(); - o.law(BatteryCase { name: "cells", landscape: singles(&vec![1; c], 0), temperatures: vec![1.0], law: Law::Settled, block: 3, read: starts }); + 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); @@ -1263,7 +1306,7 @@ impl Capacity { 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: read(blocks, 3) }); + 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) { @@ -1448,32 +1491,55 @@ mod tests { #[test] fn long_settles_bands_cells_and_spans_are_generated() { let n = names(&Capacity::NONE.edges()); - for size in [17, 31, 33, 63, 4095] { - assert!(n.contains(&format!("ladder/cells-{size}/hot-then-0")), "{size}"); + 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-513", 513), ("wave/cells-4096/to-1025", 1025), ("wave/cells-4096/to-2049", 2049), ("wave/cells-4096/to-4095", 4095)] { + for (name, bound) in [("wave/cells-4096", 8194), ("wave/cells-4096/to-4096", 4096), ("slow/cells-4096/to-513", 513), ("slow/cells-4096/to-1025", 1025), ("slow/cells-4096/to-2049", 2049), ("slow/cells-4096/to-4097", 4097), ("slow/cells-4096/to-8182", 8182)] { 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:?}"); + assert!(s5000.contains(&("wave/cells-4096".to_string(), 5000)) && s5000.contains(&("slow/cells-4096/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(&("wave/cells-4096/to-513".to_string(), 513)), "{s1000:?}"); - // The wave rests where its bound lets it, and stopped short it still moves. + assert!(s1000.contains(&("wave/cells-999".to_string(), 1000)) && s1000.contains(&("slow/cells-4096/to-513".to_string(), 513)), "{s1000:?}"); + // 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, 4094).state, w.settle(&start, 4095).state, "the last sweep of a stopped wave moves a cell"); - // The battery's alike cells. - let cells = |cap: Capacity| cap.edges_of(Want::BATTERY).battery.into_iter().find(|c| c.name == "cells").map(|c| (c.landscape.len(), c.read)); + 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_ne!(w.settle(&start, 2 * n - 11).state, w.settle(&start, 2 * n - 10).state, "{units} units: its last moving sweep"); + } + // 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) in [(Capacity::NONE, 1023), (Capacity { cells: Some(4096), ..Capacity::NONE }, 4096), (Capacity { cells: Some(1 << 22), couplings: Some(1 << 22), at_zero: wide, above_zero: wide, table_entries: Some(1 << 22), ..Capacity::NONE }, 4096), (Capacity { cells: Some(2000), ..Capacity::NONE }, 2000)] { - let (got, read) = cells(cap).unwrap(); - assert_eq!(got, want, "{}", cap.json()); - assert!(read.len() <= 12 && read.contains(&0) && read.contains(&(want - 3)) && read.contains(&1022.min(want - 3)), "{read:?}"); + 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); @@ -1547,6 +1613,8 @@ mod tests { ("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; diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index 1ea45954a..12c101e1a 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -714,6 +714,8 @@ 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, @@ -770,64 +772,51 @@ pub fn run_battery(m: &dyn Member) -> Vec { run_battery_within(m, &m.capacity()) } -/// The chance that a correct sampler fails one block of a generated battery -/// workload: each block is held at the chi-square value of its degrees that -/// chance leaves, so that the workloads a capacity adds, some dozens of blocks -/// between them, fail a correct sampler, whatever its draws, less than once in -/// a million (§7.2). A wrong cell fails either. +/// 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 chance that Pearson's statistic of `df` degrees exceeds `x`, a correct -/// sampler's: the upper regularized gamma function `Q(df/2, x/2)`, in closed -/// form for whole degrees. -pub fn chi_square_tail(x: f64, df: usize) -> f64 { - if x <= 0.0 { - return 1.0; - } - let z = x / 2.0; - if df.is_multiple_of(2) { - // e^{−z} Σ_{i 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 complementary error function, to a fractional error under 1.2·10⁻⁷ -/// (Chebyshev fit of Numerical Recipes' `erfcc`). -fn erfc(x: f64) -> f64 { - let t = 1.0 / (1.0 + 0.5 * x.abs()); - let y = t * (-x * x - 1.265_512_23 + t * (1.000_023_68 + t * (0.374_091_96 + t * (0.096_784_18 + t * (-0.186_288_06 + t * (0.278_868_07 + t * (-1.135_203_98 + t * (1.488_515_87 + t * (-0.822_152_23 + t * 0.170_872_77))))))))).exp(); - if x >= 0.0 { y } else { 2.0 - y } -} - -/// The value Pearson's statistic of `df` degrees exceeds with chance `p`. -pub fn chi_square_critical(df: usize, p: f64) -> f64 { - let (mut lo, mut hi) = (0.0f64, df as f64 + 100.0 * (2.0 * df as f64).sqrt() + 200.0); - for _ in 0..200 { - let mid = 0.5 * (lo + hi); - if chi_square_tail(mid, df) > p { lo = mid } else { hi = mid } - } - hi +/// 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 per_call = cap.batch(4096); 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 and of 31 in turn (no more than the host gives), + // so that every call ends in a partial warp and a partial workgroup. + let (whole, a, b) = (cap.batch(4096), cap.batch(63), cap.batch(31)); + let b = if a == b { a.saturating_sub(1).max(1) } else { b }; battery() .into_iter() .chain(cap.edges_of(super::capacity::Want::BATTERY).battery) @@ -843,7 +832,10 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec let mut counts = vec![vec![0u64; exact.len()]; blocks.len()]; let mut ok = true; let mut at = 0u64; + let mut call = 0usize; while at < u64::from(BATTERY_CHAINS) { + let per_call = if k < fixed { whole } else if call.is_multiple_of(2) { a } else { b }; + 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) { @@ -859,13 +851,17 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec 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(); - let (chi, df, critical) = chi_square(&got, &expected, u64::from(BATTERY_CHAINS)); - (chi, df, if k < fixed { critical } else { chi_square_critical(df, GENERATED_FALSE) }) + 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; @@ -967,18 +963,39 @@ mod tests { } } - /// The tail a generated workload's critical value comes from is the closed - /// form, odd degrees and even, and the critical value leaves the chance asked. + /// 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_chi_square_tail_is_the_closed_form() { - for (x, df, want) in [(12.0, 2, 0.002_478_752_176_666_357), (62.0, 26, 8.977_207_619_300_019e-5), (15.0, 1, 1.075_111_767_295_005_4e-4), (50.0, 9, 1.077_238_202_257_469_3e-7), (30.0, 3, 1.380_057_031_293_254_7e-6), (100.0, 80, 0.064_570_368_921_131_95)] { - let got = chi_square_tail(x, df); - assert!((got / want - 1.0).abs() < 1e-5, "{x} at {df}: {got} against {want}"); - } - for (df, want) in [(1, 32.841_253), (2, 36.841_361), (8, 53.169_478), (9, 55.449_105), (26, 88.411_594), (80, 172.346_607)] { - let c = chi_square_critical(df, GENERATED_FALSE); - assert!((c - want).abs() < 1e-3, "{df}: {c} against {want}"); + 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] diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index d84f34ff9..339527df5 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -165,6 +165,12 @@ pub trait Member { } } +/// 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 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); + /// 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 @@ -173,11 +179,15 @@ pub trait Member { /// /// A store with a file reads it afresh at every certification, under a shared /// lock, and records a demotion by reading, merging and writing it under an -/// exclusive one (on the file of its name with `.lock` added), so that hosts and jobs -/// sharing it never lose one another's demotions. It fails closed: where the -/// file cannot be read, does not parse or cannot be written, 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`]). +/// exclusive one (on the file of its name with `.lock` added), so that hosts and +/// jobs sharing it never lose one another's demotions. Where the path is a link, +/// the store is its target's: its lock, its temporary files and its rename. It +/// fails closed: where the path names no file or a link to nothing, the file +/// cannot be read or does not parse, its folder does not take a file as large as +/// its next rewrite, 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. @@ -186,16 +196,28 @@ pub struct Demotions { broken: Mutex>, } +/// Why the store's lock could not be had: for good, or for now. +enum Unlocked { + Fault(String), + Busy(String), +} + impl Demotions { - /// A store kept in `path`, or in memory alone with none (for this process). + /// 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::open(None); + let d = Demotions { path: None, records: Mutex::new(BTreeMap::new()), broken: Mutex::new(None) }; d.fail(why); d } @@ -215,18 +237,55 @@ impl Demotions { why } - /// The lock beside the file, held shared or exclusive until it is dropped; - /// its folder made where it is missing. - fn lock(path: &std::path::Path, exclusive: bool) -> Result { + 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, never the link, so that + /// its lock, its temporary files and its rename are the target's. A link to + /// nothing is an error, not an empty store. + fn target(p: &std::path::Path) -> Result { + match std::fs::symlink_metadata(p) { + Ok(m) if m.file_type().is_symlink() => std::fs::canonicalize(p).map_err(|e| format!("{} is a link to nothing that can be read ({e})", p.display())), + Ok(_) => Ok(p.to_path_buf()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(p.to_path_buf()), + Err(e) => Err(format!("{} cannot be read: {e}", p.display())), + } + } + + /// A temporary file beside the store's file, named for it, this process and a + /// count, so that no two writers share one and a later writer can find it. + fn temporary(target: &std::path::Path, what: &str) -> PathBuf { + static MADE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let name = target.file_name().map_or_else(|| "demotions".into(), |n| n.to_string_lossy().into_owned()); + target.with_file_name(format!(".{name}.{what}.{}.{}.tmp", std::process::id(), MADE.fetch_add(1, std::sync::atomic::Ordering::Relaxed))) + } + + /// 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| format!("its folder {} cannot be made: {e}", dir.display()))?; + 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 = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&at).map_err(|e| format!("its lock {} cannot be opened: {e}", at.display()))?; - if exclusive { f.lock() } else { f.lock_shared() }.map_err(|e| format!("its lock {} cannot be taken: {e}", at.display()))?; - Ok(f) + let f = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&at).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's records: none where it does not exist yet. @@ -238,73 +297,109 @@ impl Demotions { } } + /// That the file's folder takes a file as large as its next rewrite: one + /// written, synced and removed, so that a folder no longer written to, a full + /// disk or a limit on a file's size is found before a member is certified. + fn takes_a_rewrite(target: &std::path::Path) -> Result<(), String> { + use std::io::Write; + let size = std::fs::metadata(target).map_or(0, |m| m.len() as usize) + 4096; + let probe = Demotions::temporary(target, "probe"); + let wrote = std::fs::File::create(&probe).and_then(|mut f| { + f.write_all(&vec![b'\n'; size])?; + f.sync_all() + }); + let _ = std::fs::remove_file(&probe); + wrote.map_err(|e| format!("the folder of {} does not take a rewrite of it: {e}", target.display())) + } + /// Why an identity was demoted, if it was, its file read afresh; 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 mut records = self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?; - if let Some(p) = &self.path { - let read = Demotions::lock(p, false).and_then(|_held| Demotions::read(p)); - match read { - Ok(on_file) => { - for (k, v) in on_file { - records.entry(k).or_insert(v); - } - } - Err(e) => return Err(self.fail(e)), + 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 on_file = Demotions::read(&t).map_err(|e| self.fail(e))?; + Demotions::takes_a_rewrite(&t).map_err(|e| self.fail(e))?; + drop(held); + on_file } + }; + 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))) } /// Remember a demotion, and where the store has a file, merge it into the /// file under its exclusive lock: written whole to a file of this process's - /// own, synced, and renamed into place. The first reason stands. An error + /// own with the file's mode, synced, and renamed into place, any temporary + /// file a writer left behind removed. The first reason stands. An error /// where it could not be kept past this process. pub fn record(&self, identity: &str, why: &str, capacity: &Capacity) -> Result<(), String> { - static WRITES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); let entry = json!({ "why": why, "capacity": capacity.json() }); - let mut records = self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?; - records.entry(identity.to_string()).or_insert_with(|| entry.clone()); + 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 write = || -> Result<(), String> { - let _held = Demotions::lock(p, true)?; - let mut on_file = Demotions::read(p)?; - on_file.entry(identity.to_string()).or_insert(entry); + let t = Demotions::target(p).map_err(|e| self.fail(e))?; + let held = Demotions::lock(&t, true).map_err(|e| self.fail(match e { + Unlocked::Fault(e) | Unlocked::Busy(e) => e, + }))?; + let mut on_file = Demotions::read(&t).map_err(|e| self.fail(e))?; + on_file.entry(identity.to_string()).or_insert(entry); + if let Ok(mut records) = self.records.lock() { for (k, v) in on_file.iter() { records.entry(k.clone()).or_insert_with(|| v.clone()); } - let text = serde_json::to_string_pretty(&on_file).map_err(|e| e.to_string())?; - let name = p.file_name().map_or_else(|| "demotions".into(), |n| n.to_string_lossy().into_owned()); - let tmp = p.with_file_name(format!(".{name}.{}.{}.tmp", std::process::id(), WRITES.fetch_add(1, std::sync::atomic::Ordering::Relaxed))); - let put = || -> std::io::Result<()> { - use std::io::Write; - let mut f = std::fs::File::create(&tmp)?; - f.write_all(text.as_bytes())?; - f.sync_all()?; - std::fs::rename(&tmp, p) - }; - put().map_err(|e| { - let _ = std::fs::remove_file(&tmp); - format!("{} cannot be written: {e}", p.display()) - }) + } + // Under the exclusive lock no other writer is mid-write: what is left is a crash's. + let name = t.file_name().map_or_else(|| "demotions".into(), |n| n.to_string_lossy().into_owned()); + if let Some(dir) = t.parent().map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d }) + && let Ok(entries) = std::fs::read_dir(dir) + { + for e in entries.flatten() { + let f = e.file_name().to_string_lossy().into_owned(); + if f.starts_with(&format!(".{name}.")) && f.ends_with(".tmp") { + let _ = std::fs::remove_file(e.path()); + } + } + } + let text = serde_json::to_string_pretty(&on_file).map_err(|e| self.fail(e.to_string()))?; + let tmp = Demotions::temporary(&t, "write"); + let put = || -> std::io::Result<()> { + use std::io::Write; + let mut f = std::fs::File::create(&tmp)?; + f.write_all(text.as_bytes())?; + if let Ok(m) = std::fs::metadata(&t) { + std::fs::set_permissions(&tmp, m.permissions())?; + } + f.sync_all()?; + std::fs::rename(&tmp, &t) }; - write().map_err(|e| self.fail(e)) + let kept = put().map_err(|e| { + let _ = std::fs::remove_file(&tmp); + self.fail(format!("{} cannot be written: {e}", t.display())) + }); + drop(held); + kept } } static DEMOTIONS: OnceLock = OnceLock::new(); -/// The host's store of demotions: the file `WAI_LANDSCAPE_DEMOTIONS` names, or +/// 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").map(PathBuf::from))) + 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 diff --git a/wai/wai-quantum/src/quantum_landscape/run.rs b/wai/wai-quantum/src/quantum_landscape/run.rs index 797e0bbd8..528f487c6 100644 --- a/wai/wai-quantum/src/quantum_landscape/run.rs +++ b/wai/wai-quantum/src/quantum_landscape/run.rs @@ -436,7 +436,7 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 // 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, across the host's cores, + // 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. @@ -514,16 +514,83 @@ 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). Each runs at +/// a low priority, so that the checks of an audited job yield to other jobs. +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; on macOS, the background band; elsewhere as it is. +fn lower_this_thread() { + #[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 now = getpriority(0, 0); + setpriority(0, 0, (now + 10).min(19)); + } + #[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 _ = getpriority; + setpriority(3, 0, 0x1000); + } +} + /// The first of a call's chains at `places` whose outcome is not the -/// reference's, each run again on the reference, side by side over the host's -/// cores (on one where it can start no thread). +/// reference's, each run again on the reference: on this thread, 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)); - let cores = std::thread::available_parallelism().map_or(1, |p| p.get()).clamp(1, 16).min(places.len().max(1)); - let parts: Vec<&[usize]> = places.chunks(places.len().div_ceil(cores).max(1)).collect(); + // 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| (part, std::thread::Builder::new().name("wai-audit".into()).spawn_scoped(s, move || wrong(part)))).collect(); + let others: Vec<_> = rest + .iter() + .map(|&part| { + let h = std::thread::Builder::new().name("wai-audit".into()).spawn_scoped(s, move || { + lower_this_thread(); + wrong(part) + }); + (part, h) + }) + .collect(); let mut found = wrong(first); for (part, h) in others { let r = match h { diff --git a/wai/wai-quantum/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index 3e89fb2f0..5b2d54031 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -441,6 +441,20 @@ enum Fault { /// 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. @@ -547,8 +561,19 @@ impl Fault { 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 (self == Fault::Final64 && in_the_tile_gap(n, i)) || (self == Fault::Final32 && in_the_warp_gap(n, i)) { + if (fires64 && in_the_tile_gap(n, i)) || (fires32 && in_the_warp_gap(n, i)) { next[i] = s[i]; } } @@ -621,11 +646,12 @@ impl Member for Fault { 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::Stale(_)) { + if matches!(self, Fault::Ring9 | Fault::Ring12 | 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::Stale(at) => u32::from(d > at), _ => 0, }; @@ -2019,3 +2045,372 @@ fn review4_the_battery_and_the_span_within_the_capacity() { ("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(); + 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: the longest moving settles the rules know are in the set: the wave +/// stopped one sweep before it would rest, and units of two cells 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), +} + +/// 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; + Ok(chains + .iter() + .enumerate() + .map(|(k, c)| { + let mut o = anneal_with(w, &c.start, temperatures, c.seed ^ 0x5EED_5EED); + let keep = |i: usize| match self.0 { + Flaw::Tail64 => in_the_tile_gap(n, i), + Flaw::CallTail32 => partial && k >= warp_start + 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 + ); + } + } +} diff --git a/wai/wai-quantum/tests/landscape_devices.rs b/wai/wai-quantum/tests/landscape_devices.rs index 8ab82aa9b..ffbf7d953 100644 --- a/wai/wai-quantum/tests/landscape_devices.rs +++ b/wai/wai-quantum/tests/landscape_devices.rs @@ -510,6 +510,7 @@ fn timings() { ("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); @@ -520,10 +521,19 @@ fn timings() { continue; } let by_name = member::named(m.as_ref()).unwrap(); - 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); + // 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()); - println!(" {} audited {audited:.3} s, unaudited {unaudited:.3} s: {:+.0}% for the audit, {:.2} of the reference's time", m.id(), 100.0 * (audited / unaudited - 1.0), audited / t_cpu); + 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); } } } -- 2.54.0 (Apple Git-157) From 4a5c1b5ccb83a7a8c0bdcedc58484c1eaa85f8d4 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 9 Nov 2026 09:01:13 -0500 Subject: [PATCH 13/15] =?UTF-8?q?wai-quantum:=20=C2=A77.1=20and=20=C2=A77.?= =?UTF-8?q?3=20give=20the=20round-5=20costs,=20generation's=20and=20the=20?= =?UTF-8?q?audit's=20wall=20time=20over=20ten=20runs=20a=20device?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- wai/extensions/quantum-landscape.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/wai/extensions/quantum-landscape.md b/wai/extensions/quantum-landscape.md index 30be5e791..3abd96c7f 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -711,14 +711,14 @@ below. 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 MOSTV generated - vectors, at most MOSTW million cell sweeps hot or settled, and at most 20 million at + 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 NONEW million cell sweeps, NONESHARE of them battery + capacity they come to about 240 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 that declares every number below what a job can reach, is about MAXW million, - MAXSHARE of them battery workloads. A test holds the bounds over the named capacities - and over random ones, and prints each one's share. + capacity of 4,096 cells that declares every other number below what a job can reach, is + about 405 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 @@ -851,7 +851,13 @@ audit costs more than its share of the work, most where a job is many short runs 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). -WALLTIME_RANGES What it catches: a fault that makes a fraction `q` +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 -- 2.54.0 (Apple Git-157) From 1efbe6653c2dab02f92f1e3171750b59e266021f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 9 Nov 2026 09:36:32 -0500 Subject: [PATCH 14/15] wai-quantum: the battery test's mutants declare 32 cells, so they meet the whole fixed battery and not 4,096 alike cells Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/tests/landscape_conformance.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wai/wai-quantum/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index 5b2d54031..0b4f084d7 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -273,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 { -- 2.54.0 (Apple Git-157) From 72829ec4caeca92bb59c6afc2bac895e4af3b290 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 9 Nov 2026 13:52:33 -0500 Subject: [PATCH 15/15] wai-quantum: the store rewrites its file at each certification as a demotion would, keeps a demotion in a file of its own, and generation reaches settles past 2^13 and late two-cycles A sixth adversarial check of the landscape capacity rule (#678) confirmed 13 findings (1 medium, 12 low). This commit fixes them and keeps each probe as a test (review6_* in landscape_conformance, and run::tests::the_audit_s_threads_are_capped_over_the_process_and_run_low). The demotion store: - Each certification rewrites the store's file as it is, the way a demotion does: a file of its own as large as the largest rewrite a demotion can make, synced, cut back and renamed over the file. A file another user owns in a sticky folder, one marked uchg or uappnd (chflags) or immutable (chattr +i), a long identity and a store written compact now grade nothing bit-exact. A reason is kept to its first 1,024 bytes (STORE_REASON_BYTES), so the rewrite's size is bounded. - A demotion is written at once to a file of its own beside the store (..demotion..json), with no lock to wait for, which every certification reads, and then merged under the exclusive lock. A lock held past 5 s leaves it in its own file until a later demotion merges it: it is kept, not dropped. - The store's temporary files are matched exactly (.....tmp), made with create_new and held locked by their writer. One unchanged for 10 s that no writer holds is removed at the next certification or demotion (STORE_STALE_AFTER), so a killed certification's probe goes, and no store removes another's file. - Links are followed link by link; a link in a world-writable sticky folder is not followed, and no file of the store's is opened through a link (O_NOFOLLOW, and the opened file checked against the path). A file with two names (a hard link) is refused. Generation: - Units of three cells that move to sweep 11u - 59 (14,956 on 4,095 cells) are stopped past 2^9 to 2^13 sweeps, each on as few units as still move there, and at their last move; units of two where couplings are scarce. The claim of a longest-moving settle is gone. Stale(8182), Stale(10000), Stale(14955) and a 13-bit counter lose the settle grade. - The same units beside an alternating pair end in a two-cycle at sweep 449, the first of a later batch of 64, and at 4,101, where 9- to 12-bit counters read the other phase. The battery: - Its added workloads run in calls of 63 down to 17 chains, every partial warp and workgroup; a call's partial group wrong at 34 to 62, or at even counts, loses statistical. - Section 7.2 states what the likelihood-ratio bound misses (a law some 10% off, a count of cells the battery does not run), a test holds each statement, and every answer from a kernel graded statistical says so (member::STATISTICAL_GRADE). The audit: - The crate docs and the change entry say the job's own thread runs its share at the job's priority, beside the low-priority audit threads; an audit thread whose priority cannot be lowered says so once. A test, run with the module's tests on Linux and in the bits job on Linux arm64 and macOS, holds the cap, setpriority's result and the priority read back, and that every thread is given back. The bits job also runs the store's tests on those platforms' file systems. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-landscape.yml | 21 +- .../2026-11-07-landscape-declared-capacity.md | 3 +- wai/extensions/quantum-landscape.md | 123 ++-- wai/wai-quantum-landscape-fpga/README.md | 15 +- wai/wai-quantum-landscape-fpga/src/main.rs | 9 +- wai/wai-quantum/docs/landscape.md | 17 +- .../src/quantum_landscape/capacity.rs | 166 +++++- .../src/quantum_landscape/conformance.rs | 11 +- .../src/quantum_landscape/member.rs | 546 +++++++++++++++--- wai/wai-quantum/src/quantum_landscape/run.rs | 138 ++++- .../tests/landscape_conformance.rs | 461 ++++++++++++++- 11 files changed, 1312 insertions(+), 198 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape.yml b/.github/workflows/wai-quantum-landscape.yml index 048de75e6..553044042 100644 --- a/.github/workflows/wai-quantum-landscape.yml +++ b/.github/workflows/wai-quantum-landscape.yml @@ -125,7 +125,7 @@ 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\. 105 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (105 expected)"; exit 1; } + 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 \ @@ -200,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 \ @@ -239,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\. 44 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (44 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 @@ -317,8 +318,22 @@ jobs: # 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; 35 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; } + 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) diff --git a/wai/changes/2026-11-07-landscape-declared-capacity.md b/wai/changes/2026-11-07-landscape-declared-capacity.md index 319a3bb03..d456905c2 100644 --- a/wai/changes/2026-11-07-landscape-declared-capacity.md +++ b/wai/changes/2026-11-07-landscape-declared-capacity.md @@ -17,4 +17,5 @@ - 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 take at most a quarter of the host's cores, 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 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 3abd96c7f..2f9f897d3 100644 --- a/wai/extensions/quantum-landscape.md +++ b/wai/extensions/quantum-landscape.md @@ -645,7 +645,7 @@ below. 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 settle can + 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 @@ -657,14 +657,29 @@ below. 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; - - the longest-moving settle the rules know: `n` cells (`C`, or as many as `P` lets) in - 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 from every cell at 0 moves a cell in every sweep to sweep `2n − 10` and rests at - `2n − 9`: stopped at its bound just past 2⁹, 2¹⁰, 2¹¹ and 2¹² sweeps, where a narrow - sweep counter wraps, and at its last moving sweep (or the most a run may have), so - that a settle ending at its bound, its last sweep moving, is held as long as the rules - know one to be; + - 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, @@ -714,10 +729,10 @@ below. 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 240 million cell sweeps, two fifths of them battery + 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 405 million, half of them battery workloads. A test holds the bounds over the + 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, @@ -750,9 +765,10 @@ 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 and of 31 chains in turn (no -more than the member takes), so that every call ends in a partial warp and workgroup. Every -block is read. A block's statistic is the likelihood ratio `G = 2 Σ O ln(O/E)` over every +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 @@ -761,9 +777,19 @@ is at most `2·(ln C(N + k − 1, k − 1) + ln(B/10⁻⁸))`, `B` the blocks th 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 from another law, 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. +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 @@ -790,10 +816,15 @@ of the job again on the reference and compares the bits: 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 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, each at a low priority (a nice value 10 higher on Linux, the background band - on macOS), so that one audited job does not crowd out another's work; + 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 @@ -818,23 +849,39 @@ places it again. The host keeps the demotion under the member's identity, its id 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 reads it afresh whenever it certifies a -member, and merges a demotion into it under a lock (the file of its name with `.lock` added), -writing it whole to a file of its own with the file's mode and renaming that into place, -so that none loses another's; a writer's file that a crash left behind it removes under -that lock. Where the path is a link, the store is the link's target: its lock, its files -and its rename. 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, where the file cannot be read or does not parse, -where its folder does not take a file as large as the file's next rewrite (written, -synced and removed at every certification, so that a folder no longer written to, a full -disk or a limit on a file's size is found before a member is graded on it), where a write -fails, or where a host that keeps one has no file to name. A demotion it made and could -not write it keeps for as long as it runs, and says so. The host waits for the lock at -most 5 s (`member::STORE_LOCK_WAIT`); past that, 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. +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 diff --git a/wai/wai-quantum-landscape-fpga/README.md b/wai/wai-quantum-landscape-fpga/README.md index 32fd2a116..d28bd5385 100644 --- a/wai/wai-quantum-landscape-fpga/README.md +++ b/wai/wai-quantum-landscape-fpga/README.md @@ -69,12 +69,15 @@ audits its answers (§7.3 of the extension) and demotes it at the first that dif 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, and merges a demotion into it under the file's own lock, -waited for at most 5 s, so jobs and hosts sharing it keep one another's; a link is followed to its -target. Where the file cannot be read, parsed or written, its folder does not take a rewrite of it -(a full disk, a read-only folder), 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`. +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`. **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 diff --git a/wai/wai-quantum-landscape-fpga/src/main.rs b/wai/wai-quantum-landscape-fpga/src/main.rs index bfb357c57..61b6d00f3 100644 --- a/wai/wai-quantum-landscape-fpga/src/main.rs +++ b/wai/wai-quantum-landscape-fpga/src/main.rs @@ -299,10 +299,11 @@ 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, and - // merged under a lock of its own, waited for at most `member::STORE_LOCK_WAIT` while - // the board is held; with no file to name (an empty variable names none), no kernel - // is graded bit-exact. + // 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()) { diff --git a/wai/wai-quantum/docs/landscape.md b/wai/wai-quantum/docs/landscape.md index a284aa100..999ba5585 100644 --- a/wai/wai-quantum/docs/landscape.md +++ b/wai/wai-quantum/docs/landscape.md @@ -248,17 +248,22 @@ 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 at most a quarter of the host's cores -(4 at most, over the whole process) at a low priority; and the +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, read at every certification with a check that -its folder takes a rewrite, and merged under a lock waited for at most 5 s), so certifying again -does not clear it; where that file cannot be read or written, no kernel is graded `bit-exact`. A kernel graded `statistical` is placed only where a job asks for it -(`"parity": "statistical"`). +`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}; diff --git a/wai/wai-quantum/src/quantum_landscape/capacity.rs b/wai/wai-quantum/src/quantum_landscape/capacity.rs index 2088290c6..f24ed9ee8 100644 --- a/wai/wai-quantum/src/quantum_landscape/capacity.rs +++ b/wai/wai-quantum/src/quantum_landscape/capacity.rs @@ -532,10 +532,12 @@ fn alike3(name: &'static str, f: i64, c: i64, t: f64) -> conformance::BatteryCas 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`, and rests at `2n − 9`. +/// 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 { @@ -549,6 +551,60 @@ fn slow(units: usize) -> Landscape { 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(); @@ -900,7 +956,7 @@ impl Capacity { } } // A wave settling down as many cells as a workload can hold, a cell a sweep: - // a settle as long as a settle within the capacity can be, coming to rest + // 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. @@ -913,23 +969,46 @@ impl Capacity { let b = (n as u32).min(r.sweeps); o.settle(format!("wave/cells-{n}/to-{b}"), &l, &l.lower(), b); } - // The longest-moving settle the rules know: units of two cells that move - // for 2n − 10 sweeps from every cell at 0, twice a wave's; stopped at its - // bound while it still moves, just past 2⁹, 2¹⁰, 2¹¹ and 2¹² sweeps, where a - // narrow sweep counter wraps, and at its last moving sweep, so that a settle - // ending at its bound is held as long as the rules know one to be. + // 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 units = (r.cells / 2).min((r.pairs + 2) / 3); - if units >= 8 { - let l = slow(units); - let w = l.lower(); - let last = (4 * units as u32 - 10).min(r.sweeps); - let mut bounds = vec![513u32, 1025, 2049, 4097, last]; - bounds.retain(|&b| b <= last); - bounds.dedup(); - for b in bounds { - o.settle(format!("slow/cells-{}/to-{b}", 2 * units), &l, &w, b); + 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 @@ -1501,13 +1580,27 @@ mod tests { } 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), ("slow/cells-4096/to-513", 513), ("slow/cells-4096/to-1025", 1025), ("slow/cells-4096/to-2049", 2049), ("slow/cells-4096/to-4097", 4097), ("slow/cells-4096/to-8182", 8182)] { + 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(&("slow/cells-4096/to-5000".to_string(), 5000)), "{s5000:?}"); + 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(&("slow/cells-4096/to-513".to_string(), 513)), "{s1000:?}"); + 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); @@ -1523,8 +1616,35 @@ mod tests { 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 }; diff --git a/wai/wai-quantum/src/quantum_landscape/conformance.rs b/wai/wai-quantum/src/quantum_landscape/conformance.rs index 12c101e1a..03f851eaa 100644 --- a/wai/wai-quantum/src/quantum_landscape/conformance.rs +++ b/wai/wai-quantum/src/quantum_landscape/conformance.rs @@ -813,10 +813,11 @@ pub fn likelihood_ratio_critical(bins: usize, total: u64, p: f64) -> f64 { 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 and of 31 in turn (no more than the host gives), - // so that every call ends in a partial warp and a partial workgroup. - let (whole, a, b) = (cap.batch(4096), cap.batch(63), cap.batch(31)); - let b = if a == b { a.saturating_sub(1).max(1) } else { b }; + // 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) @@ -834,7 +835,7 @@ pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec let mut at = 0u64; let mut call = 0usize; while at < u64::from(BATTERY_CHAINS) { - let per_call = if k < fixed { whole } else if call.is_multiple_of(2) { a } else { b }; + 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); diff --git a/wai/wai-quantum/src/quantum_landscape/member.rs b/wai/wai-quantum/src/quantum_landscape/member.rs index 339527df5..04fdfeeb2 100644 --- a/wai/wai-quantum/src/quantum_landscape/member.rs +++ b/wai/wai-quantum/src/quantum_landscape/member.rs @@ -167,27 +167,56 @@ pub trait Member { /// 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 board job waits no +/// 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 reads it afresh at every certification, under a shared -/// lock, and records a demotion by reading, merging and writing it under an -/// exclusive one (on the file of its name with `.lock` added), so that hosts and -/// jobs sharing it never lose one another's demotions. Where the path is a link, -/// the store is its target's: its lock, its temporary files and its rename. It -/// fails closed: where the path names no file or a link to nothing, the file -/// cannot be read or does not parse, its folder does not take a file as large as -/// its next rewrite, 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. +/// 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. @@ -202,6 +231,129 @@ enum Unlocked { 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. @@ -247,24 +399,59 @@ impl Demotions { } } - /// The file the store's path names: a link's target, never the link, so that - /// its lock, its temporary files and its rename are the target's. A link to - /// nothing is an error, not an empty store. + /// 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 { - match std::fs::symlink_metadata(p) { - Ok(m) if m.file_type().is_symlink() => std::fs::canonicalize(p).map_err(|e| format!("{} is a link to nothing that can be read ({e})", p.display())), - Ok(_) => Ok(p.to_path_buf()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(p.to_path_buf()), - Err(e) => Err(format!("{} cannot be read: {e}", p.display())), + 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())) } - /// A temporary file beside the store's file, named for it, this process and a - /// count, so that no two writers share one and a later writer can find it. - fn temporary(target: &std::path::Path, what: &str) -> PathBuf { - static MADE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + /// 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()); - target.with_file_name(format!(".{name}.{what}.{}.{}.tmp", std::process::id(), MADE.fetch_add(1, std::sync::atomic::Ordering::Relaxed))) + (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 @@ -276,7 +463,7 @@ impl Demotions { let mut name = path.as_os_str().to_owned(); name.push(".lock"); let at = PathBuf::from(name); - let f = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&at).map_err(|e| Unlocked::Fault(format!("its lock {} cannot be opened: {e}", at.display())))?; + 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() } { @@ -288,32 +475,127 @@ impl Demotions { } } - /// The file's records: none where it does not exist yet. - fn read(path: &std::path::Path) -> Result, String> { - match std::fs::read(path) { - Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| format!("{} does not parse as demotions: {e}", path.display())), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(BTreeMap::new()), - Err(e) => Err(format!("{} cannot be read: {e}", path.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); + } } } - /// That the file's folder takes a file as large as its next rewrite: one - /// written, synced and removed, so that a folder no longer written to, a full - /// disk or a limit on a file's size is found before a member is certified. - fn takes_a_rewrite(target: &std::path::Path) -> Result<(), String> { - use std::io::Write; - let size = std::fs::metadata(target).map_or(0, |m| m.len() as usize) + 4096; - let probe = Demotions::temporary(target, "probe"); - let wrote = std::fs::File::create(&probe).and_then(|mut f| { - f.write_all(&vec![b'\n'; size])?; - f.sync_all() - }); - let _ = std::fs::remove_file(&probe); - wrote.map_err(|e| format!("the folder of {} does not take a rewrite of it: {e}", target.display())) + /// 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 } - /// Why an identity was demoted, if it was, its file read afresh; an error - /// where the store is broken, and then nothing it holds can be trusted to be all. + /// 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); @@ -323,10 +605,17 @@ impl Demotions { 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 on_file = Demotions::read(&t).map_err(|e| self.fail(e))?; - Demotions::takes_a_rewrite(&t).map_err(|e| self.fail(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); - on_file + all } }; let mut records = self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?; @@ -336,59 +625,116 @@ impl Demotions { Ok(records.get(identity).map(|r| r["why"].as_str().map_or_else(|| r.to_string(), String::from))) } - /// Remember a demotion, and where the store has a file, merge it into the - /// file under its exclusive lock: written whole to a file of this process's - /// own with the file's mode, synced, and renamed into place, any temporary - /// file a writer left behind removed. The first reason stands. An error - /// where it could not be kept past this process. + /// 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": why, "capacity": capacity.json() }); + 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 held = Demotions::lock(&t, true).map_err(|e| self.fail(match e { - Unlocked::Fault(e) | Unlocked::Busy(e) => e, - }))?; - let mut on_file = Demotions::read(&t).map_err(|e| self.fail(e))?; - on_file.entry(identity.to_string()).or_insert(entry); - if let Ok(mut records) = self.records.lock() { - for (k, v) in on_file.iter() { - records.entry(k.clone()).or_insert_with(|| v.clone()); + 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)) } - } - // Under the exclusive lock no other writer is mid-write: what is left is a crash's. - let name = t.file_name().map_or_else(|| "demotions".into(), |n| n.to_string_lossy().into_owned()); - if let Some(dir) = t.parent().map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d }) - && let Ok(entries) = std::fs::read_dir(dir) - { - for e in entries.flatten() { - let f = e.file_name().to_string_lossy().into_owned(); - if f.starts_with(&format!(".{name}.")) && f.ends_with(".tmp") { - let _ = std::fs::remove_file(e.path()); - } - } - } - let text = serde_json::to_string_pretty(&on_file).map_err(|e| self.fail(e.to_string()))?; - let tmp = Demotions::temporary(&t, "write"); - let put = || -> std::io::Result<()> { - use std::io::Write; - let mut f = std::fs::File::create(&tmp)?; - f.write_all(text.as_bytes())?; - if let Ok(m) = std::fs::metadata(&t) { - std::fs::set_permissions(&tmp, m.permissions())?; - } - f.sync_all()?; - std::fs::rename(&tmp, &t) + Err(Unlocked::Busy(e)) => Err((e, true)), + Err(Unlocked::Fault(e)) => Err((self.fail(e.clone()), false)), }; - let kept = put().map_err(|e| { - let _ = std::fs::remove_file(&tmp); - self.fail(format!("{} cannot be written: {e}", t.display())) - }); - drop(held); - kept + 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}"))), + } } } @@ -444,6 +790,10 @@ 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, @@ -519,13 +869,17 @@ impl<'a> Certified<'a> { self.capacity } - /// What every answer from it says of itself: its own label, and the - /// capacity its parity holds within where it declares one. + /// 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}."); } diff --git a/wai/wai-quantum/src/quantum_landscape/run.rs b/wai/wai-quantum/src/quantum_landscape/run.rs index 528f487c6..7896db086 100644 --- a/wai/wai-quantum/src/quantum_landscape/run.rs +++ b/wai/wai-quantum/src/quantum_landscape/run.rs @@ -515,8 +515,10 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64 } /// 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). Each runs at -/// a low priority, so that the checks of an audited job yield to other jobs. +/// 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) } @@ -547,27 +549,78 @@ unsafe extern "C" { } /// This thread at a low priority: on Linux, where a nice value is a thread's, 10 -/// more; on macOS, the background band; elsewhere as it is. -fn lower_this_thread() { +/// 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 now = getpriority(0, 0); - setpriority(0, 0, (now + 10).min(19)); + 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 _ = getpriority; - setpriority(3, 0, 0x1000); + 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, and on as many -/// of the audit's threads as are free ([`audit_threads`]), each at a low priority. +/// 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. @@ -585,7 +638,10 @@ fn differs(w: &Workload, temperatures: &[f64], chains: &[Chain], outcomes: &[Out .iter() .map(|&part| { let h = std::thread::Builder::new().name("wai-audit".into()).spawn_scoped(s, move || { - lower_this_thread(); + let lowered = lower_this_thread(); + lowered_or_said(&lowered); + #[cfg(test)] + let _running = census::enter(&lowered); wrong(part) }); (part, h) @@ -1204,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/tests/landscape_conformance.rs b/wai/wai-quantum/tests/landscape_conformance.rs index 0b4f084d7..5b704916a 100644 --- a/wai/wai-quantum/tests/landscape_conformance.rs +++ b/wai/wai-quantum/tests/landscape_conformance.rs @@ -465,6 +465,18 @@ enum Fault { 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. @@ -652,17 +664,28 @@ impl Member for Fault { 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::Stale(_)) { + 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 }); @@ -674,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 }); @@ -2148,6 +2176,8 @@ fn review5_the_store_follows_links_waits_a_bounded_time_and_cleans_up() { 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"); @@ -2171,9 +2201,9 @@ fn review5_the_store_follows_links_waits_a_bounded_time_and_cleans_up() { let _ = std::fs::remove_dir_all(&dir); } -/// 2, 11: the longest moving settles the rules know are in the set: the wave -/// stopped one sweep before it would rest, and units of two cells stopped past -/// 2⁹ to 2¹² sweeps and at their last moving sweep. +/// 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 }; @@ -2236,6 +2266,14 @@ enum Flaw { 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. @@ -2258,14 +2296,23 @@ impl Member for Flawed { 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, @@ -2420,3 +2467,407 @@ fn review5_an_audited_replay_beside_an_honest_job() { } } } + +// ── 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"]); +} -- 2.54.0 (Apple Git-157)