wai-quantum: a landscape member declares its capacity, and is held to everything within it #678
26 changed files with 7984 additions and 295 deletions
|
|
@ -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
|
||||
|
|
|
|||
41
.github/workflows/wai-quantum-landscape.yml
vendored
41
.github/workflows/wai-quantum-landscape.yml
vendored
|
|
@ -125,8 +125,18 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
cargo test --locked --release --features full --lib -- quantum_landscape:: 2>&1 | tee "$RUNNER_TEMP/landscape.log"
|
||||
grep -qE '^test result: ok\. 95 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (95 expected)"; exit 1; }
|
||||
for t in quantum_landscape::classical::tests::a_solver_keeps_to_its_budget_and_repeats_itself \
|
||||
grep -qE '^test result: ok\. 106 passed; 0 failed' "$RUNNER_TEMP/landscape.log" || { echo "::error::the landscape tests did not all pass (106 expected)"; exit 1; }
|
||||
for t in quantum_landscape::capacity::tests::a_capacity_declines_exactly_what_is_past_it \
|
||||
quantum_landscape::capacity::tests::a_capacity_is_stated_in_numbers_and_in_words \
|
||||
quantum_landscape::capacity::tests::the_floor_is_the_core_set_s_own_reach \
|
||||
quantum_landscape::capacity::tests::every_declared_number_has_vectors_at_its_edge \
|
||||
quantum_landscape::capacity::tests::a_table_and_a_span_are_reached_exactly \
|
||||
quantum_landscape::capacity::tests::generated_vectors_cost_what_the_spec_says \
|
||||
quantum_landscape::capacity::tests::with_no_capacity_the_generated_set_reaches_what_a_job_can \
|
||||
quantum_landscape::capacity::tests::long_settles_bands_cells_and_spans_are_generated \
|
||||
quantum_landscape::conformance::tests::the_likelihood_ratio_bound_holds_exactly \
|
||||
quantum_landscape::capacity::tests::the_hottest_sweep_has_the_widest_uncertain_span \
|
||||
quantum_landscape::classical::tests::a_solver_keeps_to_its_budget_and_repeats_itself \
|
||||
quantum_landscape::classical::tests::each_solver_finds_the_exact_lowest_on_small_instances_with_a_generous_budget \
|
||||
quantum_landscape::classical::tests::with_no_budget_a_solver_misses \
|
||||
quantum_landscape::conformance::tests::a_state_s_index_is_its_digits \
|
||||
|
|
@ -143,7 +153,7 @@ jobs:
|
|||
quantum_landscape::job::tests::a_member_that_fails_a_job_passes_it_on \
|
||||
quantum_landscape::job::tests::an_uncertified_kernel_is_never_placed \
|
||||
quantum_landscape::job::tests::every_answer_says_who_ran_it_at_what_parity \
|
||||
quantum_landscape::job::tests::a_member_may_decline_only_what_does_not_fit_32_bits \
|
||||
quantum_landscape::job::tests::a_member_is_held_to_every_vector_within_its_declared_capacity \
|
||||
quantum_landscape::job::tests::a_sealed_answer_verifies_and_follows_its_schema \
|
||||
quantum_landscape::job::tests::a_seed_keeps_all_64_bits \
|
||||
quantum_landscape::job::tests::a_temperature_in_text_keeps_every_bit \
|
||||
|
|
@ -190,6 +200,7 @@ jobs:
|
|||
quantum_landscape::run::tests::a_member_s_claim_of_parity_is_not_taken_on_its_word \
|
||||
quantum_landscape::run::tests::a_run_s_reading_is_its_calls_added_up \
|
||||
quantum_landscape::run::tests::a_run_too_big_is_refused_before_it_starts \
|
||||
quantum_landscape::run::tests::the_audit_s_threads_are_capped_over_the_process_and_run_low \
|
||||
quantum_landscape::run::tests::a_sweep_keeps_the_budget_and_the_cold_row_of_a_minimize_runs_once \
|
||||
quantum_landscape::run::tests::the_same_seed_gives_the_same_answer_and_another_seed_another_run \
|
||||
quantum_landscape::run::tests::the_temperature_picks_the_mode_and_the_rate \
|
||||
|
|
@ -229,7 +240,7 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
cargo test --locked --release --features quantum_landscape --test landscape_conformance 2>&1 | tee "$RUNNER_TEMP/conformance.log"
|
||||
grep -qE '^test result: ok\. 12 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (12 expected)"; exit 1; }
|
||||
grep -qE '^test result: ok\. 54 passed; 0 failed' "$RUNNER_TEMP/conformance.log" || { echo "::error::the conformance tests did not all pass (54 expected)"; exit 1; }
|
||||
# With the module's feature alone: a temperature given as a JSON number is read to the nearest double.
|
||||
- name: Numbers read to the nearest double
|
||||
shell: bash
|
||||
|
|
@ -304,9 +315,25 @@ jobs:
|
|||
quantum_landscape::job::tests::every_platform_gives_the_same_bits \
|
||||
quantum_landscape::settle::tests::four_recall_vectors_are_pinned 2>&1 | tee "$RUNNER_TEMP/bits.log"
|
||||
grep -qE '^test result: ok\. 2 passed; 0 failed' "$RUNNER_TEMP/bits.log" || { echo "::error::the pinned bits differ here ($(uname -sm), ${profile:-debug})"; exit 1; }
|
||||
nice -n 10 cargo test --locked $profile --no-default-features --features quantum_landscape --test landscape_conformance 2>&1 | tee "$RUNNER_TEMP/vectors.log"
|
||||
grep -qE '^test result: ok\. 12 passed; 0 failed' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; }
|
||||
# The reviews' probes, the audit's tests and the battery's grading test certification,
|
||||
# the audit and the battery, not the bits, and run in the conformance job.
|
||||
nice -n 10 cargo test --locked $profile --no-default-features --features quantum_landscape --test landscape_conformance -- --skip review --skip audit_ --skip the_battery_tells --skip a_member_is_graded 2>&1 | tee "$RUNNER_TEMP/vectors.log"
|
||||
grep -qE '^test result: ok\. 10 passed; 0 failed; 0 ignored; 0 measured; 45 filtered out' "$RUNNER_TEMP/vectors.log" || { echo "::error::the conformance set differs here ($(uname -sm), ${profile:-debug})"; exit 1; }
|
||||
done
|
||||
# The store of demotions and the audit's threads rest on this platform's file system and
|
||||
# scheduler (chflags, sticky folders, flock, setpriority): their tests run here too.
|
||||
- name: The store of demotions and the audit's threads on this platform
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
nice -n 10 cargo test --locked --release --no-default-features --features quantum_landscape --lib -- \
|
||||
quantum_landscape::run::tests::the_audit_s_threads_are_capped_over_the_process_and_run_low 2>&1 | tee "$RUNNER_TEMP/threads.log"
|
||||
grep -qE '^test result: ok\. 1 passed; 0 failed' "$RUNNER_TEMP/threads.log" || { echo "::error::the audit's threads are not held here ($(uname -sm))"; exit 1; }
|
||||
nice -n 10 cargo test --locked --release --no-default-features --features quantum_landscape --test landscape_conformance -- \
|
||||
review4_the_store review4_stores review5_the_store review6_the_store review6_a_demotion review6_a_store review6_the_probe review6_a_link review6_a_hard review6_a_killed 2>&1 | tee "$RUNNER_TEMP/store.log"
|
||||
grep -qE '^test result: ok\. 11 passed; 0 failed' "$RUNNER_TEMP/store.log" || { echo "::error::the store of demotions is not held here ($(uname -sm))"; exit 1; }
|
||||
|
||||
wasip2:
|
||||
name: the module under wasm32-wasip2 (wasmtime)
|
||||
|
|
@ -354,7 +381,7 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
cargo test --locked --target wasm32-wasip2 --no-default-features --features quantum_engine,quantum_receipt --lib -- quantum_landscape:: 2>&1 | tee "$RUNNER_TEMP/wasip2.log"
|
||||
grep -qE '^test result: ok\. 94 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (94 expected)"; exit 1; }
|
||||
grep -qE '^test result: ok\. 104 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the landscape tests did not all pass under wasip2 (104 expected)"; exit 1; }
|
||||
|
||||
# The browser build's answers against the command line's, byte for byte, in Node.
|
||||
parity:
|
||||
|
|
|
|||
21
wai/changes/2026-11-07-landscape-declared-capacity.md
Normal file
21
wai/changes/2026-11-07-landscape-declared-capacity.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
### wai-quantum: a member of an energy-landscape job declares its capacity, and is held to everything within it
|
||||
|
||||
- **A declared capacity** (`extensions/quantum-landscape.md` §5; `quantum_landscape::capacity`): a member states once, as fixed numbers, what its kernels hold: cells and couplings; on each side of temperature 0, its largest field, field bound and cost; above 0, the cut points a sweep or the span of uncertain fields it holds; the sweeps of a run; and the chains it takes a call, which is a batch, not a limit: the host splits a job, and certification splits every vector, into calls of that many. The host declines for it exactly the jobs past those numbers, by a function of the job and the numbers alone, so no member declines by choice. `Member::capacity` takes the place of `Member::takes`.
|
||||
- **Graded within it, and at its edges.** `member::certify` reads the capacity once, and runs every vector and battery workload within it, and, for each number declared below what a job can reach, vectors generated at exactly that number (`Capacity::edges`, the reference their oracle): so a limit set between the set's fixed points is held at the limit, not at the nearest vector under it. The grade holds within the capacity: `member.capacity` in every answer from a member that declares one (none from the reference, whose answers keep their bytes), a sentence in the label, and the number a job is past in its refusal. A member that refuses or fails a case within its capacity loses the grade.
|
||||
- **A floor.** A capacity under the least one that takes every case of the core set (32 cells, fields to ±40, a field bound of 41, a cost of 8, a run of 5,000 sweeps, and the table and span those cases need) is not certified at all. The core set holds a long schedule, 5,000 sweeps on 32 cells within the floor, so that no capacity a member may declare leaves it untested across a device's windows; the full set adds 200,000 sweeps of two cells, across a window by work.
|
||||
- The GPU members declare their real limits (WebGPU a field bound of 2³¹ − 1 on both sides of temperature 0); the CPU reference declares none. The FPGA reference design declares 64 cells and, above temperature 0, fields to ±524,287, a cost to 1,048,575 and an uncertain span of 8,192, and the host certifies its register-level model `bit-exact` within that capacity on the set as it stands; a test holds the declared numbers to the engine's own limits case by case.
|
||||
- An adversarial check found five ways to be certified `bit-exact` past what the vectors had tested: a window fault at the floor itself, a batch size that skipped vectors, limits set between the vectors' points, a capacity read more than once, and grade text without the capacity. Each of its probes is kept as a test, and every fault test also runs declaring the most hostile capacity it may: the least, at or above the floor, that still takes a job the fault gets wrong.
|
||||
- A second adversarial check found ten more fault classes that a finite set missed: cells past a couplings limit, the last sweep of an annealing job at 0 on large fields, workgroup and warp remainders, rows longer than 32, a field and a cost past a lane together, fields at a lane's wrap points, draw indices past 24 bits and past a signed 32-bit index, tables and spans short of the declared number, and nothing past 1,000 cells with no capacity at all. So certification now generates vectors by rule for every capacity (`Capacity::generate`, §7.1): every cell count at and beside a power of two to the most a workload can hold, 4,096 with none; every call size to 32 chains, each band of a wavefront and a whole batch; the longest row; fields at the wrap points of 16, 24 and 32-bit lanes, alone, through couplings and beside the largest cost, at 0, above it and ending at 0; a corner with every number at its most; and a run past draw index 2²⁴; a narrower index past that is left to the audit, as a run past 2³¹ would cost minutes on a device. The edges reach a declared table and span exactly, and the battery gets workloads at a declared cell count, couplings and schedule length, reading a few blocks of many (`BatteryCase::read`). The cost is bounded and stated in §7.1.
|
||||
- **The host's audit** (§7.3): every answer from a kernel graded `bit-exact` has the last chain of each call and one chain of each run in it, drawn with a key of the host's own, run again on the reference, and every settle within a work budget (one in 8 past it). At the first difference the member is demoted to `uncertified` for both kernels, the job is answered from the reference, and later answers say so (`member.demoted`).
|
||||
- The reference's sampler skips a run of sweeps at 0 after a fixed point, as they cannot move it; its bits are unchanged.
|
||||
- A decline for capacity now reads "The host declined this job for …", not "the member refused or failed", and the label says the host runs the member on no job past its capacity; a member run by name without certification claims no grade.
|
||||
- **Breaking:** `api:rust:wai_quantum::quantum_landscape::member::Member::takes`: removed; a member declares a capacity instead (`Member::capacity`).
|
||||
- **Breaking:** `api:rust:wai_quantum::quantum_landscape::conformance::MUST_TAKE_FIELD_BOUND`: removed; what a member must take is its declared capacity, at or above the floor.
|
||||
- **Breaking:** `api:rust:wai_quantum::quantum_landscape::job::Prepared::sampling`: removed; `Prepared::schedules` and `Prepared::settles` hold what the job runs.
|
||||
- **Breaking:** `api:rust:wai_quantum::quantum_landscape::conformance::BatteryCase::read`: a new field, the blocks a battery workload counts; empty counts every block, as before.
|
||||
- **Breaking:** `api:rust:impl core::marker::Freeze for wai_quantum::quantum_landscape::member::Certified`: a certified member now holds the audit's demotion, so it is no longer `Freeze`.
|
||||
- A third adversarial check found the audit resting on what a member reports or can predict, and gaps in generation. Now: the audit runs one chain of every seed's run, as well as the last of every call, in replay, sweep and compare; it decides a settle's re-run from the work the host asked for, never the sweeps the member reports, and compares the reference's settle as far as the budget goes; it draws with a host key made at start and a count every draw moves on, and each audited answer records what it checked (`audit`). A demotion is kept by identity (`Member::fingerprint`, `member::Demotions`), in a file where the host names one (the FPGA host does), and certifying again or declaring another capacity does not clear it. A kernel graded `statistical`, which the audit cannot hold to bits, is placed only where the job asks for it (`"parity": "statistical"`). Generation: lane vectors are packed to what the capacity takes; ladder sizes in a remainder band are quenched too; a wave settles down as many cells as a workload holds for every capacity; the corner moves cells at 0 and reaches the field bound through as many couplings as a cell can have; the battery's workload of alike cells holds a declared cell count exactly. The bound on what generation costs holds over random capacities, and the text states the bound the test holds.
|
||||
- The CUDA kernel's mark on a field past its table can be overwritten by a later sweep; the vectors and the audit catch such an answer, and the text no longer says the mark does.
|
||||
- A fourth adversarial check found faults past the generated set and the audit. Now: ladder sizes in a remainder band are also run hot and then at 0 for one sweep, an engine's final sweep; the wave comes to rest within a declared sweep limit instead of being dropped, and the widest wave is also stopped at its bound while it still moves, past 2⁹, 2¹⁰ and 2¹¹ sweeps and one sweep before it would rest; the battery's alike cells follow a declared cell count to 4,096; the uncertain span is reached where a table limit is the narrower, and where no span is declared; and the workloads a capacity adds to the battery hold each block at a chance of 10⁻⁸ of failing a correct sampler, and read few blocks (§7.2). The audit draws one chain of each run in every call, so a member that times the host learns nothing of the next call, and runs its checks across the host's cores. An audited answer no longer carries a key digest (`audit.key`), which nothing could open. The store of demotions reads its file at each certification and merges each demotion under a lock, so hosts and jobs sharing it keep one another's; it fails closed, grading no kernel `bit-exact`, where the file cannot be read, parsed or written, or the FPGA host has no `HOME` (`Demotions::get` and `record` return what failed; `Demotions::broken`, `keep_demotions`). `"parity": null` is the default, as for every optional field.
|
||||
- A fifth adversarial check found the store failing open where its folder stopped taking writes after first use, and gaps in generation and the battery. Now: each certification writes, syncs and removes a file as large as the store's next rewrite, so a folder no longer written to, a full disk or a file-size limit grades nothing `bit-exact`; an empty `WAI_LANDSCAPE_DEMOTIONS` is as if unset and a path naming no file is a broken store; a linked store is its target's, and a link to nothing fails closed; the store's lock is waited for at most 5 s (`member::STORE_LOCK_WAIT`); a rewrite keeps the file's mode and removes files a crashed writer left. The wave is stopped one sweep before its rest, and a settle of two-cell units that moves for `2n − 10` sweeps is stopped past 2⁹ to 2¹² sweeps and at its last moving sweep. The final sweep at 0 after sweeps above 0 runs at odd, even and middle remainders of a partial warp and workgroup, after two, three and a cold sweep above 0. The battery's alike cells reach every cell a workload can hold (4,096 with none), every cell read, with a second count at a partial group's most; the workloads a capacity adds run in calls ending in a partial warp, and are held by a likelihood-ratio bound that holds whatever the law (`conformance::likelihood_ratio`, `likelihood_ratio_critical`). The audit's re-runs run on the job's own thread and on at most a quarter of the host's cores more, 4 at most over the process, at a low priority (`run::audit_threads`), and §7.3 states the audit's cost as work, with wall time measured over five runs a device.
|
||||
- A sixth adversarial check found the store's probe proving less than a demotion needs, and gaps in generation and the battery's stated power. Now: each certification rewrites the store's file as a demotion does, as large as the largest rewrite one can make (a reason is kept to its first 1,024 bytes, `member::STORE_REASON_BYTES`), and renames it over the file, so a file another user owns in a sticky folder, one marked immutable or append-only, a store written compact and a long identity grade nothing `bit-exact`; a demotion is kept at once in a file of its own beside the store, which every certification reads, and is merged under the lock, so a busy lock no longer drops it; the store's temporary files are matched exactly and removed only once unchanged for 10 s and held by no writer (`member::STORE_STALE_AFTER`), so a certification removes what a killed one left and no store removes another's; a link in a sticky folder anyone may write is not followed, no file of the store's is opened through a link, and a file with two names is refused. Generation stops units of three cells, which move to sweep 14,956 on 4,095 cells, past 2⁹ to 2¹³ sweeps and at their last move (units of two where couplings are scarce); the claim of a longest-moving settle is gone; two-cycles end at sweep 449, the first of a later batch, and 4,101. The battery's added workloads run in calls ending in every partial warp and workgroup; §7.2 states what the likelihood-ratio bound misses (a law some 10% off, a count of cells the battery does not run), and every answer from a kernel graded `statistical` says so (`member::STATISTICAL_GRADE`). The audit's docs say the job's own thread runs its share at the job's priority, an audit thread that cannot be lowered says so, and a test run on Linux and macOS holds the threads' number and priority.
|
||||
|
|
@ -55,8 +55,11 @@ A job is one JSON request and one operation. The request:
|
|||
| `temperature` | optional | the starting temperature, over the program's own; 0 is the deterministic mode. A JSON number, which a reader MUST read to the nearest double, or a string `[0-9]+(.[0-9]+)?` of at most 40 characters, read to the nearest double: no sign, no exponent, no other form |
|
||||
| `seeds` | `sweep`, `compare`, `replay` | how many seeds, from `seed` up, 1 to 100,000; default 20 |
|
||||
| `temperatures` | `sweep` | 1 to 64 of the same; by default 0, 0.25, 0.5, 1, 2, 4 and 8, with the default start (§4.3) and the largest row of couplings added where they are not already there, in increasing order |
|
||||
| `parity` | optional, default `bit-exact` | `bit-exact`, or `statistical`, which lets a placing host give the job to a kernel graded `statistical` too (§5.1) |
|
||||
| `protocol` | optional | if given, it MUST be this protocol's name |
|
||||
|
||||
An optional field given as `null` is absent.
|
||||
|
||||
The operations are `run`, `sweep` (the program at each temperature, with the
|
||||
same budget at each), `compare` (§6), `replay` (many seeds, one line each) and
|
||||
`examples`.
|
||||
|
|
@ -181,7 +184,7 @@ A `run` answers with these fields (the schema has them all):
|
|||
|---|---|
|
||||
| `protocol` | `wai.quantum.landscape/1` |
|
||||
| `label` | what kind of run this is, in the member's own words (§5.3) |
|
||||
| `member` | what ran it: `id`, `class`, `kernel` and `parity` (§5) |
|
||||
| `member` | what ran it: `id`, `class`, `kernel`, `parity`, the `capacity` that parity holds within, absent where none is declared (§5), and `demoted`, why the host holds it at `uncertified`, where it does (§7.3) |
|
||||
| `program_digest` | BLAKE3 of the program's bytes |
|
||||
| `seed`, `mode`, `temperature`, `temperature_from`, `restarts`, `sweeps` | how it ran; `seed` is a decimal string; `mode` is `settle`, `quench` or `sample` |
|
||||
| `state`, `energy` | the state of lowest energy found, and its energy from the program |
|
||||
|
|
@ -195,12 +198,15 @@ A `run` answers with these fields (the schema has them all):
|
|||
| `joules` | §4.4 |
|
||||
| `state_hash`, `replay` | §3.2 |
|
||||
| `refusals` | each member that a placing host passed over, with why (§5); absent where there was none |
|
||||
| `audit` | what the host's audit ran again on the reference, for an answer from a kernel graded `bit-exact`: `checked`, each run's `seed` with the `chains` of it checked, or how much of a `settle` (§7.3); absent where nothing was audited |
|
||||
|
||||
A `replay` answers with `protocol`, `label`, `member` (`id`, `class`, `kernel`,
|
||||
`parity`), `seeds` and `lines`. A `sweep` and a `compare` give `member` as `id`,
|
||||
`class` and `kernels`, each kernel the job used with the parity the host found for it;
|
||||
`parity`, `capacity`, `demoted`), `seeds` and `lines`. A `sweep` and a `compare` give
|
||||
`member` as `id`, `class`, `capacity`, `demoted` and `kernels`, each kernel the job used
|
||||
with the parity the host found for it;
|
||||
each row of a `sweep` gives its own `kernel` and `parity`, and each of a `compare` the
|
||||
member's `parity`, null for a classical method. Each carries `refusals` as a `run` does.
|
||||
member's `parity`, null for a classical method. Each carries `refusals` and `audit` as a
|
||||
`run` does.
|
||||
|
||||
An accumulate is one read of one coupling into one cell's field, and is the unit
|
||||
of work. The accumulates of a sweep are the non-zero entries of `W`, counting each
|
||||
|
|
@ -399,17 +405,65 @@ batch of chains at a time (§2.0).
|
|||
What a member gives back, for a settle or each chain: the state, the sweeps, how it
|
||||
ended, the accumulates and the draws.
|
||||
|
||||
**Declining.** Before anything runs, a member MAY decline a workload whose field bound
|
||||
(§4.1) is above 2³¹ − 1, which a device with 32-bit lanes cannot hold, saying why in
|
||||
words that are true of the workload. It MAY decline no other workload a job can give;
|
||||
§2.0's limits keep every other need of a job, such as a sweep's table (§4.2), within
|
||||
what any member can hold. A member may decline for one kernel and not the other: the
|
||||
rule is the same for each. A host runs a member on no workload it declines, however the
|
||||
member was reached. The reference declines nothing.
|
||||
**Declared capacity.** Real hardware holds so many cells, fields so wide, a table so large.
|
||||
A member declares what its kernels hold, once and before anything runs, as fixed numbers
|
||||
(`member.capacity`, each of them optional, none declared being no limit):
|
||||
|
||||
| number | what it bounds |
|
||||
|---|---|
|
||||
| `cells` | the cells of a workload |
|
||||
| `couplings` | its couplings, each pair counted twice (`2P`) |
|
||||
| `at_zero`: `field`, `field_bound`, `cost` | for a run with a sweep at temperature 0 (a settle, a quench, the end of a schedule): the largest `\|b_i\|`, field bound (§4.1) and cost |
|
||||
| `above_zero`: `field`, `field_bound`, `cost` | the same, for a run with a sweep above temperature 0 |
|
||||
| `table_entries` | above temperature 0: one sweep's cut points over the fields the cells can reach (§4.2) |
|
||||
| `uncertain_span` | above temperature 0: the span, first to last, of the fields the cells can reach whose draw is uncertain at a sweep's temperature, where it is not the threshold's `(0, 0)`, `(0, 2⁵³)` or `(2⁵³, 2⁵³)`. Those fields at a temperature hold those at any lower one, so the hottest sweep's span is the widest, and it is the one held to the number |
|
||||
| `sweeps` | the sweeps of one run: a settle's bound, a schedule's length |
|
||||
| `chains_per_call` | the chains it takes in one call. Not a limit on a job: a host splits a job into calls of at most this many, and certification splits every vector the same way (§5.1), so no job goes elsewhere for it |
|
||||
|
||||
A workload fits when every declared number but `chains_per_call` holds for it. Whether it does is a function of
|
||||
the workload, its schedule and the declared numbers alone: the host works it out, and
|
||||
declines for the member exactly the workloads that do not fit, saying which number each
|
||||
is past. A member declines nothing by choice, job by job, and is run on no workload past
|
||||
its capacity, however it was reached. A member that refuses or fails a workload within
|
||||
its capacity has failed it. The reference declares no capacity.
|
||||
|
||||
**The floor.** A member's declared numbers MUST be at least the floor's, or the host does
|
||||
not certify it at all, so that no member is certified by declaring itself too small to
|
||||
be held to anything. The floor is the least capacity that takes every case of the core
|
||||
set (§7.1): the settle, chains and battery cases the set held before it grew to reach
|
||||
large jobs. Its numbers are 32 cells and 992 couplings (the recall vector of 32 cells,
|
||||
every pair coupled); on each side of temperature 0, fields to ±40, a field bound of 41
|
||||
and a cost of 8 (the wide-fields chains, run cooling and quenched at 0); 31 entries of
|
||||
cut points a sweep (the dense chains of 16 cells, every pair coupled, at their hottest);
|
||||
an uncertain span of 61 (the wide-fields chains at their hottest); and a run of 5,000
|
||||
sweeps (the core set's long schedule, §7.1). It declares no batch: any number of chains a
|
||||
call is certified by splitting. A device that cannot hold these cannot be held to the
|
||||
core set, and the grade would vouch for nothing.
|
||||
|
||||
**Generated vectors.** A finite set samples each axis of a workload at a few points,
|
||||
and a declared number takes vectors out of it. So the host also holds a member to vectors
|
||||
it generates from the member's capacity, by the rules of §7.1, with the reference's bits
|
||||
as their expected bits and the same on every host. Some are generated for every
|
||||
capacity, the reference's none among them, so that a member is held across each axis up
|
||||
to what its capacity lets a workload reach: every cell count at and beside a power of
|
||||
two, every call size to 32 chains and each band of a wavefront past that, the longest
|
||||
row, fields at each lane's wrap points, a corner with every number at its most, and a
|
||||
run past a 24-bit draw index. Others
|
||||
are generated for each number a member declares below what a job can reach (§2.0: 4096
|
||||
cells; 838,860 couplings, two for each ten bytes of the longest program; a field and a
|
||||
cost of 2⁴⁰; a field bound of 2⁴⁰ + 4095; a table of twice the couplings and the cells;
|
||||
a span of twice the field bound and one; 1,000,000 sweeps), at exactly that number, each
|
||||
also within every other number declared. A table or a span is reached through couplings;
|
||||
where the other declared numbers do not let a workload reach it, the vector reaches as
|
||||
far as they do. Only vectors the capacity admits are run. So a grade is earned across a
|
||||
capacity and at its edges, not only at the fixed vectors inside it. No finite set catches
|
||||
every fault, so the host then audits every answer from a kernel it graded `bit-exact`
|
||||
(§7.3).
|
||||
|
||||
**Placing a job.** A host offered several members gives the job to the first that can
|
||||
take it: one whose kernel is certified for every kernel the job runs (§5.1), that does
|
||||
not decline the workload, and that gives an answer the host's checks keep. Each member
|
||||
take it: one whose kernel is certified for every kernel the job runs (§5.1), within
|
||||
whose capacity the workload fits at every schedule the job runs, and that gives an
|
||||
answer the host's checks keep. Each member
|
||||
passed over, for any of those, is named under `refusals`, in the order offered, with
|
||||
why; with none, `refusals` is absent. A member that took the job and failed it, or
|
||||
whose answer the checks refused, is passed over the same way and the next is asked.
|
||||
|
|
@ -439,11 +493,16 @@ took before it came to rest can be checked only by running it again: a `bit-exac
|
|||
certificate is what vouches for it.
|
||||
|
||||
The reference implementation has three GPU members besides the CPU reference, each
|
||||
certified by the host on its own run of the conformance set (informative): through WebGPU
|
||||
(32-bit lanes; it declines a workload whose field bound is above 2³¹ − 1, holds a cost
|
||||
above every field at its lanes' top, where it holds every cell at 0 as it does at its
|
||||
own size, and its coloured kernel can be `statistical` at most), through CUDA and through
|
||||
Metal (64-bit fields, so they take every workload). All three run the sampling kernel one
|
||||
certified by the host on its own run of the conformance set within the capacity it
|
||||
declares (informative): through WebGPU (a field bound of 2³¹ − 1 on both sides of
|
||||
temperature 0, which its 32-bit lanes hold; cells and couplings within a 32nd of a
|
||||
storage binding; 4,194,304 entries of cut points a sweep. It holds a cost above every
|
||||
field at its lanes' top, where it holds every cell at 0 as it does at its own size, and
|
||||
its coloured kernel can be `statistical` at most), and through CUDA and Metal (64-bit
|
||||
fields, and 4,194,304 entries of cut points a sweep, which no job reaches). An FPGA board
|
||||
(`wai/wai-quantum-landscape-fpga`) declares 64 cells and, above temperature 0, fields to
|
||||
±524,287, a cost to 1,048,575 and an uncertain span of 8,192, its registers and its
|
||||
table's window. All three run the sampling kernel one
|
||||
chain a thread, the cells in the reference's order, with the reference's draws and the
|
||||
host's cut points for the fields the cells can reach, a window of sweeps a dispatch, each
|
||||
window waited for before the next is sent; and give its bits.
|
||||
|
|
@ -455,20 +514,28 @@ at most 64 characters in all. `cpu:reference` is the reference's, and no other m
|
|||
### 5.1. Parity
|
||||
|
||||
A member does not state its parity: the host finds it, by running the member's kernels
|
||||
on the conformance set (§7), and an answer states what the host found for the kernel the
|
||||
run used (`member.kernel`, `member.parity`).
|
||||
on the conformance set (§7) within the member's declared capacity (§5) and at its edges,
|
||||
and an answer
|
||||
states what the host found for the kernel the run used (`member.kernel`,
|
||||
`member.parity`) and the capacity it holds within (`member.capacity`, absent where none
|
||||
is declared, so that the reference's answers are what they were). A parity is a statement about the jobs within that capacity, and no others.
|
||||
|
||||
| parity | granted when |
|
||||
|---|---|
|
||||
| `reference` | to the host's own reference kernels, and to nothing else |
|
||||
| `bit-exact` | settle: the member gives every `settle` vector it takes exactly. Sample: it gives every `chains` vector it takes exactly, each case's chains in one call. Either way it takes every vector §5 does not let it decline |
|
||||
| `statistical` | sample: not `bit-exact`, and every workload of the `statistical` battery passes (§7.2) |
|
||||
| `bit-exact` | settle: the member gives every `settle` vector within its capacity, and every one generated for it (§5), exactly. Sample: it gives every `chains` vector within its capacity and every one generated for it exactly, each case's chains in calls of as many as a host would give it at run time: the batch of §2.0, or `chains_per_call` where it declares fewer |
|
||||
| `statistical` | sample: not `bit-exact`, and every workload of the `statistical` battery within its capacity, and every battery workload generated for it, passes (§7.2), in calls of the same size |
|
||||
| `uncertified` | anything else, or a member the host was asked to run by name without certifying it |
|
||||
|
||||
A member is held to every vector it takes, and declining a vector §5 does not let it
|
||||
decline is failing that vector. The set holds vectors on both sides of the line, at a
|
||||
field bound of exactly 2³¹ − 1 and just above it by the couplings alone, so that a
|
||||
member is held to the edge of what it takes.
|
||||
The host reads a member's capacity once, before anything runs: the floor is checked
|
||||
against that reading, the vectors are held to it, and every answer states it. A member
|
||||
is held to every vector within its capacity, and refusing one is failing it. A grade
|
||||
holds while the host's audit finds nothing (§7.3): at the first answer from a kernel
|
||||
graded `bit-exact` that differs from the reference's, the member is `uncertified` for
|
||||
both kernels from then on.
|
||||
The set holds vectors on both sides of a field bound of 2³¹ − 1, at it and just above it
|
||||
by the couplings alone, so that a member that declares 32-bit lanes is held to their
|
||||
edge. A capacity under the floor (§5) is not certified.
|
||||
|
||||
A settle kernel can earn `bit-exact` on any device. A sampling kernel can, if it uses the
|
||||
reference's draws and order and the cut points of §4.2.
|
||||
|
|
@ -476,7 +543,7 @@ reference's draws and order and the cut points of §4.2.
|
|||
A host placing a job never places an uncertified kernel (§5). A host MAY still run one
|
||||
when it is asked for that member by name, without placing it, which is how a board gets
|
||||
its first answers; the answer says `uncertified`, and nothing should be concluded from
|
||||
its state but its energy.
|
||||
its state but its energy. Even then the host runs it on nothing past its capacity.
|
||||
|
||||
### 5.2. The receipt
|
||||
|
||||
|
|
@ -500,7 +567,10 @@ A member states its own, and MUST say what device ran the job, whether its joule
|
|||
priced or read from a meter, and that no quantum effect is involved where none is. It
|
||||
calls a device what it is: a software driver that runs on the processor is not a GPU.
|
||||
An answer adds that a reading stands beside the priced figures, and what it covers, only
|
||||
where that answer carries one (§4.4).
|
||||
where that answer carries one (§4.4); and, for a member that declares a capacity, that
|
||||
the host runs it on no job past that capacity, in its numbers, and, where the host
|
||||
certified it, that its parity holds within it (§5). A member run by name without
|
||||
certification claims no grade, and one the audit demoted says so and why (§7.3).
|
||||
|
||||
## 6. The comparison
|
||||
|
||||
|
|
@ -521,6 +591,14 @@ vectors`, and a test holds the file to the code. Each part pins a rule of this t
|
|||
that an implementation written from the text alone, which computes any of these another
|
||||
way, is caught.
|
||||
|
||||
The **core set** is the part of it the set held before it grew to reach large jobs, and a
|
||||
long schedule: the `settle` vectors but `fields-at-the-bound`, the first fifteen `chains`
|
||||
vectors (three landscapes cooling at five schedules), `a-long-schedule-across-windows`,
|
||||
and the first eight battery workloads. It sets the floor of a declared capacity (§5), so
|
||||
no capacity a member may declare leaves out the long schedule. The vectors generated for
|
||||
a capacity (§5) are not in the file: a host builds them from the capacity, by the rules
|
||||
below.
|
||||
|
||||
### 7.1. Bits
|
||||
|
||||
- `settle`: whole workloads, starts and bounds, each with its field bound, and the state,
|
||||
|
|
@ -548,10 +626,114 @@ way, is caught.
|
|||
product, a cut point compared by its high word alone); 16,384 draws in one sweep;
|
||||
more chains than a workgroup; chains that stop hot, in which the order of the cells
|
||||
shows; chains long enough to cross a device's windows of sweeps, by table and by
|
||||
work, hot in every window; and the same fields past 16 and 32 bits and at the line as
|
||||
the settle vectors, at 0 and at a temperature of their own size. A sampling kernel is
|
||||
`bit-exact` when it gives every case it takes exactly, each from one call, and takes
|
||||
every one §5 does not let it decline.
|
||||
work, hot in every window: among them, in the core set, 5,000 sweeps on 32 cells each
|
||||
coupled to 15 others, within the floor on every number, which cross windows of both
|
||||
kinds, and 200,000 sweeps of two cells, across a window by work on the smallest
|
||||
workload; and the same fields past 16 and 32 bits and at the line as the settle
|
||||
vectors, at 0 and at a temperature of their own size. A sampling kernel is
|
||||
`bit-exact` when it gives every case it takes exactly, each case's chains in calls of
|
||||
as many as a host would give it (§5.1), and takes every one §5 does not let it
|
||||
decline.
|
||||
- Generated, by rule, from a member's capacity (§5), where `C` is the most cells it lets
|
||||
a workload hold (4096 with none), `P` the most coupled pairs, `B` the chains of one call
|
||||
(4096, or `chains_per_call` where fewer), and each vector run only where the capacity
|
||||
admits it:
|
||||
- cells: for each `n` of 1, 2 and 3, each power of two to `C` with one less and one
|
||||
more, and `C` and `C − 1`: `n` cells, each with a field of 1 or 2 of either sign so
|
||||
that every one moves in its first sweep, coupled each to the next (spread along the
|
||||
whole line where `P` is fewer); settled for 4 sweeps, and held at 1.5 for 3; and,
|
||||
where a last partial warp of 32 or workgroup of 64 holds more than half of it,
|
||||
quenched at 0 for 3 sweeps; and a wave settling down as many cells as a workload can
|
||||
hold (`C`, or `P + 1` where fewer), one cell a sweep, coming to rest within the most
|
||||
sweeps a run may have (of fewer cells where those are fewer): as long as a wave can
|
||||
be; and the same wave stopped at its bound one sweep before it would rest, its last
|
||||
sweep moving a cell;
|
||||
- the final sweep: a sweep at 0 straight after sweeps above 0, as an engine runs it at
|
||||
the end of a job, on as many uncoupled cells as a last, partial warp of 32 or
|
||||
workgroup of 64 holds past half of it: 17, 18, 24, 30 and 31, and 33, 34, 48, 62 and
|
||||
63 (odd and even, at the band's edges and in its middle), each after two sweeps at
|
||||
1.5, after three (the sweep at 0 at an odd index), and after two and one at 1/64 (a
|
||||
cold sweep, as an annealing job ends); and, after two sweeps at 1.5, three quarters,
|
||||
all but two and all but one of a group of 128, 256, 512 and 1,024. Each cell's field
|
||||
equals the cost of 1, a tie between two states above 0 that the sweep at 0 settles to
|
||||
0 however cold the sweep before it, so that the sweep moves cells;
|
||||
- a settle that keeps moving: `u` units of three cells, each unit's third cell agreeing
|
||||
with the other two and each unit coupled to the next two by fixed patterns (`13u − 16`
|
||||
couplings), with fields `−2, 1, 6` on the first unit, `0, 3, −1` between and `0, −5, 0`
|
||||
on the last, which from every cell at 0 moves a cell in every sweep to sweep `11u − 59`
|
||||
and rests at `11u − 58` (for 10 units or more); or, where couplings are too few for
|
||||
those to move longer, units of two, each cell opposed to its partner and to the other
|
||||
cell of the next unit, with fields `0, −5` on the first unit, `1, 2` between and
|
||||
`−4, 5` on the last, which move to sweep `4u − 10`. Stopped at its bound just past 2⁹,
|
||||
2¹⁰, 2¹¹, 2¹² and 2¹³ sweeps, where a narrow sweep counter wraps, each on as few units
|
||||
as still move there, and at its last moving sweep (or the most a run may have) on as
|
||||
many cells as the capacity takes: with none, 1,365 units of three, 4,095 cells, moving
|
||||
to sweep 14,956.
|
||||
A search over units of three found these; no longest-moving settle within the rules is
|
||||
known, and a sweep counter of 14 bits or more, which none of these wraps, or a stale
|
||||
read at a bound past the sweeps they reach, is left to the audit (§7.3);
|
||||
- two-cycles that end late: the same units of three beside two cells that alternate from
|
||||
the start (agreeing, with fields 1 and −1), so that the whole comes to a two-cycle at
|
||||
sweep `11u − 57`: with 46 units at sweep 449, the first sweep of a later batch of 64,
|
||||
where a host that runs a settle in batches must carry the state two sweeps back from
|
||||
the batch before; and with 378 units at sweep 4,101, past 2¹², where a sweep counter
|
||||
of 9 to 12 bits read at the ending gives the other phase of the cycle (on as many
|
||||
units as the capacity takes, to 378). A fault at a two-cycle ending later than these is
|
||||
left to the audit;
|
||||
- calls: one call of each number of chains from 1 to 32, of 33, 48, 63, 64, 65, 96, 127
|
||||
and 128, and of `B` and `B − 1`;
|
||||
- a row: one cell coupled to as many as a workload within the capacity can give it,
|
||||
their fields holding them so that its whole row gives it a field of −1 and any
|
||||
shorter part of the row 0 or more; settled, held at 0.5, and ending at 0;
|
||||
- lanes: on each side of 0, a cell at each of `2^(L−1) − 1`, `2^(L−1)`, `3·2^(L−2)`
|
||||
and `2^L`, for `L` of 16, 24 and 32, that the field reaches, of both signs, and a cell
|
||||
whose field is reached exactly through two couplings at each one the field bound
|
||||
reaches; and the same beside the largest cost (to `2¹⁵ − 2`), with a cell each side
|
||||
of the threshold it sets: packed into as few workloads as the capacity takes, so that
|
||||
a small capacity holds each in a workload of its own size. Each is settled for 3
|
||||
sweeps and for 1 and quenched at 0; held at `2¹⁵` and at 1/64 above 0; and at `2¹⁵`,
|
||||
at 1/64 and at 0, and at 1/64 and then 0, where both sides take it, the shape an
|
||||
annealing job ends in;
|
||||
- a corner on each side of 0, as the capacity takes it: the largest field of both
|
||||
signs beside the largest cost under it, so that those cells move at 0, among as
|
||||
many cells and couplings as the capacity allows (fewer where a table or span limit
|
||||
asks); and a cell whose field is reached exactly through as many couplings as a cell
|
||||
can have, the field bound at its most; and a cell at field 0 whose row of cells held
|
||||
at +1 sums to 128, and to as many as a cell can have, past what a narrow lane holds.
|
||||
Each at 0, cold above 0, and ending at 0;
|
||||
- a span: where no span is declared, two hubs on shared cells as wide apart as the
|
||||
other numbers let them reach (the widest a declared table takes, where it is the
|
||||
narrower), held at a temperature of the span's size and ending at 0;
|
||||
- draw indices: one run of `C` cells carried past `2²⁴` where `C` times the most sweeps
|
||||
reaches it, at 0 until the sweeps that cross it and hot to the end. A run past `2³¹`
|
||||
would be at least `2³¹` updates in one chain, minutes on a device that runs a chain a
|
||||
thread, so a draw index narrower than 64 bits past 24 is left to the audit (§7.3);
|
||||
- at each number declared below what a job can reach: a wave settling down `C` cells;
|
||||
`P` couplings, each cell's as near as the field bounds allow; the field bound at 0
|
||||
through couplings and by a field alone, settled to the end and for one sweep, and
|
||||
above 0 alone, coupled and reached exactly; the field and the cost at 0, and above 0
|
||||
hot, cold and ending at 0; a table of exactly that many entries and a span exactly as
|
||||
wide, through hubs coupled to shared cells where the other numbers allow it (where a
|
||||
declared table or the couplings do not take that span, the widest under it they do);
|
||||
and a schedule of exactly that many sweeps;
|
||||
- for the battery (§7.2), where `C` is past 24: `C` alike cells, every cell read in
|
||||
blocks of three (the last block ending at the last cell), and, where `C` is not one
|
||||
less than a power of two, as many alike cells as the largest such count under it
|
||||
from 31 to 1,023, every cell read, so that a last, partial warp or workgroup holds as
|
||||
many as it can; at declared couplings, alike triangles, every one read; at a declared
|
||||
schedule length, one alike triangle warm and then cold, to 1,024 sweeps; at a
|
||||
declared field bound, field and cost, three alike cells at it, and the field bound
|
||||
through couplings; at a declared span, its two hubs where they hold no other cell.
|
||||
Identical blocks cannot reach a table, so a table has none.
|
||||
|
||||
The rules bound what certification costs, whatever the capacity: at most 300 generated
|
||||
vectors, at most 450 million cell sweeps hot or settled, and at most 20 million at
|
||||
temperature 0 (the run past `2²⁴`, at a fixed point, which the reference skips). With no
|
||||
capacity they come to about 262 million cell sweeps, two fifths of them battery
|
||||
workloads, which run only for a kernel that is not `bit-exact`; the most, for a
|
||||
capacity of 4,096 cells that declares every other number below what a job can reach, is
|
||||
about 430 million, half of them battery workloads. A test holds the bounds over the
|
||||
named capacities and over random ones, and prints each one's share.
|
||||
- `jobs`: whole requests and what their answers hold, refusals included: every worked
|
||||
example at seed 1, and requests that pin a rule each (a `minimize` above temperature 0,
|
||||
a temperature given as text, a pin's size, quantised weights, the state a two-cycle
|
||||
|
|
@ -565,7 +747,9 @@ way, is caught.
|
|||
A sampling kernel with draws of its own is held to `statistical`: workloads of two to six
|
||||
cells, and one of eight blocks of three side by side, none coupled to another, each with
|
||||
a schedule. A member may decline a workload of the battery only where §5 lets it decline
|
||||
a vector, and it then passes that workload without running it. Workload `k` runs `statistical.chains` (20,000) chains, chain `r` with the
|
||||
a vector, and it then passes that workload without running it. A workload of many blocks
|
||||
may name the blocks it reads (`read`); only those are counted, so that many tests at once
|
||||
do not fail a correct sampler by chance. Workload `k` runs `statistical.chains` (20,000) chains, chain `r` with the
|
||||
chain seed `out(statistical.seed + k, r + 1)` and its start from that seed (§4.2),
|
||||
through the workload's temperatures. The final states of each `block` of cells are
|
||||
counted over its `3ⁿ` states, in the order `Σ (s_i + 1)·3ⁱ`, and held to `exact`. Where
|
||||
|
|
@ -579,6 +763,34 @@ bin expected fewer than `statistical.pool_below` (5) times pooled into one; with
|
|||
bins less one, a block passes when it is at most `df + 5·√(2·df)`, and a workload when
|
||||
every block does.
|
||||
|
||||
The workloads a capacity adds (§7.1) are held by a bound that holds whatever the law,
|
||||
however small its bins, so that they fail a correct sampler, whatever its draws, less
|
||||
than once in a million between them. They run in calls of 63, 62 and on down to 17 chains
|
||||
in turn (no more than the member takes), so that their calls end in every partial warp of
|
||||
more than 16 chains and every partial workgroup of more than 32. Every block is read. A
|
||||
block's statistic is the likelihood ratio `G = 2 Σ O ln(O/E)` over every
|
||||
bin, none pooled: `2N` times the divergence of its counts from its law, `N` the chains.
|
||||
By the method of types, the chance that `N` draws over `k` bins fall at a divergence `t` or
|
||||
more from their law is at most `C(N + k − 1, k − 1)·e^(−N·t)`, the number of ways `N` draws
|
||||
can fall in `k` bins times the chance of the likeliest of them. So a block passes when `G`
|
||||
is at most `2·(ln C(N + k − 1, k − 1) + ln(B/10⁻⁸))`, `B` the blocks the workload reads: a
|
||||
workload fails a correct sampler with chance at most 10⁻⁸ (for 27 bins and one block,
|
||||
429.3; for 10 bins, 189.5). A test sums the chance exactly over every way 1,000 draws can
|
||||
fall in three bins, for laws with bins a pooled chi-square mishandles. A sampler that
|
||||
leaves a cell unmoved, draws one at random, or leaves a call's last partial warp at its
|
||||
starts lies far past the bound on the block that holds it: a cell left at its start in a
|
||||
block of three gives `G` near 12,000 at `N` = 20,000.
|
||||
|
||||
The bound is loose, and what it misses is part of the grade. It sits at a divergence of
|
||||
about 0.011 a block (429.3 over `2N`), so a law near the exact one passes: three alike
|
||||
cells at field 1 drawn at a temperature 10% too high are 0.0055 from their law, `G` near
|
||||
250, and pass; 15% too high, `G` near 490, and fail. The battery's cells are the fixed
|
||||
workloads' (2 to 24), `C` and the largest `2^j − 1` under it: a fault keyed to another
|
||||
count of cells, such as the lanes past 32 of a partial last workgroup only where it holds
|
||||
an even count, is never run. A test holds each of these statements. A kernel graded
|
||||
`statistical` is never audited (§7.3), and every answer from one says what the grade rests
|
||||
on and what it misses.
|
||||
|
||||
The workloads hold pairs of coupled cells next to each other, two apart and three apart,
|
||||
a triangle no two colours can split, fields to 40 and a cost of 8 at a high temperature, a
|
||||
small field at a low one, a schedule that ends at 0, one that cools from 6 to 0.7, a ring
|
||||
|
|
@ -592,6 +804,121 @@ where they started fails at least one of them; the reference's own kernel, one t
|
|||
takes its cells last to first pass every one. A test holds both statements, with
|
||||
kernels that are wrong in each of those ways.
|
||||
|
||||
### 7.3. The audit
|
||||
|
||||
A finite set cannot catch every fault, so a `bit-exact` grade rests on a continuous check
|
||||
as well as on certification, and the check rests only on what the host computes and
|
||||
remembers. For every answer from a member's kernel graded `bit-exact`, the host runs part
|
||||
of the job again on the reference and compares the bits:
|
||||
|
||||
- sampling: in every job (a run, a replay, a sweep or a comparison), in every call to the
|
||||
member, the call's last chain and, for each seed's run the call holds chains of, one of
|
||||
those chains, each drawn afresh: what the host checks in one call says nothing of what
|
||||
it checks in the next, so a member that times the host between its calls learns
|
||||
nothing it can use. The host runs a call's checked chains again after the call returns
|
||||
and before it makes the next: on the job's own thread, at the job's priority, and on as
|
||||
many of the audit's threads as are free, a quarter of the host's cores and at most 4
|
||||
over the whole process at once (none on a host of fewer than 4 cores), each at a low
|
||||
priority (a nice value 10 higher, to 19, on Linux; the background band on macOS). So an
|
||||
audited job takes at most its own thread and the audit's threads; several audited jobs
|
||||
at once each run a share on their own threads at their own priority, beside the
|
||||
audit's threads, and a host that must keep audited jobs from crowding out others lowers
|
||||
those jobs' own priority. An audit thread whose priority cannot be lowered says so on
|
||||
standard error, once, and runs its share all the same;
|
||||
- settling: the reference's settle, as far as a budget of 2²⁴ of work goes, the work
|
||||
being the cells and couplings times the sweeps the host asked for, never the sweeps the
|
||||
member reports. Where the reference comes to rest within the budget, or the whole bound
|
||||
fits it, that is the whole answer, and the member's is compared with it. Past it, the
|
||||
whole settle is run in one job in 8; in the others a member whose settle ends before
|
||||
the reference's had is wrong. The checks of §5 hold as before.
|
||||
|
||||
The chains, and which settles past the budget are run whole, are drawn with a key the
|
||||
host makes when it starts, from its operating system's randomness, and a count that
|
||||
every draw moves on: no member can tell which of its chains are checked, and the same job
|
||||
sent again is drawn afresh. Every audited answer records what was checked
|
||||
(`audit.checked`: each run's seed and its chains checked, or how much of a settle). The
|
||||
draws are the host's, as the rest of its work is: nothing in an answer lets a reader check
|
||||
afterwards that the chains were drawn and not chosen, and a reader who needs that runs
|
||||
the job on the reference.
|
||||
|
||||
At the first difference, or the first answer the checks refuse, the host demotes the
|
||||
member to `uncertified` for both kernels, at once. It answers that job from the reference,
|
||||
naming the member under `refusals` with why, and states the demotion in every later
|
||||
answer from the member (`member.demoted`) and in its label. A host placing jobs never
|
||||
places it again. The host keeps the demotion under the member's identity, its id and its
|
||||
fingerprint (a bitstream's or a firmware's digest, a driver's version: `Member::fingerprint`),
|
||||
in a file where it names one, so that it outlives the process: certifying the member again,
|
||||
or its declaring another capacity, does not clear it. Only another identity is certified
|
||||
afresh. Hosts and jobs may share the file. The host keeps a demotion at once in a file of
|
||||
its own beside it (`.<name>.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 (`.<name>.<write|probe|demotion>.<process>.<count>.tmp`,
|
||||
matched exactly, so that no other store's file is taken for one of its own), each held
|
||||
locked by its writer while it lives; one unchanged for 10 s that no writer holds is a
|
||||
killed writer's, and the next certification or demotion removes it. Where the path is a
|
||||
link, the store is the link's target, followed link by link: its lock, its files and its
|
||||
rename; a link in a folder anyone may write that has the sticky bit is not followed,
|
||||
whoever made it, and no file of the store's is opened through a link. The store fails
|
||||
closed. The host says so on its standard error, grades no kernel `bit-exact`, and holds
|
||||
the member at `uncertified`, saying why in `member.demoted`, where the path names no file
|
||||
(an empty `WAI_LANDSCAPE_DEMOTIONS` names none, and is as if unset), where it is a link to
|
||||
nothing or a link in such a folder, where the file has more than one name (a hard link,
|
||||
which a rewrite would split), where the file or a demotion in a file of its own cannot be
|
||||
read or does not parse, where the file cannot be rewritten as a demotion rewrites it,
|
||||
where a write fails, or where a host that keeps one has no file to name. A demotion it
|
||||
could neither keep in its own file nor merge it keeps for as long as it runs, and says
|
||||
so. Where the lock is held past 5 s, the certification that waited grades no kernel
|
||||
`bit-exact` and says why, and the next waits again, so that a board job holds the board
|
||||
no longer than that waiting on the store.
|
||||
|
||||
A kernel graded `statistical` gives draws of its own, which no audit can hold to the
|
||||
reference's bits. A host placing jobs places it only where the job asks for it
|
||||
(`"parity": "statistical"`); otherwise it passes over it, saying why, and a job is
|
||||
answered by a kernel whose bits are audited, or by the reference. Asked for by name, it
|
||||
is run, and its answer says `statistical`.
|
||||
|
||||
Its cost: in every call, its last chain and one chain of each run it holds chains of, so
|
||||
for a run of `R` chains in calls of `B` at most `2⌈R/B⌉/R` of its sampling work again,
|
||||
and for a job of many seeds' runs in one call, one chain of each and one more; for a
|
||||
settle, at most 2²⁴ of work, and past it the whole settle again one job in 8. The bound
|
||||
is that work. It is work on the host's CPU, which a device outruns, so in wall time the
|
||||
audit costs more than its share of the work, most where a job is many short runs, and by
|
||||
how much depends on the host, its load and the device: a host that needs a wall-time
|
||||
figure measures it on its own hardware (the `timings` test of `landscape_devices` prints
|
||||
five runs of each of four jobs, audited against the same member run by name).
|
||||
Ten runs a device on two shared hosts, with the audit's threads as above, give
|
||||
an idea: a replay of 256 seeds of 8 chains of 64 cells for 500 sweeps took 108% to 171%
|
||||
longer audited through Metal on an M3 Ultra's GPU, and 85% to 122% through CUDA on an RTX
|
||||
4050 laptop GPU; a replay of 256 seeds of one chain each, where every chain is checked,
|
||||
112% to 183% and 90% to 120% longer, so that through Metal it took up to 1.18 times what
|
||||
the reference alone took; a run of 4,096 chains of 64 cells at most 18% longer, and one of
|
||||
1,024 chains of 1,024 cells at most 4%. What it catches: a fault that makes a fraction `q`
|
||||
of a run's chains wrong, on each such run with a chance of at least `q`, whatever chains
|
||||
it chooses, since it cannot tell which are checked; one that makes the last chain of a
|
||||
call wrong (a partial last warp or workgroup, a call longer than the member keeps), on
|
||||
the first job that shows it; one in a settle whose reference comes to rest within the
|
||||
budget, on the first job that shows it; one that ends a settle at or before the last
|
||||
sweep the budget reaches while the reference's goes on, on the first such job; and any
|
||||
other in a settle past the budget, one that ends it later than that sweep and sooner
|
||||
than the reference among them, only within `N` such jobs, with a chance of
|
||||
`1 − (7/8)^N` (0.74 at 10 jobs, 0.99 at 35), each job drawn afresh, repeats of one job
|
||||
included. A draw index narrower than 64 bits, which
|
||||
certification holds only to `2²⁴` (§7.1), is caught on the first job whose run passes its
|
||||
width, since every chain of that run is then wrong. The audit is a check that runs as long
|
||||
as the member does. It is not a proof.
|
||||
|
||||
## 8. What does not run on this model
|
||||
|
||||
The worked examples include each of these, and they fail there, on purpose: problems of three
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -58,20 +58,44 @@ the draw the reference takes next.
|
|||
**The member.** `src/member.rs` implements `wai_quantum::quantum_landscape::member::Member`. It is
|
||||
named `fpga:kv260-landscape-64`. It says nothing of its own parity: the host certifies it
|
||||
(`member::certify`), running the conformance set's `settle` and `chains` cases on the device and
|
||||
granting `bit-exact` to a kernel only if every case it takes comes back bit for bit (and, for a
|
||||
sampling kernel that misses, trying the statistical battery). It declines only what the spec lets a
|
||||
member decline (§5: sampling fields above 2^31 − 1).
|
||||
granting `bit-exact` to a kernel only if every case within its declared capacity comes back bit for
|
||||
bit, and every case the host generates for it: by rule, every cell count to 64 (each power of two,
|
||||
one less and one more), every call size to 32 chains, each band of a wavefront and a whole batch, a row of 63 couplings, fields
|
||||
at the wrap points of 16-bit lanes, a run past draw index 2²⁴, and a corner with every number at its
|
||||
most; and at each number it declares, a wave down 64 cells, fields at ±524,287 and a cost of 1,048,575
|
||||
drawn hot, cold and hot then at 0, and two fields spanning 8,192 drawn at temperature 8,192. For a
|
||||
sampling kernel that misses, the host tries the statistical battery within it. After that, the host
|
||||
audits its answers (§7.3 of the extension) and demotes it at the first that differs. This host keeps
|
||||
the demotion in `~/.local/state/wai-quantum-landscape-fpga/demotions.json` (or the file
|
||||
`WAI_LANDSCAPE_DEMOTIONS` names), under the board's id and its engine's ID and version, so a demoted
|
||||
board is not certified again on its next job; a new engine is. It reads the file when it certifies
|
||||
the board, after it holds the board's lock, with every demotion kept in a file of its own beside it,
|
||||
and rewrites it as a demotion would; it keeps a demotion at once in a file of its own and merges it
|
||||
into the file under the file's own lock, waited for at most 5 s (where that runs out, the demotion
|
||||
stays in its own file, which every certification reads), so jobs and hosts sharing it keep one
|
||||
another's; a link is followed to its target, except a link in a sticky folder anyone may write.
|
||||
Where the file cannot be read, parsed or rewritten (a full disk, a read-only folder, a file another
|
||||
user owns in a sticky folder, one marked immutable), it has a second name (a hard link), the lock is
|
||||
held past 5 s, or `HOME` is unset and no file is named (an empty `WAI_LANDSCAPE_DEMOTIONS` names
|
||||
none), it says so on standard error and grades no kernel `bit-exact`.
|
||||
|
||||
**What it cannot hold, and what that costs.** The engine has 64 cells, 20-bit field and cost
|
||||
registers for sampling, and a table window of 8,192 fields a temperature. The conformance set as it
|
||||
stands now holds cases past all three (settle cases of 65 to 1,000 cells, chains of 128 and 1,024
|
||||
cells, sampled fields up to 2^31 − 1, a battery workload whose uncertain fields span 135,000), and
|
||||
the spec does not let a member decline them. The board refuses each with a sentence, never with
|
||||
wrong bits, and every case it can hold it gives bit for bit (`conform`). But a refused case is a
|
||||
missed vector, so the host certifies both kernels `uncertified` under the current set, and a job
|
||||
on the board says so. The board was certified `bit-exact` for both kernels under the set of
|
||||
2026-11-06 (`vectors.json` sha256 `43e543a634e0ffdc...`), before the set grew; the measurements
|
||||
below were taken then. It runs whatever schedule the job gives, one
|
||||
**Its declared capacity** (`member::CAPACITY`, §5 of the extension): 64 cells; above temperature 0,
|
||||
fields to ±524,287 and a cost to 1,048,575, its 20-bit registers, and uncertain fields within a span
|
||||
of 8,192 at a temperature, its table's window. At temperature 0 it takes any field (below). The host
|
||||
runs it on no job past those numbers, and a test holds the declared numbers to the engine's own
|
||||
limits, case by case over the whole set and the generated cases
|
||||
(`the_declared_capacity_is_exactly_what_the_engine_holds`), and on 3,000 random workloads and
|
||||
schedules (`the_capacity_and_the_engine_agree_on_random_jobs`).
|
||||
|
||||
**What it cannot hold.** The conformance set holds cases past all three of its limits (settle
|
||||
cases of 65 to 1,000 cells, chains of 128 and 1,024 cells, sampled fields up to 2^31 − 1, a battery
|
||||
workload whose uncertain fields span 135,000). Those are past its declared capacity, so the host
|
||||
neither runs them on it nor counts them against it: on the register-level model, which the
|
||||
testbenches hold the logic to, the host certifies both kernels `bit-exact` within that capacity
|
||||
against the set as it stands (`the_model_is_certified_bit_exact_within_its_declared_capacity`), and
|
||||
every job's answer states the capacity. The board itself was certified `bit-exact` for both kernels
|
||||
under the set of 2026-11-06 (`vectors.json` sha256 `43e543a634e0ffdc...`), before the set grew;
|
||||
the measurements below were taken then, and it is to be certified again on the set as it stands. It runs whatever schedule the job gives, one
|
||||
temperature a sweep: a run of sweeps above 0 as sampled segments, a run at 0 in T0 mode, sweep `k`
|
||||
drawing from `k·n + 1` whatever came before it. The reader, the truth, the energy, the verdict, the
|
||||
checks on what a member returns, the replay digest and the seal stay on the host
|
||||
|
|
@ -255,9 +279,8 @@ agreement checks also on the EMIO build, with the same results (`measurements/20
|
|||
- **Certified by the host: settle `bit-exact`, sample `bit-exact`, under the set of that day.** The
|
||||
13 settle cases (the one with fields of 2^40 included) and the 42 chains of the `chains` cases,
|
||||
bit for bit, each answer also held to the job by `check_settle` and `check_chain`. Under the set
|
||||
as it stands now, on the model, which the testbenches hold the logic to: every case the engine can
|
||||
hold, 21 settle cases and 227 chains, bit for bit, and 13 cases refused as beyond it, so
|
||||
`uncertified` (above).
|
||||
as it stands now, on the model, which the testbenches hold the logic to: every case within its
|
||||
declared capacity bit for bit, and so `bit-exact` within it (above).
|
||||
- **Cut points and draws:** the 650 `cells` cut points the host computes equal the set's; 29 of
|
||||
those cells drawn 100,000 times each on the device, every draw the reference's and every frequency
|
||||
within 4 standard errors.
|
||||
|
|
@ -385,11 +408,12 @@ with its couplings fixed in the logic.
|
|||
- **The port widths are the boot's.** The build assumes the processing system's master port and its
|
||||
high-performance port at 128 bits, as this board's boot sets them; the host reads
|
||||
both and refuses otherwise, and does not set them. Another boot image may need another build.
|
||||
- **Not certified under the current set.** 64 cells, sampled fields of ±524,287 and a cost of
|
||||
1,048,575, and a window of 8,192 fields a temperature: the set now holds cases past each, which a
|
||||
member may not decline, so both kernels come out `uncertified` (above). At temperature 0 large
|
||||
fields are mapped exactly; more cells would need the engine to tile its cells, wider sampling
|
||||
fields wider registers, and a wider spread of uncertain fields more than one window.
|
||||
- **Certified within a capacity, not beyond it.** 64 cells, sampled fields of ±524,287 and a cost of
|
||||
1,048,575, and a window of 8,192 fields a temperature: its grade holds within those, and the host
|
||||
runs it on no job past them. A host offered other members gives such a job to the next; this
|
||||
board's own host, with one member, refuses it, saying which number it is past. At temperature 0 large fields are mapped exactly; more cells
|
||||
would need the engine to tile its cells, wider sampling fields wider registers, and a wider spread
|
||||
of uncertain fields more than one window.
|
||||
- **The joint test's histogram is the reference's.** The board is bit-exact, so the test holds the
|
||||
shared kernel to exact enumeration on the board's outputs, not an independent sampler.
|
||||
- **A cell update takes four clocks**, and in sequential mode one of 64 cones is used at a time.
|
||||
|
|
|
|||
|
|
@ -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<P: Port>(m: &BoardMember<P>) -> Result<Bits, String> {
|
||||
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<P: Port>(m: &BoardMember<P>) -> Result<Bits, String> {
|
|||
}
|
||||
}
|
||||
}
|
||||
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<P: Port>(m: &BoardMember<P>) -> Result<Bits, String> {
|
|||
}
|
||||
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<P: Port>(m: &BoardMember<P>, scale: f64) -> Result<Report, String> {
|
|||
// the statistical battery, and the all-at-once control it is built to catch
|
||||
let held: Vec<bool> = 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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ fn on_board<P: Port>(cmd: &str, args: &[String], port: P, info: BoardInfo) {
|
|||
"agree" => {
|
||||
let t = m.earn().unwrap_or_else(|e| die(e));
|
||||
print!("{t}");
|
||||
println!("parity earned on this board: settle {}, sample {}", m.settle_parity.label(), m.sample_parity.label());
|
||||
println!("parity earned on this board, within its declared capacity: settle {}, sample {}", m.settle_parity.label(), m.sample_parity.label());
|
||||
let rep = conform::agree(&m, flag(args, "--seeds").unwrap_or(200)).unwrap_or_else(|e| die(e));
|
||||
finish(&rep)
|
||||
}
|
||||
|
|
@ -297,6 +297,20 @@ macro_rules! with_link {
|
|||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
// The host's audit demotions outlive a job (§7.3): they are kept in a file, so a board
|
||||
// the audit demoted is not certified again on its next job, here or after a restart.
|
||||
// The file is read when the board is certified, which is after its lock is taken; a
|
||||
// demotion is kept at once in a file of its own beside it and then merged under the
|
||||
// file's lock, waited for at most `member::STORE_LOCK_WAIT` while the board is held
|
||||
// (where that runs out, it stays in its own file, which every certification reads);
|
||||
// with no file to name (an empty variable names none), no kernel is graded bit-exact.
|
||||
if std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").is_none_or(|v| v.is_empty()) {
|
||||
use wai_quantum::quantum_landscape::member::{Demotions, keep_demotions};
|
||||
keep_demotions(match std::env::var_os("HOME").filter(|h| !h.is_empty()) {
|
||||
Some(home) => Demotions::open(Some(std::path::PathBuf::from(home).join(".local/state/wai-quantum-landscape-fpga/demotions.json"))),
|
||||
None => Demotions::broken("HOME is not set, so this host has no file to keep demotions in (set HOME, or WAI_LANDSCAPE_DEMOTIONS to a file)".into()),
|
||||
});
|
||||
}
|
||||
let cmd = args.first().map(|s| s.as_str()).unwrap_or_else(|| usage());
|
||||
match cmd {
|
||||
"selftest" => {
|
||||
|
|
|
|||
|
|
@ -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<Vec<Segment>, String> {
|
|||
let span = (hi - lo + 1) as u64;
|
||||
let wbits = 64 - (span - 1).leading_zeros().min(64);
|
||||
let wbits = if span <= 1 { 0 } else { wbits };
|
||||
// Fewer temperatures a table, down to one, before the window is past what it holds:
|
||||
// only a single temperature's uncertain fields can be past it, as CAPACITY declares.
|
||||
if wbits > MAX_WBITS && take > 1 {
|
||||
take = (take / 2).max(1);
|
||||
continue;
|
||||
}
|
||||
if wbits > MAX_WBITS {
|
||||
return Err(format!(
|
||||
"the fields whose draw is uncertain run from {lo} to {hi}, {span} of them, and the engine's table holds {TABLE} a temperature"
|
||||
|
|
@ -287,7 +307,7 @@ impl<P: Port> BoardMember<P> {
|
|||
};
|
||||
self.settle_parity = settle;
|
||||
self.sample_parity = sample;
|
||||
Ok(format!("certified by the host on the conformance set: settle {}, sample {}\n", settle.label(), sample.label()))
|
||||
Ok(format!("certified by the host on the conformance set, within its declared capacity: settle {}, sample {}. {}\n", settle.label(), sample.label(), CAPACITY.words(true).unwrap_or_default()))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -308,19 +328,16 @@ impl<P: Port> Member for BoardMember<P> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Declines only what `extensions/quantum-landscape.md` §5 lets a member decline: sampling a
|
||||
/// workload whose field bound is above 2^31 − 1. (Sweeps at 0 it takes at any field: the host
|
||||
/// maps them exactly, `lowered_settle`.) Everything else it takes, and what the engine cannot
|
||||
/// hold, more than 64 variables or sampling fields beyond its 20-bit registers, it then fails
|
||||
/// with a sentence: the spec does not let a member decline those.
|
||||
fn takes(&self, w: &Workload, sampling: bool) -> Result<(), String> {
|
||||
if sampling && w.field_bound() > MUST_TAKE_FIELD_BOUND {
|
||||
return Err(format!(
|
||||
"its fields reach {}, past 2^31 - 1, and the engine samples fields of at most ±{FIELD_MAX}",
|
||||
w.field_bound()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
/// The engine's capacity, [`CAPACITY`]: the host declines for it exactly the jobs past it.
|
||||
/// (Sweeps at 0 it takes at any field: the host maps them exactly, `lowered_settle`.)
|
||||
fn capacity(&self) -> Capacity {
|
||||
CAPACITY
|
||||
}
|
||||
|
||||
/// The engine the logic reports (its ID and version registers) and the board's
|
||||
/// name: a demotion is kept under it, so a new engine is certified afresh.
|
||||
fn fingerprint(&self) -> String {
|
||||
format!("engine {:#010x} {:#010x} on {}", ID_VALUE, VERSION_VALUE, self.info.name)
|
||||
}
|
||||
|
||||
fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result<Outcome, String> {
|
||||
|
|
@ -461,6 +478,111 @@ mod tests {
|
|||
assert_eq!(lowered_settle(&w), lowered(&w));
|
||||
}
|
||||
|
||||
/// The declared capacity is the engine's, exactly: every settle and chains vector and every
|
||||
/// battery workload the engine cannot hold is past the capacity, and every one it can is
|
||||
/// within it.
|
||||
#[test]
|
||||
fn the_declared_capacity_is_exactly_what_the_engine_holds() {
|
||||
use wai_quantum::quantum_landscape::conformance::{battery, chain_cases, settle_cases};
|
||||
let (mut within, mut past) = (0, 0);
|
||||
let mut agree = |name: &str, w: &Workload, temps: &[f64]| {
|
||||
let engine = beyond(w, temps);
|
||||
let declared = CAPACITY.admits(w, temps);
|
||||
assert_eq!(engine.is_some(), declared.is_err(), "{name}: the engine says {engine:?}, the capacity {declared:?}");
|
||||
if engine.is_some() { past += 1 } else { within += 1 }
|
||||
};
|
||||
for c in settle_cases() {
|
||||
agree(&c.name, &c.landscape.lower(), &[]);
|
||||
}
|
||||
for c in chain_cases() {
|
||||
agree(&c.name, &c.landscape.lower(), &c.temperatures.each());
|
||||
}
|
||||
for c in battery() {
|
||||
agree(c.name, &c.landscape.lower(), &c.temperatures);
|
||||
}
|
||||
assert!(within > 0 && past > 0, "{within} within, {past} past");
|
||||
// The vectors certification adds at the capacity's edges are all within it, and the engine holds each.
|
||||
let edges = CAPACITY.edges();
|
||||
for c in &edges.settle {
|
||||
assert_eq!(beyond(&c.landscape.lower(), &[]), None, "{}", c.name);
|
||||
}
|
||||
for c in &edges.chains {
|
||||
assert_eq!(beyond(&c.landscape.lower(), &c.temperatures.each()), None, "{}", c.name);
|
||||
}
|
||||
for c in &edges.battery {
|
||||
assert_eq!(beyond(&c.landscape.lower(), &c.temperatures), None, "{}", c.name);
|
||||
}
|
||||
assert!(edges.settle.len() + edges.chains.len() >= 6, "an edge for each number it declares");
|
||||
}
|
||||
|
||||
/// The review's probe (#678), kept: on 3,000 random workloads and schedules, cooling ones and
|
||||
/// lists with zeros inside and out of order, the declared capacity and the engine agree on
|
||||
/// every one, and on the model every one within it that is run gives the reference's bits.
|
||||
#[test]
|
||||
fn the_capacity_and_the_engine_agree_on_random_jobs() {
|
||||
use wai_quantum::quantum_landscape::sample::{anneal_with, chain_seed, schedule, start};
|
||||
let mut x: u64 = 0x0F96_A678;
|
||||
let mut next = move || {
|
||||
x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
x >> 17
|
||||
};
|
||||
let m = model();
|
||||
let (mut admitted, mut declined, mut ran) = (0, 0, 0);
|
||||
for trial in 0..3000u64 {
|
||||
let n = 2 + (next() % 63) as usize;
|
||||
let mut l = Landscape::new(n);
|
||||
let density = [2u64, 5, 20, 60][(next() % 4) as usize];
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
if next() % density == 0 {
|
||||
l.couple(i, j, if next() % 2 == 0 { 1 } else { -1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
let scale = [2i64, 40, 3000, 100_000, 524_287, 600_000][(next() % 6) as usize];
|
||||
for i in 0..n {
|
||||
if next() % 2 == 0 {
|
||||
l.set_field(i, (next() as i64 % (2 * scale + 1)) - scale);
|
||||
}
|
||||
}
|
||||
l.set_cost([0i64, 1, 8, 5000, 1_048_575][(next() % 5) as usize]);
|
||||
let w = l.lower();
|
||||
let temps: Vec<f64> = 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<Chain> = (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<Outcome> = chains.iter().map(|c| anneal_with(&w, &c.start, &temps, c.seed)).collect();
|
||||
assert_eq!(got, want, "trial {trial}: n={n}, {} sweeps", temps.len());
|
||||
}
|
||||
}
|
||||
eprintln!("random jobs: {admitted} admitted, {declined} declined, {ran} run on the model");
|
||||
assert!(admitted > 0 && declined > 0 && ran == 250, "{admitted} admitted, {declined} declined, {ran} run");
|
||||
}
|
||||
|
||||
/// The host certifies the engine bit-exact for both kernels within its declared capacity, on
|
||||
/// the register-level model the testbenches hold the logic to, against the set on main.
|
||||
#[test]
|
||||
fn the_model_is_certified_bit_exact_within_its_declared_capacity() {
|
||||
use wai_quantum::quantum_landscape::member::certify;
|
||||
let m = model();
|
||||
let c = certify(&m).unwrap();
|
||||
assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact));
|
||||
assert_eq!(c.capacity(), CAPACITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sampling_beyond_the_registers_is_refused() {
|
||||
let m = model();
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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⁻⁹` 以内;超过之后,其误差随它所报告的舍弃权重变化 |
|
||||
|
|
|
|||
|
|
@ -176,12 +176,13 @@ The kernels run on a GPU too, as members the host certifies and checks like any
|
|||
|
||||
| feature | member | lanes | parity it can earn |
|
||||
|---|---|---|---|
|
||||
| `quantum_landscape_webgpu` | `quantum_landscape::webgpu`: Metal, Vulkan or DX12 through WebGPU | 32-bit: it declines a workload whose field bound is above 2³¹ − 1 | bit-exact for both kernels; its coloured kernel, a colour class of cells at once, statistical at most |
|
||||
| `quantum_landscape_cuda` | `quantum_landscape::cuda`: an NVIDIA GPU, compiled at run time | 64-bit: it takes every workload | bit-exact for both kernels |
|
||||
| `quantum_landscape_metal` | `quantum_landscape::metal`: an Apple GPU, macOS | 64-bit: it takes every workload | bit-exact for both kernels |
|
||||
| `quantum_landscape_webgpu` | `quantum_landscape::webgpu`: Metal, Vulkan or DX12 through WebGPU | 32-bit: it declares a field bound of 2³¹ − 1, cells and couplings within a 32nd of a storage binding, and 4,194,304 entries of cut points a sweep | bit-exact for both kernels within that capacity; its coloured kernel, a colour class of cells at once, statistical at most |
|
||||
| `quantum_landscape_cuda` | `quantum_landscape::cuda`: an NVIDIA GPU, compiled at run time | 64-bit fields; it declares 4,194,304 entries of cut points a sweep, which no job reaches | bit-exact for both kernels within that capacity |
|
||||
| `quantum_landscape_metal` | `quantum_landscape::metal`: an Apple GPU, macOS | 64-bit fields; it declares 4,194,304 entries of cut points a sweep, which no job reaches | bit-exact for both kernels within that capacity |
|
||||
|
||||
A parity in this table is what a member can earn, not what any one device has: the host finds it
|
||||
on each device, every time it certifies one, and states it in every answer.
|
||||
on each device, every time it certifies one, within the capacity the member declares and at that
|
||||
capacity's edges, and states both in every answer.
|
||||
|
||||
The settle kernel runs one thread a cell and one dispatch a sweep. The sampling kernel runs one
|
||||
thread a chain, its cells in the reference's order, with the reference's draws and the cut points the
|
||||
|
|
@ -191,9 +192,9 @@ starts of a run and the seeds of a sweep, a comparison or a replay. A member run
|
|||
a dispatch and waits for each before it sends the next.
|
||||
|
||||
A workload's field bound is the largest field any state can give a cell, `max_i (|b_i| + deg_i)`.
|
||||
A member may decline a workload only where that is above 2³¹ − 1, and the host never runs it on one
|
||||
it declines. Placing a job, the host passes over a member whose kernel for the job is not certified,
|
||||
one that declines the workload, and one that fails the job or whose answer its checks refuse, and
|
||||
Each member declares its capacity (`Member::capacity`) and the host runs it on nothing past it.
|
||||
Placing a job, the host passes over a member whose kernel for the job is not certified, one whose
|
||||
capacity the workload does not fit, and one that fails the job or whose answer its checks refuse, and
|
||||
names each under `refusals`; offered last, the reference answers what the others did not.
|
||||
|
||||
```rust norun=account
|
||||
|
|
@ -228,3 +229,51 @@ the host checks every answer a member returns, and finds its parity by running i
|
|||
[the conformance set](../../quantum-conformance/landscape/vectors.json) (`member::certify`). A member
|
||||
says nothing of its own parity. [The extension](../../extensions/quantum-landscape.md#5-members) states
|
||||
what a member takes and returns, what the host checks, and what each parity takes.
|
||||
|
||||
A member declares what its kernels hold, once, as fixed numbers (`member::Member::capacity`, a
|
||||
[`capacity::Capacity`](../src/quantum_landscape/capacity.rs)): its cells and couplings; on each side
|
||||
of temperature 0, its largest field, field bound and cost; above 0, how many cut points a sweep it
|
||||
holds or how wide a span of uncertain fields; how many sweeps a run; and how many chains a call,
|
||||
which is a batch and not a limit: the host splits a job, and certification every vector, into calls
|
||||
of that many. The host declines for it exactly the jobs past those numbers (`Capacity::admits`, a
|
||||
function of the job and the numbers alone). It certifies it on every vector and battery workload
|
||||
within them, and on vectors it generates from them by rule (`Capacity::generate`): every cell
|
||||
count at and beside a power of two, every call size to 32 chains and each band of a wavefront, the longest row, fields at each
|
||||
lane's wrap points, a corner with every number at its most, a run past a 24-bit draw index, and
|
||||
workloads at exactly each number declared. It states the numbers in every answer: `member.capacity`,
|
||||
and a sentence in the label. A member that refuses or fails a case within its capacity loses the
|
||||
grade, and one that declares less than the floor (`capacity::floor`, what the conformance set's core
|
||||
cases need: 32 cells, fields to ±40, a cost of 8, a run of 5,000 sweeps and so on) is not certified
|
||||
at all.
|
||||
|
||||
Then the host audits it. For every answer from a kernel graded `bit-exact`, it runs again on the
|
||||
reference, in every call, the call's last chain and one chain of each seed's run in it, drawn afresh
|
||||
with a key the host makes at start, before the next call: on the job's own thread at the job's
|
||||
priority, and on as many of the audit's threads as are free, a quarter of the host's cores and 4 at
|
||||
most over the whole process, each at a low priority (`run::audit_threads`); and the
|
||||
reference's settle as far as a budget of 2²⁴ of the work the host asked for (the whole settle past
|
||||
that one job in 8). Each audited answer says what was checked (`audit`). At the first difference it
|
||||
demotes the member to `uncertified` for both kernels, answers that job from the reference with the
|
||||
member named under `refusals`, and every later answer says so (`member.demoted`). The demotion is
|
||||
kept under the member's id and fingerprint (`member::Demotions`, in the file
|
||||
`WAI_LANDSCAPE_DEMOTIONS` names, or a link's target: kept at once in a file of its own beside it and
|
||||
merged under a lock waited for at most 5 s; read, with every demotion in a file of its own, at every
|
||||
certification, which then rewrites the file as a demotion would), so certifying again does not
|
||||
clear it; where that file cannot be read or rewritten, has a second name, or is reached by a link in
|
||||
a sticky folder anyone may write, no kernel is graded `bit-exact`. A kernel graded `statistical` is
|
||||
placed only where a job asks for it (`"parity": "statistical"`), and its answers say what the grade
|
||||
rests on: the battery's workloads alone, which a fault at a count of cells they do not run, or a law
|
||||
near the exact one, can pass.
|
||||
|
||||
```rust norun=account
|
||||
use wai_quantum::quantum_landscape::capacity::{Capacity, Limits};
|
||||
|
||||
// A board of 64 cells whose sampling registers hold fields to ±524,287 and a cost to 1,048,575,
|
||||
// and whose table holds a window of 8,192 fields a temperature; at temperature 0, any field.
|
||||
let board = Capacity {
|
||||
cells: Some(64),
|
||||
above_zero: Limits { field: Some(524_287), field_bound: None, cost: Some(1_048_575) },
|
||||
uncertain_span: Some(8_192),
|
||||
..Capacity::NONE
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -128,6 +128,62 @@
|
|||
"uncertified"
|
||||
],
|
||||
"description": "What the member has earned for this kernel."
|
||||
},
|
||||
"demoted": {
|
||||
"description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.",
|
||||
"type": "string"
|
||||
},
|
||||
"capacity": {
|
||||
"description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cells": {
|
||||
"type": "integer"
|
||||
},
|
||||
"couplings": {
|
||||
"type": "integer"
|
||||
},
|
||||
"at_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"above_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table_entries": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uncertain_span": {
|
||||
"type": "integer"
|
||||
},
|
||||
"sweeps": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chains_per_call": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -651,6 +707,21 @@
|
|||
"type": "string",
|
||||
"description": "BLAKE3 of the protocol, the program, the seed, the plan, the state and its energy."
|
||||
},
|
||||
"audit": {
|
||||
"type": "object",
|
||||
"description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.",
|
||||
"required": [
|
||||
"checked"
|
||||
],
|
||||
"properties": {
|
||||
"checked": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"refusals": {
|
||||
"type": "array",
|
||||
"description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).",
|
||||
|
|
@ -754,6 +825,62 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"demoted": {
|
||||
"description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.",
|
||||
"type": "string"
|
||||
},
|
||||
"capacity": {
|
||||
"description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cells": {
|
||||
"type": "integer"
|
||||
},
|
||||
"couplings": {
|
||||
"type": "integer"
|
||||
},
|
||||
"at_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"above_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table_entries": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uncertain_span": {
|
||||
"type": "integer"
|
||||
},
|
||||
"sweeps": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chains_per_call": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -857,6 +984,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"type": "object",
|
||||
"description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.",
|
||||
"required": [
|
||||
"checked"
|
||||
],
|
||||
"properties": {
|
||||
"checked": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"refusals": {
|
||||
"type": "array",
|
||||
"description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).",
|
||||
|
|
@ -957,6 +1099,62 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"demoted": {
|
||||
"description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.",
|
||||
"type": "string"
|
||||
},
|
||||
"capacity": {
|
||||
"description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cells": {
|
||||
"type": "integer"
|
||||
},
|
||||
"couplings": {
|
||||
"type": "integer"
|
||||
},
|
||||
"at_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"above_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table_entries": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uncertain_span": {
|
||||
"type": "integer"
|
||||
},
|
||||
"sweeps": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chains_per_call": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -1047,6 +1245,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"type": "object",
|
||||
"description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.",
|
||||
"required": [
|
||||
"checked"
|
||||
],
|
||||
"properties": {
|
||||
"checked": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"refusals": {
|
||||
"type": "array",
|
||||
"description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).",
|
||||
|
|
@ -1116,6 +1329,62 @@
|
|||
"uncertified"
|
||||
],
|
||||
"description": "What the member has earned for this kernel."
|
||||
},
|
||||
"demoted": {
|
||||
"description": "Why the host holds the member at uncertified for both kernels (extensions/quantum-landscape.md §7.3): its audit found a kernel graded bit-exact giving other bits than the reference's, or the host cannot keep its demotions. Absent otherwise.",
|
||||
"type": "string"
|
||||
},
|
||||
"capacity": {
|
||||
"description": "The capacity the member declares, which its parity holds within and past which the host runs it on nothing (extensions/quantum-landscape.md §5); absent where none is declared, as from the reference.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cells": {
|
||||
"type": "integer"
|
||||
},
|
||||
"couplings": {
|
||||
"type": "integer"
|
||||
},
|
||||
"at_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"above_zero": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "integer"
|
||||
},
|
||||
"field_bound": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table_entries": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uncertain_span": {
|
||||
"type": "integer"
|
||||
},
|
||||
"sweeps": {
|
||||
"type": "integer"
|
||||
},
|
||||
"chains_per_call": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -1125,6 +1394,21 @@
|
|||
"lines": {
|
||||
"type": "string"
|
||||
},
|
||||
"audit": {
|
||||
"type": "object",
|
||||
"description": "What the host's audit ran again on the reference, for an answer from a kernel graded bit-exact (extensions/quantum-landscape.md §7.3): each run's seed with the chains of it checked, or how much of a settle; absent where nothing was audited.",
|
||||
"required": [
|
||||
"checked"
|
||||
],
|
||||
"properties": {
|
||||
"checked": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"refusals": {
|
||||
"type": "array",
|
||||
"description": "Members a placing host passed over before one answered, each with why (an uncertified kernel, a declined workload, a failed run or an answer the host's checks refused), in the order they were offered; absent where there was none (extensions/quantum-landscape.md §5).",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
1780
wai/wai-quantum/src/quantum_landscape/capacity.rs
Normal file
1780
wai/wai-quantum/src/quantum_landscape/capacity.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,10 +3,10 @@
|
|||
//!
|
||||
//! - `settle`: whole workloads, the state, the sweeps, the ending and the
|
||||
//! accumulates a settle kernel gives from a start. A member's settle kernel
|
||||
//! is `bit-exact` when it gives exactly every one it takes, and it takes
|
||||
//! every one it must ([`MUST_TAKE_FIELD_BOUND`]). They reach past one
|
||||
//! workgroup and past one batch of sweeps, pin the width of a field at 16, 32
|
||||
//! and 40 bits, and sit on both sides of the line a member may decline at.
|
||||
//! is `bit-exact` within its declared capacity ([`super::capacity`]) when it
|
||||
//! gives exactly every one within it. They reach past one workgroup and past
|
||||
//! one batch of sweeps, pin the width of a field at 16, 32 and 40 bits, and
|
||||
//! sit on both sides of a field bound of 2³¹ − 1.
|
||||
//! - `exp`, `schedule`, `cells`: the reference's exponential, its cooling
|
||||
//! schedule and its cut points, bit for bit, so that a text-only
|
||||
//! implementation that computes any of them another way is caught here.
|
||||
|
|
@ -25,8 +25,17 @@
|
|||
//! ignores its schedule, keeps only the end of it or mishandles the last sweep
|
||||
//! at 0, one whose table is wrong for large fields or costs, one that is
|
||||
//! wrong past the first four cells of a workload, and one whose fields are
|
||||
//! held in 16 or 32 bits. A member may decline a workload of it only where
|
||||
//! it may decline a vector.
|
||||
//! held in 16 or 32 bits. A member is held to every workload within its
|
||||
//! declared capacity.
|
||||
//!
|
||||
//! The core set is the part of these the set held before it grew to reach
|
||||
//! large jobs, and a long schedule across windows
|
||||
//! ([`core_settle_cases`], [`core_chain_cases`], [`core_battery`]): the least
|
||||
//! capacity that takes all of it is the floor a member must declare
|
||||
//! ([`super::capacity::floor`]). For each number a member declares below what a
|
||||
//! job can reach, certification adds vectors at that number
|
||||
//! ([`super::capacity::Capacity::edges`]), and it splits every vector's chains
|
||||
//! into calls of as many as the host would give the member.
|
||||
//! - `jobs`: whole requests, what their answers hold, refusals included: the
|
||||
//! worked examples and the rules a reader must follow.
|
||||
|
||||
|
|
@ -35,6 +44,7 @@ use super::energy::Landscape;
|
|||
use super::job;
|
||||
use super::member::{Chain, Member, check_chain, check_settle};
|
||||
use super::run::state_text;
|
||||
use super::capacity::Capacity;
|
||||
use super::sample::{anneal_with, chain_seed, cuts, out, schedule, start};
|
||||
use super::settle::{Outcome, state_hash};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -192,29 +202,51 @@ fn width_cases() -> Vec<(String, Landscape)> {
|
|||
]
|
||||
}
|
||||
|
||||
/// The widest field bound a member must take: every vector at or below it. The
|
||||
/// bound is [`super::settle::Workload::field_bound`], the largest field any
|
||||
/// state can give a cell, couplings included.
|
||||
pub const MUST_TAKE_FIELD_BOUND: i64 = i32::MAX as i64;
|
||||
|
||||
/// Whether a member declines a vector it may decline, before running it: one
|
||||
/// whose field bound is above [`MUST_TAKE_FIELD_BOUND`]. `None` when it takes
|
||||
/// the vector, `Some(true)` when it may skip it, `Some(false)` when it must not.
|
||||
fn declines(m: &dyn Member, w: &super::settle::Workload, sampling: bool) -> Option<bool> {
|
||||
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<SettleCase> {
|
||||
const CORE: [&str; 12] = [
|
||||
"recall/n=4",
|
||||
"recall/n=8",
|
||||
"recall/n=16",
|
||||
"recall/n=32",
|
||||
"two-cycle",
|
||||
"at-rest",
|
||||
"bound",
|
||||
"chain-arrives",
|
||||
"one-sweep-bound",
|
||||
"field-and-cost",
|
||||
"zero-and-minus-at-the-start",
|
||||
"two-cycle-with-fields",
|
||||
];
|
||||
settle_cases().into_iter().filter(|c| CORE.contains(&c.name.as_str())).collect()
|
||||
}
|
||||
|
||||
/// Whether a member's settle kernel gives every `settle` vector it takes
|
||||
/// exactly, and takes every one it must.
|
||||
/// Whether a member's settle kernel gives every `settle` vector within its
|
||||
/// declared capacity exactly, and every vector at that capacity's edges
|
||||
/// ([`Capacity::edges`]). One past it is not run.
|
||||
pub fn settle_vectors_hold(m: &dyn Member) -> bool {
|
||||
settle_cases().iter().all(|c| {
|
||||
settle_vectors_hold_within(m, &m.capacity())
|
||||
}
|
||||
|
||||
/// [`settle_vectors_hold`] against a capacity read once, as certification reads it.
|
||||
pub fn settle_vectors_hold_within(m: &dyn Member, cap: &Capacity) -> bool {
|
||||
let holds = |c: &SettleCase| {
|
||||
let w = c.landscape.lower();
|
||||
if let Some(may) = declines(m, &w, false) {
|
||||
return may;
|
||||
}
|
||||
let want = w.settle(&c.start, c.bound);
|
||||
m.settle(&w, &c.start, c.bound).is_ok_and(|o| check_settle(&o, &w, c.bound).is_ok() && o == want)
|
||||
})
|
||||
};
|
||||
if !settle_cases().iter().filter(|c| cap.admits_settle(&c.landscape.lower(), c.bound).is_ok()).all(holds) {
|
||||
return false;
|
||||
}
|
||||
let mut all = true;
|
||||
cap.generate(super::capacity::Want::SETTLE, &mut |g| {
|
||||
if let super::capacity::Generated::Settle(c) = g {
|
||||
all = holds(&c);
|
||||
}
|
||||
all
|
||||
});
|
||||
all
|
||||
}
|
||||
|
||||
fn settle_json() -> Vec<Value> {
|
||||
|
|
@ -326,6 +358,17 @@ impl ChainCase {
|
|||
/// 53 bits puts them on the right side of it.
|
||||
pub const DRAW_EDGES: [(&str, u64); 3] = [("the-carry-in-a-product", 1982), ("under-the-first-cut-by-its-low-word", 1_174_725), ("under-the-second-cut-by-its-low-word", 6_472_161)];
|
||||
|
||||
/// The chains vectors of the core set: the first fifteen, three landscapes
|
||||
/// cooling at five schedules, and [`LONG_SCHEDULE`], 5,000 sweeps on 32
|
||||
/// cells, which crosses a device's windows of sweeps, by table and by work,
|
||||
/// whatever capacity at or above the floor it declares.
|
||||
pub fn core_chain_cases() -> Vec<ChainCase> {
|
||||
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<ChainCase> {
|
||||
let mixed = landscape(8, &[(0, 1, -1), (1, 2, -1), (0, 2, -1), (3, 4, 1), (4, 5, 1), (3, 5, 1), (5, 6, -1), (3, 6, -1), (6, 7, 1)], &[], 1);
|
||||
let dense = stored(&pattern(0xBEEF, 16));
|
||||
|
|
@ -357,6 +400,22 @@ pub fn chain_cases() -> Vec<ChainCase> {
|
|||
out.push(ChainCase { name: "windows-by-their-table".into(), landscape: spread, temperatures: held(&[(400.0, 70), (0.0, 2), (250.0, 70), (150.0, 60)]), seed: 9, restarts: 4 });
|
||||
// Windows of sweeps, by work: about 170 sweeps a window, couplings of both signs.
|
||||
out.push(ChainCase { name: "windows-by-their-work".into(), landscape: random(1024, 4, 0x71D0), temperatures: held(&[(3.0, 200), (1.5, 200)]), seed: 10, restarts: 2 });
|
||||
// Windows of sweeps within the floor, in the core set, so that no capacity a member may
|
||||
// declare leaves it untested across its windows: 32 cells each coupled to 15 others
|
||||
// (at offsets 1 to 7, and 16), 31 entries a sweep, held hot for 5,000
|
||||
// sweeps. About 530 sweeps a window by table, and 1,930 by work.
|
||||
let mut circle = Landscape::new(32);
|
||||
for i in 0..32 {
|
||||
for k in 1..=7usize {
|
||||
circle.couple(i, (i + k) % 32, if k % 2 == 1 { 1 } else { -1 });
|
||||
}
|
||||
if i < 16 {
|
||||
circle.couple(i, i + 16, -1);
|
||||
}
|
||||
}
|
||||
out.push(ChainCase { name: LONG_SCHEDULE.into(), landscape: circle, temperatures: held(&[(20.0, 5000)]), seed: 13, restarts: 4 });
|
||||
// A window by work on the smallest workload: 200,000 sweeps of two cells.
|
||||
out.push(ChainCase { name: "a-schedule-across-a-work-window".into(), landscape: landscape(2, &[(0, 1, 1)], &[(0, 1)], 0), temperatures: held(&[(1.5, 200_000)]), seed: 14, restarts: 2 });
|
||||
// Fields only their full width tells apart: at temperature 0, and at a temperature
|
||||
// of their own size, so that every cell's field is in its draw.
|
||||
for (name, l) in width_cases() {
|
||||
|
|
@ -370,16 +429,46 @@ pub fn chain_cases() -> Vec<ChainCase> {
|
|||
/// Whether a member's sampling kernel gives every `chains` vector it takes
|
||||
/// exactly, every case in one call, and takes every one it must.
|
||||
pub fn chain_vectors_hold(m: &dyn Member) -> bool {
|
||||
chain_cases().iter().all(|c| {
|
||||
chain_vectors_hold_within(m, &m.capacity())
|
||||
}
|
||||
|
||||
/// [`chain_vectors_hold`] against a capacity read once: every `chains` vector
|
||||
/// within it and every vector at its edges, each case's chains in calls of as
|
||||
/// many as the host would give the member at run time, so that a vector is
|
||||
/// split as a job is and never left out for it.
|
||||
pub fn chain_vectors_hold_within(m: &dyn Member, cap: &Capacity) -> bool {
|
||||
let fixed = chain_cases().into_iter().filter(|c| cap.admits(&c.landscape.lower(), &c.temperatures.each()).is_ok());
|
||||
if !fixed.into_iter().all(|c| chain_holds(m, cap, &c)) {
|
||||
return false;
|
||||
}
|
||||
let mut all = true;
|
||||
cap.generate(super::capacity::Want::CHAINS, &mut |g| {
|
||||
if let super::capacity::Generated::Chains(c) = g {
|
||||
all = chain_holds(m, cap, &c);
|
||||
}
|
||||
all
|
||||
});
|
||||
all
|
||||
}
|
||||
|
||||
/// Whether a member gives one `chains` case's every chain exactly, in calls of
|
||||
/// as many as the host would give it.
|
||||
fn chain_holds(m: &dyn Member, cap: &Capacity, c: &ChainCase) -> bool {
|
||||
{
|
||||
let w = c.landscape.lower();
|
||||
let temperatures = c.temperatures.each();
|
||||
if let Some(may) = declines(m, &w, temperatures.iter().any(|&t| t > 0.0)) {
|
||||
return may;
|
||||
}
|
||||
let chains = c.chains();
|
||||
let batch = cap.batch((super::CHUNK_CELLS / w.n().max(1)).clamp(1, super::CHUNK_CHAINS));
|
||||
let want: Vec<Outcome> = chains.iter().map(|ch| anneal_with(&w, &ch.start, &temperatures, ch.seed)).collect();
|
||||
m.chains(&w, &chains, &temperatures).is_ok_and(|got| got.len() == want.len() && got.iter().all(|o| check_chain(o, &w, &temperatures).is_ok()) && got == want)
|
||||
})
|
||||
let mut got = Vec::with_capacity(chains.len());
|
||||
for call in chains.chunks(batch) {
|
||||
match m.chains(&w, call, &temperatures) {
|
||||
Ok(o) if o.len() == call.len() && o.iter().all(|o| check_chain(o, &w, &temperatures).is_ok()) => got.extend(o),
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
got == want
|
||||
}
|
||||
}
|
||||
|
||||
fn chains_json() -> Vec<Value> {
|
||||
|
|
@ -451,12 +540,18 @@ pub struct BatteryCase {
|
|||
pub temperatures: Vec<f64>,
|
||||
pub law: Law,
|
||||
pub block: usize,
|
||||
/// The first cell of each block whose final states are counted; empty, every
|
||||
/// block at a multiple of `block`. A workload of many blocks reads a few, so
|
||||
/// that so many tests at once do not fail a correct sampler by chance; of
|
||||
/// alike cells, none coupled, a block may start at any cell.
|
||||
pub read: Vec<usize>,
|
||||
}
|
||||
|
||||
impl BatteryCase {
|
||||
fn of(name: &'static str, landscape: Landscape, temperatures: Vec<f64>) -> 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<f64>) -> BatteryCase {
|
||||
let block = landscape.len();
|
||||
BatteryCase { name, landscape, temperatures, law: Law::Settled, block }
|
||||
BatteryCase { name, landscape, temperatures, law: Law::Settled, block, read: Vec::new() }
|
||||
}
|
||||
|
||||
/// The temperature the schedule settles at: its last sweep above 0.
|
||||
|
|
@ -537,7 +632,12 @@ impl BatteryCase {
|
|||
}
|
||||
|
||||
pub fn chains(&self, k: usize) -> Vec<Chain> {
|
||||
(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<Chain> {
|
||||
(from..to)
|
||||
.map(|r| {
|
||||
let c = chain_seed(BATTERY_SEED + k as u64, r);
|
||||
Chain { start: start(c, self.landscape.len()), seed: c }
|
||||
|
|
@ -560,6 +660,11 @@ fn index_of(s: &[i8]) -> usize {
|
|||
s.iter().rev().fold(0, |acc, &v| acc * 3 + (v + 1) as usize)
|
||||
}
|
||||
|
||||
/// The battery's core: its first eight workloads, of two to four cells.
|
||||
pub fn core_battery() -> Vec<BatteryCase> {
|
||||
battery().into_iter().take(8).collect()
|
||||
}
|
||||
|
||||
pub fn battery() -> Vec<BatteryCase> {
|
||||
let constant = |t: f64, sweeps: usize| vec![t; sweeps];
|
||||
let mut cooling = vec![6.0; 8];
|
||||
|
|
@ -595,8 +700,8 @@ pub fn battery() -> Vec<BatteryCase> {
|
|||
BatteryCase::of("ending-at-zero", landscape(4, &[(0, 1, 1), (1, 2, 1), (2, 3, -1)], &[(0, 1)], 1), quench),
|
||||
BatteryCase::of("cooling", landscape(3, &[(0, 1, -1), (1, 2, -1), (0, 2, -1)], &[(2, -2)], 0), cooling),
|
||||
BatteryCase::of("a-ring-of-six", ring, constant(1.1, 32)),
|
||||
BatteryCase { name: "triangles-side-by-side", landscape: triangles, temperatures: constant(1.2, 32), law: Law::Settled, block: side },
|
||||
BatteryCase { name: "a-schedule-that-matters", landscape: alike, temperatures: two_stages, law: Law::Walked, block: 3 },
|
||||
BatteryCase { name: "triangles-side-by-side", landscape: triangles, temperatures: constant(1.2, 32), law: Law::Settled, block: side, read: Vec::new() },
|
||||
BatteryCase { name: "a-schedule-that-matters", landscape: alike, temperatures: two_stages, law: Law::Walked, block: 3, read: Vec::new() },
|
||||
// Fields that only their full width tells apart, at a temperature of their own size:
|
||||
// past 16 bits, which every member takes, and past 32, which a member may decline.
|
||||
BatteryCase::of("fields-past-16-bits", landscape(3, &[(0, 1, 1), (1, 2, -1)], &[(0, 70_000), (1, -65_536), (2, 40_000)], 30_000), constant(40_000.0, 32)),
|
||||
|
|
@ -609,12 +714,13 @@ pub fn battery() -> Vec<BatteryCase> {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BatteryResult {
|
||||
pub name: &'static str,
|
||||
/// The statistic of the block furthest from its law: Pearson's for the fixed
|
||||
/// battery, the likelihood ratio for a workload a capacity adds.
|
||||
pub chi_square: f64,
|
||||
pub degrees: usize,
|
||||
pub critical: f64,
|
||||
pub passed: bool,
|
||||
/// The member declined the workload, which it may where the field bound is above
|
||||
/// [`MUST_TAKE_FIELD_BOUND`], as for a vector; it then passes without running.
|
||||
/// The workload is past the member's declared capacity: it passes without running.
|
||||
pub declined: bool,
|
||||
}
|
||||
|
||||
|
|
@ -663,37 +769,100 @@ fn binned(law: Law, n: usize, by_state: &[f64]) -> Vec<f64> {
|
|||
/// 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<BatteryResult> {
|
||||
battery()
|
||||
run_battery_within(m, &m.capacity())
|
||||
}
|
||||
|
||||
/// The most chance that a correct sampler fails one generated battery workload,
|
||||
/// whatever its draws (§7.2): each block is held at the likelihood-ratio
|
||||
/// statistic this chance, shared among the workload's blocks, bounds by the
|
||||
/// method of types ([`likelihood_ratio_critical`]); so the few workloads a
|
||||
/// capacity adds fail a correct sampler less than once in a million between them.
|
||||
pub const GENERATED_FALSE: f64 = 1e-8;
|
||||
|
||||
/// The likelihood-ratio statistic `G = 2 Σ O ln(O / E)` of `counts` against the
|
||||
/// law `expected` over `total` draws, no bin pooled: `2·total` times the
|
||||
/// divergence of the counts from the law. Infinite where a count falls in a bin
|
||||
/// the law gives no chance.
|
||||
pub fn likelihood_ratio(counts: &[u64], expected: &[f64], total: u64) -> f64 {
|
||||
let n = total as f64;
|
||||
counts
|
||||
.iter()
|
||||
.zip(expected)
|
||||
.map(|(&o, &p)| match (o, p > 0.0) {
|
||||
(0, _) => 0.0,
|
||||
(_, false) => f64::INFINITY,
|
||||
(o, true) => 2.0 * o as f64 * (o as f64 / (n * p)).ln(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The value of [`likelihood_ratio`] over `bins` bins and `total` draws that a
|
||||
/// correct sampler's reaches with chance at most `p`, whatever the law. By the
|
||||
/// method of types, the chance that the counts lie at a divergence `t` or more
|
||||
/// from the law is at most the number of ways `total` draws fall in `bins` bins,
|
||||
/// `C(total + bins − 1, bins − 1)`, times `e^(−total·t)`.
|
||||
pub fn likelihood_ratio_critical(bins: usize, total: u64, p: f64) -> f64 {
|
||||
let n = total as f64;
|
||||
let types: f64 = (1..bins).map(|i| ((n + i as f64) / i as f64).ln()).sum();
|
||||
2.0 * (types - p.ln())
|
||||
}
|
||||
|
||||
/// [`run_battery`] against a capacity read once: every workload within it, and
|
||||
/// the workloads at its edges ([`Capacity::edges`]), each in calls of as many
|
||||
/// chains as the host would give the member.
|
||||
pub fn run_battery_within(m: &dyn Member, cap: &Capacity) -> Vec<BatteryResult> {
|
||||
let fixed = battery().len();
|
||||
// The fixed workloads in calls of as many chains as the host gives; the ones a
|
||||
// capacity adds in calls of 63, 62 and on down to 17 chains in turn (no more than
|
||||
// the host gives), so that their calls end in every partial warp of more than 16
|
||||
// chains and every partial workgroup of more than 32.
|
||||
let whole = cap.batch(4096);
|
||||
let sizes: Vec<usize> = (17..=63).rev().map(|k| cap.batch(k).max(1)).collect();
|
||||
battery()
|
||||
.into_iter()
|
||||
.chain(cap.edges_of(super::capacity::Want::BATTERY).battery)
|
||||
.enumerate()
|
||||
.map(|(k, case)| {
|
||||
let case = &case;
|
||||
let w = case.landscape.lower();
|
||||
if let Some(may) = declines(m, &w, true) {
|
||||
return BatteryResult { name: case.name, chi_square: 0.0, degrees: 0, critical: 0.0, passed: may, declined: true };
|
||||
if cap.admits(&w, &case.temperatures).is_err() {
|
||||
return BatteryResult { name: case.name, chi_square: 0.0, degrees: 0, critical: 0.0, passed: true, declined: true };
|
||||
}
|
||||
let exact = case.exact();
|
||||
let blocks = case.landscape.len() / case.block;
|
||||
let mut counts = vec![vec![0u64; exact.len()]; blocks];
|
||||
let blocks: Vec<usize> = if case.read.is_empty() { (0..case.landscape.len() / case.block).map(|b| b * case.block).collect() } else { case.read.clone() };
|
||||
let mut counts = vec![vec![0u64; exact.len()]; blocks.len()];
|
||||
let mut ok = true;
|
||||
for chunk in case.chains(k).chunks(4096) {
|
||||
match m.chains(&w, chunk, &case.temperatures) {
|
||||
let mut at = 0u64;
|
||||
let mut call = 0usize;
|
||||
while at < u64::from(BATTERY_CHAINS) {
|
||||
let per_call = if k < fixed { whole } else { sizes[call % sizes.len()] };
|
||||
call += 1;
|
||||
let to = (at + per_call as u64).min(u64::from(BATTERY_CHAINS));
|
||||
let chunk = case.chains_from(k, at, to);
|
||||
match m.chains(&w, &chunk, &case.temperatures) {
|
||||
Ok(got) if got.len() == chunk.len() && got.iter().all(|o| check_chain(o, &w, &case.temperatures).is_ok()) => {
|
||||
for o in got {
|
||||
for (b, c) in counts.iter_mut().enumerate() {
|
||||
c[index_of(&o.state[b * case.block..(b + 1) * case.block])] += 1;
|
||||
for (c, &b) in counts.iter_mut().zip(&blocks) {
|
||||
c[index_of(&o.state[b..b + case.block])] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => ok = false,
|
||||
}
|
||||
at = to;
|
||||
}
|
||||
let expected = binned(case.law, case.block, &exact);
|
||||
let generated = likelihood_ratio_critical(expected.len(), u64::from(BATTERY_CHAINS), GENERATED_FALSE / blocks.len().max(1) as f64);
|
||||
let worst = counts
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let as_real: Vec<f64> = c.iter().map(|&v| v as f64).collect();
|
||||
let got: Vec<u64> = binned(case.law, case.block, &as_real).iter().map(|&v| v as u64).collect();
|
||||
chi_square(&got, &expected, u64::from(BATTERY_CHAINS))
|
||||
if k < fixed {
|
||||
chi_square(&got, &expected, u64::from(BATTERY_CHAINS))
|
||||
} else {
|
||||
(likelihood_ratio(&got, &expected, u64::from(BATTERY_CHAINS)), expected.len() - 1, generated)
|
||||
}
|
||||
})
|
||||
.fold((0.0f64, 1usize, 0.0f64), |w, r| if r.0 - r.2 > w.0 - w.2 || w.2 == 0.0 { r } else { w });
|
||||
let (chi, df, critical) = worst;
|
||||
|
|
@ -795,6 +964,41 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// The bound a generated workload's blocks are held to holds exactly: over
|
||||
/// every way 1,000 draws fall in three bins, under laws with bins a pooled
|
||||
/// chi-square mishandles, a correct sampler's statistic reaches it with less
|
||||
/// chance than it is set at. And its values at the battery's size.
|
||||
#[test]
|
||||
fn the_likelihood_ratio_bound_holds_exactly() {
|
||||
let n = 1000u64;
|
||||
let ln_fact: Vec<f64> = (0..=n).scan(0.0f64, |acc, i| {
|
||||
if i > 0 {
|
||||
*acc += (i as f64).ln();
|
||||
}
|
||||
Some(*acc)
|
||||
}).collect();
|
||||
for law in [[0.665, 0.245, 0.090], [0.9897, 0.0101, 0.0002], [1.0 / 3.0; 3]] {
|
||||
for p in [1e-2, 1e-4, 1e-8] {
|
||||
let critical = likelihood_ratio_critical(3, n, p);
|
||||
let mut tail = 0.0f64;
|
||||
for a in 0..=n {
|
||||
for b in 0..=(n - a) {
|
||||
let c = n - a - b;
|
||||
if likelihood_ratio(&[a, b, c], &law, n) >= critical {
|
||||
let ln = ln_fact[n as usize] - ln_fact[a as usize] - ln_fact[b as usize] - ln_fact[c as usize] + a as f64 * law[0].ln() + b as f64 * law[1].ln() + c as f64 * law[2].ln();
|
||||
tail += ln.exp();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(tail <= p, "{law:?} at {p}: {tail}");
|
||||
}
|
||||
}
|
||||
let at = |bins: usize, p: f64| likelihood_ratio_critical(bins, u64::from(BATTERY_CHAINS), p);
|
||||
assert!((at(3, 1e-8) - 75.0).abs() < 1.0 && (at(10, 1e-8) - 189.5).abs() < 1.0 && (at(27, 1e-8) - 429.3).abs() < 1.0, "{} {} {}", at(3, 1e-8), at(10, 1e-8), at(27, 1e-8));
|
||||
assert_eq!(likelihood_ratio(&[3, 0], &[1.0, 0.0], 3), 0.0);
|
||||
assert_eq!(likelihood_ratio(&[2, 1], &[1.0, 0.0], 3), f64::INFINITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_state_s_index_is_its_digits() {
|
||||
for k in 0..81 {
|
||||
|
|
|
|||
|
|
@ -286,8 +286,9 @@ impl Member for CudaLandscape {
|
|||
device::label("gpu", &self.name, "CUDA", "its cells in the reference's order")
|
||||
}
|
||||
|
||||
fn takes(&self, w: &Workload, sampling: bool) -> Result<(), String> {
|
||||
if sampling { device::table_fits(&device::reach(w)) } else { Ok(()) }
|
||||
/// 64-bit fields: no limit but one sweep's cut points, which no job reaches.
|
||||
fn capacity(&self) -> super::capacity::Capacity {
|
||||
super::capacity::Capacity { table_entries: Some(device::MAX_TABLE_ENTRIES), ..super::capacity::Capacity::NONE }
|
||||
}
|
||||
|
||||
fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result<Outcome, String> {
|
||||
|
|
|
|||
|
|
@ -101,10 +101,17 @@ pub struct Prepared {
|
|||
temperature: Option<f64>,
|
||||
seeds: u32,
|
||||
temperatures: Vec<f64>,
|
||||
/// Whether any sweep of the job is above temperature 0.
|
||||
pub sampling: bool,
|
||||
/// Every schedule of chains the job runs a member on, a temperature a
|
||||
/// sweep. A member's capacity is held to each.
|
||||
pub schedules: Vec<Vec<f64>>,
|
||||
/// The bound of every settle the job runs a member on.
|
||||
pub settles: Vec<u32>,
|
||||
/// The kernels the job runs.
|
||||
pub kernels: Vec<&'static str>,
|
||||
/// Whether the job takes a kernel graded `statistical` (`"parity":
|
||||
/// "statistical"`): placed, a job is otherwise run only where a kernel gives
|
||||
/// the reference's bits (§5.1).
|
||||
pub statistical: bool,
|
||||
}
|
||||
|
||||
/// Read a request and check every bound of its job before any of it runs:
|
||||
|
|
@ -122,6 +129,12 @@ pub fn prepare(op: &str, request: &Value, max_bytes: u64) -> Result<Option<Prepa
|
|||
return Err(Refusal::general(format!("`{op}` is not an operation here: run, sweep, compare, replay and examples are.")));
|
||||
}
|
||||
let source = request.get("program").and_then(Value::as_str).ok_or_else(|| Refusal::general("A job gives its program as text, under `program`."))?;
|
||||
// Absent and `null` alike are the default, as for every optional field.
|
||||
let statistical = match request.get("parity").filter(|v| !v.is_null()).map(|v| v.as_str()) {
|
||||
None | Some(Some("bit-exact")) => false,
|
||||
Some(Some("statistical")) => true,
|
||||
Some(_) => return Err(Refusal::general("A job's `parity` is `bit-exact`, the default, or `statistical`, which also takes a kernel graded statistical.")),
|
||||
};
|
||||
let p = read(source)?;
|
||||
let seed = seed_of(request)?;
|
||||
let temperature = temperature_of(request)?;
|
||||
|
|
@ -161,15 +174,46 @@ pub fn prepare(op: &str, request: &Value, max_bytes: u64) -> Result<Option<Prepa
|
|||
if bytes > most {
|
||||
return Err(Refusal::general(format!("This job could hold up to {} bytes, and this host allows {}.", super::price::group(bytes), super::price::group(most))));
|
||||
}
|
||||
let sampling = op == "compare" || runs.iter().any(|(plan, _)| plan.mode() == super::price::Mode::Sample);
|
||||
let (mut schedules, mut settles): (Vec<Vec<f64>>, Vec<u32>) = (Vec::new(), Vec::new());
|
||||
for (plan, _) in &runs {
|
||||
let mut each = match plan {
|
||||
Plan::Settle { sweeps } => {
|
||||
if !settles.contains(sweeps) {
|
||||
settles.push(*sweeps);
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
_ => vec![plan.temperatures()],
|
||||
};
|
||||
// A comparison also runs the emulator at temperature 0, with the sampling run's starts and sweeps.
|
||||
if op == "compare" {
|
||||
each.push(vec![0.0; plan.sweeps() as usize]);
|
||||
}
|
||||
for s in each {
|
||||
if !schedules.iter().any(|t| t.len() == s.len() && t.iter().zip(&s).all(|(a, b)| a.to_bits() == b.to_bits())) {
|
||||
schedules.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut kernels: Vec<&'static str> = runs.iter().map(|(plan, _)| plan.kernel()).collect();
|
||||
kernels.sort_unstable();
|
||||
kernels.dedup();
|
||||
Ok(Some(Prepared { job: Job::new(p), op: op.into(), seed, temperature, seeds, temperatures, sampling, kernels }))
|
||||
Ok(Some(Prepared { job: Job::new(p), op: op.into(), seed, temperature, seeds, temperatures, schedules, settles, kernels, statistical }))
|
||||
}
|
||||
|
||||
/// Answer a prepared job on a certified member.
|
||||
/// Answer a prepared job on a certified member, with what the host's audit
|
||||
/// checked of it (`audit`, §7.3) where it checked anything.
|
||||
pub fn answer(c: &Certified<'_>, prepared: &Prepared) -> Result<Value, Refusal> {
|
||||
c.take_audit();
|
||||
let mut a = answer_unaudited(c, prepared)?;
|
||||
let checked = c.take_audit();
|
||||
if !checked.is_empty() {
|
||||
a["audit"] = json!({ "checked": checked });
|
||||
}
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
fn answer_unaudited(c: &Certified<'_>, prepared: &Prepared) -> Result<Value, Refusal> {
|
||||
let Prepared { job, op, seed, temperature, seeds, temperatures, .. } = prepared;
|
||||
let (seed, temperature, seeds) = (*seed, *temperature, *seeds);
|
||||
match op.as_str() {
|
||||
|
|
@ -198,7 +242,7 @@ pub fn answer(c: &Certified<'_>, prepared: &Prepared) -> Result<Value, Refusal>
|
|||
));
|
||||
}
|
||||
}
|
||||
Ok(json!({ "protocol": PROTOCOL, "label": c.member().label(), "member": super::member::describe(c, plan.kernel()), "seeds": seeds, "lines": lines }))
|
||||
Ok(json!({ "protocol": PROTOCOL, "label": c.label(), "member": super::member::describe(c, plan.kernel()), "seeds": seeds, "lines": lines }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -209,14 +253,29 @@ pub fn answer(c: &Certified<'_>, prepared: &Prepared) -> Result<Value, Refusal>
|
|||
pub fn call_certified(c: &Certified<'_>, op: &str, request: &Value, max_bytes: u64) -> Result<Value, Refusal> {
|
||||
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<Value>) -> Result<Value, Refusal> {
|
||||
let mut a = answer(&Certified::reference(), p)?;
|
||||
a["refusals"] = Value::Array(refusals);
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
/// Answer a request on the first of `members` that can: one certified for
|
||||
/// every kernel the job runs (an uncertified kernel is never placed; a member
|
||||
/// asked for by name goes through [`call_certified`]), that takes the workload
|
||||
/// ([`super::member::Member::takes`]), and that gives an answer. Each member
|
||||
/// asked for by name goes through [`call_certified`]), within whose declared
|
||||
/// capacity the workload fits at every schedule the job runs
|
||||
/// ([`super::capacity::Capacity::admits`]), and that gives an answer. Each member
|
||||
/// passed over, for any of those, is named in the answer's `refusals` with why,
|
||||
/// and the next is asked. Put the CPU reference last, and a job that a member
|
||||
/// could not finish is still answered.
|
||||
|
|
@ -227,9 +286,12 @@ pub fn call_placed(members: &[Certified<'_>], op: &str, request: &Value, max_byt
|
|||
let m = c.member();
|
||||
let why = if let Some(k) = p.kernels.iter().find(|k| c.parity(k) == Parity::Uncertified) {
|
||||
format!("its kernel {k} is not certified")
|
||||
} else if let Err(why) = m.takes(&p.job.workload, p.sampling) {
|
||||
} else if let Some(k) = p.kernels.iter().find(|k| c.parity(k) == Parity::Statistical && !p.statistical) {
|
||||
format!("its kernel {k} is graded statistical, which the host does not audit, and the job asks for the reference's bits (\"parity\": \"statistical\" takes it)")
|
||||
} else if let Some(why) = p.settles.iter().find_map(|&b| c.capacity().admits_settle(&p.job.workload, b).err()).or_else(|| p.schedules.iter().find_map(|t| c.capacity().admits(&p.job.workload, t).err())) {
|
||||
why
|
||||
} else {
|
||||
let before = c.demoted().is_some();
|
||||
match answer(c, &p) {
|
||||
Ok(mut a) => {
|
||||
if !refusals.is_empty() {
|
||||
|
|
@ -237,6 +299,10 @@ pub fn call_placed(members: &[Certified<'_>], op: &str, request: &Value, max_byt
|
|||
}
|
||||
return Ok(a);
|
||||
}
|
||||
Err(r) if !before && c.demoted().is_some() => {
|
||||
refusals.push(json!({ "member": m.id(), "why": r.plain() }));
|
||||
return from_the_reference(&p, refusals);
|
||||
}
|
||||
Err(r) => r.plain(),
|
||||
}
|
||||
};
|
||||
|
|
@ -543,8 +609,8 @@ mod tests {
|
|||
assert_eq!(a["rows"][0]["mean_energy"].as_f64(), Some(want), "{}", a["rows"][0]);
|
||||
}
|
||||
|
||||
/// A stand-in member with narrow lanes: it declines a workload whose field
|
||||
/// bound is above `widest`, and runs the reference's kernels otherwise.
|
||||
/// A stand-in member with narrow lanes: it declares field bounds to `.0` on
|
||||
/// both sides of temperature 0, and runs the reference's kernels.
|
||||
struct Narrow(i64);
|
||||
|
||||
impl super::super::member::Member for Narrow {
|
||||
|
|
@ -563,8 +629,34 @@ mod tests {
|
|||
fn chains(&self, w: &super::super::settle::Workload, chains: &[super::super::member::Chain], temperatures: &[f64]) -> Result<Vec<super::super::settle::Outcome>, 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<super::super::settle::Outcome, String> {
|
||||
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<Vec<super::super::settle::Outcome>, String> {
|
||||
if w.field_bound() > 1000 { Err("not this one".into()) } else { super::super::member::CpuReference.chains(w, chains, temperatures) }
|
||||
}
|
||||
fn capacity(&self) -> super::super::capacity::Capacity {
|
||||
Narrow(i64::from(i32::MAX)).capacity()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -576,7 +668,7 @@ mod tests {
|
|||
let placed = [certify(&lanes32).unwrap(), Certified::reference()];
|
||||
let wide = call_placed(&placed, "run", &json!({ "program": "vars 2\nfield 0 1099511627776\nagree 0 1\nminimize\n" }), MAX_JOB_BYTES).unwrap();
|
||||
assert_eq!(wide["member"]["id"], "cpu:reference");
|
||||
assert_eq!(wide["refusals"], json!([{ "member": "fpga:narrow-2147483647", "why": "its fields reach 1099511627777, beyond 2147483647" }]));
|
||||
assert_eq!(wide["refusals"], json!([{ "member": "fpga:narrow-2147483647", "why": "past its declared capacity: its field bound is 1099511627777 at temperature 0, and it holds 2147483647" }]));
|
||||
let narrow = call_placed(&placed, "run", &json!({ "program": example("talks"), "seed": "3" }), MAX_JOB_BYTES).unwrap();
|
||||
assert_eq!(narrow["member"]["id"], "fpga:narrow-2147483647");
|
||||
assert!(narrow.get("refusals").is_none());
|
||||
|
|
@ -585,17 +677,32 @@ mod tests {
|
|||
assert!(none.message.starts_with("No member offered answered this job"), "{}", none.message);
|
||||
}
|
||||
|
||||
/// A member may decline only the vectors whose fields do not fit 32 bits:
|
||||
/// declining one that does is failing it.
|
||||
/// A member is held to every vector within the capacity it declares, and
|
||||
/// its grade holds within that capacity, which every answer states. One
|
||||
/// that refuses a vector within its capacity loses the grade; one that
|
||||
/// declares a capacity under the floor is not certified at all.
|
||||
#[test]
|
||||
fn a_member_may_decline_only_what_does_not_fit_32_bits() {
|
||||
fn a_member_is_held_to_every_vector_within_its_declared_capacity() {
|
||||
use super::super::member::{KERNEL_SAMPLE, KERNEL_SETTLE, Parity, certify};
|
||||
let lanes32 = Narrow(i64::from(i32::MAX));
|
||||
let c = certify(&lanes32).unwrap();
|
||||
assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact));
|
||||
let lanes8 = Narrow(10);
|
||||
let c = certify(&lanes8).unwrap();
|
||||
assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::Uncertified, Parity::Uncertified), "it declines vectors whose fields fit 32 bits");
|
||||
// At the floor itself: held to the core set and to whatever else fits, and certified.
|
||||
let at_floor = Narrow(41);
|
||||
let c = certify(&at_floor).unwrap();
|
||||
assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact));
|
||||
let a = call_certified(&c, "run", &json!({ "program": example("talks"), "seed": "3" }), MAX_JOB_BYTES).unwrap();
|
||||
assert_eq!(a["member"]["capacity"], json!({ "at_zero": { "field_bound": 41 }, "above_zero": { "field_bound": 41 } }));
|
||||
assert!(a["label"].as_str().unwrap().ends_with("Its parity holds within the capacity it declares, and the host runs it on no job past it: at temperature 0, a field bound to 41; above temperature 0, a field bound to 41."), "{}", a["label"]);
|
||||
// Under the floor: not certified, saying which number.
|
||||
let under = Narrow(40);
|
||||
let e = certify(&under).err().unwrap();
|
||||
assert!(e.contains("under the floor") && e.contains("a field bound at temperature 0 40, and the floor is 41"), "{e}");
|
||||
// Declaring 2³¹ − 1 and refusing what it would rather not run: the vectors within it are failed.
|
||||
let c = certify(&Choosy).unwrap();
|
||||
assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::Uncertified, Parity::Uncertified));
|
||||
// The reference declares none, and its answers carry no capacity: the same bytes as before.
|
||||
assert!(call("run", &json!({ "program": example("talks") })).unwrap()["member"].get("capacity").is_none());
|
||||
}
|
||||
|
||||
/// A member whose chains come back where they started: its settle kernel is the
|
||||
|
|
@ -682,7 +789,7 @@ mod tests {
|
|||
for op in ["run", "replay", "sweep", "compare"] {
|
||||
for program in ["vars 3\nfield 0 4294967295\nagree 0 1\nagree 0 2\nminimize steps=1\n", "vars 3\nfield 0 4294967295\nagree 0 1\nsolve\n"] {
|
||||
let r = call_certified(&c, op, &json!({ "program": program, "seeds": 2 }), MAX_JOB_BYTES).unwrap_err();
|
||||
assert!(r.message.contains("fpga:narrow-2147483647") && r.message.contains("beyond 2147483647"), "{op}: {}", r.message);
|
||||
assert!(r.message.contains("fpga:narrow-2147483647") && r.message.contains("past its declared capacity"), "{op}: {}", r.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,22 +17,43 @@
|
|||
//! conformance set and grants what they pass:
|
||||
//! - [`Parity::Reference`]: only this crate's own CPU kernels, through
|
||||
//! [`Certified::reference`]; no member can be granted it.
|
||||
//! - [`Parity::BitExact`]: its settle kernel gives every `settle` vector it
|
||||
//! takes bit for bit, or its sampling kernel every `chains` vector it takes,
|
||||
//! and it takes every one it must ([`conformance::MUST_TAKE_FIELD_BOUND`]).
|
||||
//! - [`Parity::BitExact`]: its settle kernel gives every `settle` vector
|
||||
//! within its declared capacity bit for bit, or its sampling kernel every
|
||||
//! `chains` vector within it.
|
||||
//! - [`Parity::Statistical`]: a sampling kernel with draws of its own whose
|
||||
//! final states, over the battery of `statistical` workloads, follow the
|
||||
//! exact distribution by the test that battery states.
|
||||
//! - [`Parity::Uncertified`]: what passed neither. An answer can still come
|
||||
//! from it when it is asked for by name ([`named`]), and says so.
|
||||
//!
|
||||
//! A member declares its capacity, the fixed numbers its kernels hold
|
||||
//! ([`Member::capacity`], [`super::capacity`]). The host runs it on no job past
|
||||
//! those numbers, holds it to every vector within them, and states them in
|
||||
//! every answer: a parity holds within the capacity it was found in. A
|
||||
//! capacity under the floor ([`super::capacity::floor`]) is not certified.
|
||||
//!
|
||||
//! A member says of itself what kind of run it is ([`Member::label`]) and may
|
||||
//! read a meter ([`Member::meter`]); a reading stands beside the priced figures
|
||||
//! and does not replace them.
|
||||
|
||||
use super::capacity::{Capacity, floor};
|
||||
use super::conformance;
|
||||
use super::settle::{Outcome, Term, Workload};
|
||||
use serde_json::{Value, json};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
/// The host's audit of a kernel graded `bit-exact` (§7.3): a settle whose work,
|
||||
/// the sweeps the host asked for times its couplings and cells, is at most this
|
||||
/// is run again on the reference whole, every time; past it, the reference's
|
||||
/// settle as far as this much work goes.
|
||||
pub const AUDIT_SETTLE_WORK: u64 = 1 << 24;
|
||||
|
||||
/// Past [`AUDIT_SETTLE_WORK`], one settle in this many is run again whole,
|
||||
/// drawn afresh with the host's audit key on every job.
|
||||
pub const AUDIT_SETTLE_ONE_IN: u64 = 8;
|
||||
|
||||
/// The deterministic kernel's name.
|
||||
pub const KERNEL_SETTLE: &str = "wai.quantum.landscape/settle";
|
||||
|
|
@ -127,16 +148,622 @@ pub trait Member {
|
|||
fn meter(&self) -> Option<Metered> {
|
||||
None
|
||||
}
|
||||
/// Whether it takes a workload, said before anything runs: the reason
|
||||
/// where it does not, and the host then gives the job to the next member it
|
||||
/// was offered. `sampling` is whether any sweep is above temperature 0.
|
||||
/// A member declines only what `extensions/quantum-landscape.md` §5 lets it,
|
||||
/// and the host never runs it on what it declines.
|
||||
fn takes(&self, _w: &Workload, _sampling: bool) -> Result<(), String> {
|
||||
/// The fixed numbers its kernels hold, declared up front and the same for
|
||||
/// every job (`extensions/quantum-landscape.md` §5). The host declines for
|
||||
/// it exactly the jobs past them ([`Capacity::admits`]) and runs it on no
|
||||
/// other; it holds the member to every vector within them. None declared is
|
||||
/// no limit, the reference's.
|
||||
fn capacity(&self) -> Capacity {
|
||||
Capacity::NONE
|
||||
}
|
||||
/// What the member runs, beyond its id: a bitstream's or a firmware's digest,
|
||||
/// a driver's version. With its id it is the identity a demotion is kept
|
||||
/// under (§7.3), so a member that runs something new is certified afresh, and
|
||||
/// one that runs the same is not. None by default.
|
||||
fn fingerprint(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// How long the store waits for its file's lock. Past it, the certification
|
||||
/// that waited grades no kernel bit-exact, and says why; the store is not broken
|
||||
/// for good, and the next certification waits again. A demotion does not wait on
|
||||
/// it to be kept: it is kept at once in a file of its own, and only merging it into
|
||||
/// the store's file waits this long ([`Demotions::record`]). A board job waits no
|
||||
/// longer than this on the store while it holds the board.
|
||||
pub const STORE_LOCK_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// The most bytes of a demotion's reason the store keeps on file; a longer one is
|
||||
/// cut at a character's edge. With it bounded, a certification knows how large the
|
||||
/// store's next rewrite can be before it grades a member.
|
||||
pub const STORE_REASON_BYTES: usize = 1024;
|
||||
|
||||
/// How long one of the store's temporary files must have gone unchanged, with no
|
||||
/// writer holding it, before the store takes it for a killed writer's and removes it.
|
||||
pub const STORE_STALE_AFTER: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// What the host remembers of the members its audit demoted (§7.3), keyed by
|
||||
/// identity (the member's id and fingerprint), and kept in a file where one is
|
||||
/// named, so that a demotion outlives the process that made it. A demoted
|
||||
/// identity is certified `uncertified` for both kernels whatever capacity it
|
||||
/// declares; only another identity is certified afresh.
|
||||
///
|
||||
/// A store with a file `<name>` keeps its demotions there and in files of their
|
||||
/// own beside it, `.<name>.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 `<name>.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 (`.<name>.<write|probe|demotion>.<process>.<count>.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<PathBuf>,
|
||||
/// Every demotion this process has read or made: none is forgotten here.
|
||||
records: Mutex<BTreeMap<String, Value>>,
|
||||
/// Why the store cannot keep demotions, once it cannot.
|
||||
broken: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
/// Why the store's lock could not be had: for good, or for now.
|
||||
enum Unlocked {
|
||||
Fault(String),
|
||||
Busy(String),
|
||||
}
|
||||
|
||||
/// What a file beside a store is to it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Beside {
|
||||
/// One of its temporary files: a rewrite, a probe or a demotion being written.
|
||||
Temporary,
|
||||
/// A demotion kept in a file of its own.
|
||||
Demotion,
|
||||
}
|
||||
|
||||
/// What the file `file` in a store's folder is to the store whose file is `name`:
|
||||
/// `.<name>.<write|probe|demotion>.<digits>.<digits>.tmp`, or
|
||||
/// `.<name>.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
|
||||
/// `<name>.<more>` has more dots after `.<name>.` than either form allows.
|
||||
fn beside(name: &str, file: &str) -> Option<Beside> {
|
||||
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::<Vec<_>>()[..] {
|
||||
["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<std::fs::File> {
|
||||
#[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<usize> {
|
||||
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<u8>,
|
||||
records: BTreeMap<String, Value>,
|
||||
mode: Option<std::fs::Permissions>,
|
||||
}
|
||||
|
||||
impl Demotions {
|
||||
/// A store kept in `path`, or in memory alone with none (for this process). A
|
||||
/// path that names no file is a broken store.
|
||||
pub fn open(path: Option<PathBuf>) -> Demotions {
|
||||
if let Some(p) = &path
|
||||
&& (p.as_os_str().is_empty() || p.file_name().is_none())
|
||||
{
|
||||
return Demotions::broken(format!("{:?} names no file to keep demotions in", p.display().to_string()));
|
||||
}
|
||||
Demotions { path, records: Mutex::new(BTreeMap::new()), broken: Mutex::new(None) }
|
||||
}
|
||||
|
||||
/// A store that cannot keep demotions, saying why: for a host that keeps them
|
||||
/// in a file and cannot name one.
|
||||
pub fn broken(why: String) -> Demotions {
|
||||
let d = Demotions { path: None, records: Mutex::new(BTreeMap::new()), broken: Mutex::new(None) };
|
||||
d.fail(why);
|
||||
d
|
||||
}
|
||||
|
||||
/// Why the store cannot keep demotions, if it cannot.
|
||||
pub fn fault(&self) -> Option<String> {
|
||||
self.broken.lock().map_or_else(|_| Some("the store's lock was poisoned".into()), |b| b.clone())
|
||||
}
|
||||
|
||||
fn fail(&self, why: String) -> String {
|
||||
if let Ok(mut b) = self.broken.lock()
|
||||
&& b.is_none()
|
||||
{
|
||||
eprintln!("wai-quantum landscape: the host cannot keep its demotions, and grades no kernel bit-exact: {why}");
|
||||
*b = Some(why.clone());
|
||||
}
|
||||
why
|
||||
}
|
||||
|
||||
fn unlocked(&self, e: Unlocked) -> String {
|
||||
match e {
|
||||
Unlocked::Fault(e) => self.fail(e),
|
||||
Unlocked::Busy(e) => {
|
||||
eprintln!("wai-quantum landscape: {e}, so this certification grades no kernel bit-exact");
|
||||
e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The file the store's path names: a link's target, followed link by link,
|
||||
/// never the link, so that its lock, its own files and its rename are the
|
||||
/// target's. A link to nothing is an error, not an empty store, and so is a
|
||||
/// link in a folder anyone may write that has the sticky bit.
|
||||
fn target(p: &std::path::Path) -> Result<PathBuf, String> {
|
||||
let mut at = p.to_path_buf();
|
||||
for _ in 0..40 {
|
||||
match std::fs::symlink_metadata(&at) {
|
||||
Ok(m) if m.file_type().is_symlink() => {
|
||||
let dir = at.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new("."));
|
||||
if sticky_and_open(dir) {
|
||||
return Err(format!("{} is a link in {}, a folder anyone may write that has the sticky bit, where the store follows no link", at.display(), dir.display()));
|
||||
}
|
||||
let to = std::fs::read_link(&at).map_err(|e| format!("{} is a link that cannot be read ({e})", at.display()))?;
|
||||
at = dir.join(to);
|
||||
}
|
||||
Ok(_) => return Ok(at),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound && at == p => return Ok(at),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(format!("{} is a link to nothing that can be read ({} is missing)", p.display(), at.display())),
|
||||
Err(e) => return Err(format!("{} cannot be read: {e}", at.display())),
|
||||
}
|
||||
}
|
||||
Err(format!("{} is a link through more than 40 links", p.display()))
|
||||
}
|
||||
|
||||
/// The store's file name, and its folder.
|
||||
fn place(target: &std::path::Path) -> (String, &std::path::Path) {
|
||||
let name = target.file_name().map_or_else(|| "demotions".into(), |n| n.to_string_lossy().into_owned());
|
||||
(name, target.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new(".")))
|
||||
}
|
||||
|
||||
/// A new temporary file beside the store's file, named for it, this process and
|
||||
/// a count, made where no file of that name is and held locked by this process
|
||||
/// for as long as it is open, so that no other writer takes it for a killed one's.
|
||||
fn temporary(target: &std::path::Path, what: &str) -> std::io::Result<(PathBuf, std::fs::File)> {
|
||||
static MADE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let (name, _) = Demotions::place(target);
|
||||
let mut last = None;
|
||||
for _ in 0..64 {
|
||||
let p = target.with_file_name(format!(".{name}.{what}.{}.{}.tmp", std::process::id(), MADE.fetch_add(1, std::sync::atomic::Ordering::Relaxed)));
|
||||
match std::fs::OpenOptions::new().write(true).create_new(true).open(&p) {
|
||||
Ok(f) => {
|
||||
if let Err(e) = f.lock() {
|
||||
let _ = std::fs::remove_file(&p);
|
||||
return Err(e);
|
||||
}
|
||||
return Ok((p, f));
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => last = Some(e),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Err(last.unwrap_or_else(|| std::io::Error::other("no free name")))
|
||||
}
|
||||
|
||||
/// The lock beside the file, held shared or exclusive until it is dropped, its
|
||||
/// folder made where it is missing; waited for at most [`STORE_LOCK_WAIT`].
|
||||
fn lock(path: &std::path::Path, exclusive: bool) -> Result<std::fs::File, Unlocked> {
|
||||
if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
|
||||
std::fs::create_dir_all(dir).map_err(|e| Unlocked::Fault(format!("its folder {} cannot be made: {e}", dir.display())))?;
|
||||
}
|
||||
let mut name = path.as_os_str().to_owned();
|
||||
name.push(".lock");
|
||||
let at = PathBuf::from(name);
|
||||
let f = open_plain(&at, std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false)).map_err(|e| Unlocked::Fault(format!("its lock {} cannot be opened: {e}", at.display())))?;
|
||||
let until = std::time::Instant::now() + STORE_LOCK_WAIT;
|
||||
loop {
|
||||
match if exclusive { f.try_lock() } else { f.try_lock_shared() } {
|
||||
Ok(()) => return Ok(f),
|
||||
Err(std::fs::TryLockError::WouldBlock) if std::time::Instant::now() < until => std::thread::sleep(std::time::Duration::from_millis(10)),
|
||||
Err(std::fs::TryLockError::WouldBlock) => return Err(Unlocked::Busy(format!("the lock {} of the host's demotions was held past {} s", at.display(), STORE_LOCK_WAIT.as_secs()))),
|
||||
Err(std::fs::TryLockError::Error(e)) => return Err(Unlocked::Fault(format!("its lock {} cannot be taken: {e}", at.display()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The file as it is: none where it does not exist yet. A file with more than
|
||||
/// one name is refused: a rewrite renames a new file into place under one name
|
||||
/// alone, and would split it from the others for good.
|
||||
fn read(path: &std::path::Path) -> Result<OnFile, String> {
|
||||
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<String, Value>, Vec<PathBuf>), 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<String, Value> = serde_json::from_slice(&bytes).map_err(|e| format!("{} does not parse as demotions: {e}", p.display()))?;
|
||||
for (k, v) in one {
|
||||
records.entry(k).or_insert(v);
|
||||
}
|
||||
files.push(p);
|
||||
}
|
||||
Ok((records, files))
|
||||
}
|
||||
|
||||
/// Remove what killed writers left beside the store: each of its temporary files
|
||||
/// unchanged for [`STORE_STALE_AFTER`] whose lock no writer holds.
|
||||
fn sweep(target: &std::path::Path) {
|
||||
let (name, dir) = Demotions::place(target);
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return };
|
||||
for e in entries.flatten() {
|
||||
if e.file_name().to_str().and_then(|f| beside(&name, f)) != Some(Beside::Temporary) {
|
||||
continue;
|
||||
}
|
||||
let p = e.path();
|
||||
let old = std::fs::symlink_metadata(&p).is_ok_and(|m| m.file_type().is_file() && m.modified().ok().and_then(|t| t.elapsed().ok()).is_some_and(|age| age >= STORE_STALE_AFTER));
|
||||
if old && let Ok(f) = open_plain(&p, std::fs::OpenOptions::new().write(true)) && f.try_lock().is_ok() {
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How large the store's file can be once a demotion of `identity` is merged
|
||||
/// into what it holds: as a demotion writes it, with the largest entry it can add.
|
||||
fn largest_rewrite(records: &mut BTreeMap<String, Value>, identity: &str) -> usize {
|
||||
let added = !records.contains_key(identity);
|
||||
if added {
|
||||
records.insert(identity.to_string(), largest_entry());
|
||||
}
|
||||
let mut n = Count(0);
|
||||
let size = serde_json::to_writer_pretty(&mut n, &*records).map_or(usize::MAX, |()| n.0);
|
||||
if added {
|
||||
records.remove(identity);
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
/// Rewrite the store's file as it is, the way a demotion rewrites it: a file of
|
||||
/// this process's own beside it, its bytes padded with line ends to `size`, the
|
||||
/// most a demotion can write, with the file's mode, synced, cut back to its bytes
|
||||
/// and renamed over the file. A folder that no longer takes a file, a full disk,
|
||||
/// a limit on a file's size and a file that cannot be replaced each fail here,
|
||||
/// before a member is graded on the store.
|
||||
fn takes_a_rewrite(target: &std::path::Path, f: &OnFile, size: usize) -> Result<(), String> {
|
||||
use std::io::Write;
|
||||
let wrote = Demotions::temporary(target, "probe").and_then(|(tmp, mut file)| {
|
||||
let put = (|| {
|
||||
file.write_all(&f.bytes)?;
|
||||
let pad = [b'\n'; 1 << 16];
|
||||
let mut left = size.saturating_sub(f.bytes.len());
|
||||
while left > 0 {
|
||||
let k = left.min(pad.len());
|
||||
file.write_all(&pad[..k])?;
|
||||
left -= k;
|
||||
}
|
||||
if let Some(m) = &f.mode {
|
||||
file.set_permissions(m.clone())?;
|
||||
}
|
||||
file.sync_all()?;
|
||||
file.set_len(f.bytes.len() as u64)?;
|
||||
std::fs::rename(&tmp, target)
|
||||
})();
|
||||
if put.is_err() {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
put
|
||||
});
|
||||
wrote.map_err(|e| format!("{} does not take a rewrite of it as a demotion makes one ({size} bytes, renamed over it): {e}", target.display()))
|
||||
}
|
||||
|
||||
/// Why an identity was demoted, if it was, its file and its own demotions read
|
||||
/// afresh and the file shown to take a rewrite; an error where the store is
|
||||
/// broken, and then nothing it holds can be trusted to be all.
|
||||
pub fn get(&self, identity: &str) -> Result<Option<String>, String> {
|
||||
if let Some(why) = self.fault() {
|
||||
return Err(why);
|
||||
}
|
||||
let on_file = match &self.path {
|
||||
None => BTreeMap::new(),
|
||||
Some(p) => {
|
||||
let t = Demotions::target(p).map_err(|e| self.fail(e))?;
|
||||
let held = Demotions::lock(&t, false).map_err(|e| self.unlocked(e))?;
|
||||
let mut f = Demotions::read(&t).map_err(|e| self.fail(e))?;
|
||||
let (own, _) = Demotions::read_own(&t).map_err(|e| self.fail(e))?;
|
||||
let mut all = std::mem::take(&mut f.records);
|
||||
for (k, v) in own {
|
||||
all.entry(k).or_insert(v);
|
||||
}
|
||||
Demotions::sweep(&t);
|
||||
let size = Demotions::largest_rewrite(&mut all, identity);
|
||||
Demotions::takes_a_rewrite(&t, &f, size).map_err(|e| self.fail(e))?;
|
||||
drop(held);
|
||||
all
|
||||
}
|
||||
};
|
||||
let mut records = self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?;
|
||||
for (k, v) in on_file {
|
||||
records.entry(k).or_insert(v);
|
||||
}
|
||||
Ok(records.get(identity).map(|r| r["why"].as_str().map_or_else(|| r.to_string(), String::from)))
|
||||
}
|
||||
|
||||
/// Keep a demotion in a file of its own beside the store's, with the store's
|
||||
/// mode: written to a temporary file, synced, and renamed to a name no other
|
||||
/// file has (128 bits drawn afresh). No lock is taken: every reader reads it.
|
||||
fn keep_alone(target: &std::path::Path, identity: &str, entry: &Value, mode: Option<std::fs::Permissions>) -> Result<PathBuf, String> {
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
use std::io::Write;
|
||||
let (name, _) = Demotions::place(target);
|
||||
let draw = |k: u64| {
|
||||
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
|
||||
h.write_u64(k);
|
||||
h.write_u32(std::process::id());
|
||||
h.write_u128(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos()));
|
||||
h.finish()
|
||||
};
|
||||
let own = target.with_file_name(format!(".{name}.demotion.{:016x}{:016x}.json", draw(1), draw(2)));
|
||||
let mut one = serde_json::Map::new();
|
||||
one.insert(identity.to_string(), entry.clone());
|
||||
let text = serde_json::to_string_pretty(&one).map_err(|e| e.to_string())?;
|
||||
let (tmp, mut f) = Demotions::temporary(target, "demotion").map_err(|e| format!("a demotion cannot be written beside {}: {e}", target.display()))?;
|
||||
let put = (|| {
|
||||
f.write_all(text.as_bytes())?;
|
||||
if let Some(m) = mode {
|
||||
f.set_permissions(m)?;
|
||||
}
|
||||
f.sync_all()?;
|
||||
std::fs::rename(&tmp, &own)
|
||||
})();
|
||||
if let Err(e) = put {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Err(format!("a demotion cannot be written beside {}: {e}", target.display()));
|
||||
}
|
||||
Ok(own)
|
||||
}
|
||||
|
||||
/// Under the store's exclusive lock: merge into its file every demotion kept in a
|
||||
/// file of its own and `identity`'s, written whole to a file of this process's own
|
||||
/// with the file's mode, synced and renamed into place; then remove the files of
|
||||
/// their own it merged, and any temporary file a killed writer left.
|
||||
fn merge(&self, t: &std::path::Path, identity: &str, entry: &Value) -> Result<(), String> {
|
||||
use std::io::Write;
|
||||
let f = Demotions::read(t)?;
|
||||
let mut on_file = f.records;
|
||||
let (own, merged) = Demotions::read_own(t)?;
|
||||
for (k, v) in own {
|
||||
on_file.entry(k).or_insert(v);
|
||||
}
|
||||
on_file.entry(identity.to_string()).or_insert_with(|| entry.clone());
|
||||
if let Ok(mut records) = self.records.lock() {
|
||||
for (k, v) in on_file.iter() {
|
||||
records.entry(k.clone()).or_insert_with(|| v.clone());
|
||||
}
|
||||
}
|
||||
Demotions::sweep(t);
|
||||
let text = serde_json::to_string_pretty(&on_file).map_err(|e| e.to_string())?;
|
||||
let (tmp, mut file) = Demotions::temporary(t, "write").map_err(|e| format!("{} cannot be written: {e}", t.display()))?;
|
||||
let put = (|| {
|
||||
file.write_all(text.as_bytes())?;
|
||||
if let Some(m) = &f.mode {
|
||||
file.set_permissions(m.clone())?;
|
||||
}
|
||||
file.sync_all()?;
|
||||
std::fs::rename(&tmp, t)
|
||||
})();
|
||||
if let Err(e) = put {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Err(format!("{} cannot be written: {e}", t.display()));
|
||||
}
|
||||
for p in merged {
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remember a demotion, and where the store has a file, keep it there: at once
|
||||
/// in a file of its own beside the store's, which every certification reads, and
|
||||
/// then merged into the store's file under its exclusive lock. The merge waits for
|
||||
/// the lock at most [`STORE_LOCK_WAIT`]; where the wait runs out, the demotion
|
||||
/// stays in its own file until a later demotion merges it, and is kept all the
|
||||
/// same. The first reason stands, its first [`STORE_REASON_BYTES`] on file. An
|
||||
/// error where it could not be kept past this process: neither its own file nor
|
||||
/// the merge could be written.
|
||||
pub fn record(&self, identity: &str, why: &str, capacity: &Capacity) -> Result<(), String> {
|
||||
let entry = json!({ "why": on_file_reason(why), "capacity": capacity.json() });
|
||||
self.records.lock().map_err(|_| self.fail("the store's lock was poisoned".into()))?.entry(identity.to_string()).or_insert_with(|| entry.clone());
|
||||
if let Some(why) = self.fault() {
|
||||
return Err(why);
|
||||
}
|
||||
let Some(p) = &self.path else { return Ok(()) };
|
||||
let t = Demotions::target(p).map_err(|e| self.fail(e))?;
|
||||
let mode = std::fs::metadata(&t).ok().map(|m| m.permissions());
|
||||
let alone = Demotions::keep_alone(&t, identity, &entry, mode);
|
||||
let merged = match Demotions::lock(&t, true) {
|
||||
Ok(held) => {
|
||||
let m = self.merge(&t, identity, &entry);
|
||||
drop(held);
|
||||
m.map_err(|e| (self.fail(e.clone()), false))
|
||||
}
|
||||
Err(Unlocked::Busy(e)) => Err((e, true)),
|
||||
Err(Unlocked::Fault(e)) => Err((self.fail(e.clone()), false)),
|
||||
};
|
||||
match (alone, merged) {
|
||||
(_, Ok(())) => Ok(()),
|
||||
(Ok(own), Err((e, busy))) => {
|
||||
if busy {
|
||||
eprintln!("wai-quantum landscape: {e}, so this demotion is kept in {}, which every certification reads, until a later demotion merges it", own.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(Err(a), Err((e, _))) => Err(self.fail(format!("{a}; and {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DEMOTIONS: OnceLock<Demotions> = OnceLock::new();
|
||||
|
||||
/// The host's store of demotions: the file `WAI_LANDSCAPE_DEMOTIONS` names (an
|
||||
/// empty value names none), or
|
||||
/// the store a host chose with [`keep_demotions`] before its first use; in
|
||||
/// memory alone otherwise, for this process.
|
||||
pub fn demotions() -> &'static Demotions {
|
||||
DEMOTIONS.get_or_init(|| Demotions::open(std::env::var_os("WAI_LANDSCAPE_DEMOTIONS").filter(|v| !v.is_empty()).map(PathBuf::from)))
|
||||
}
|
||||
|
||||
/// Keep the host's demotions in `store` (a host that runs members does so at
|
||||
/// start). False where the store was already in use.
|
||||
pub fn keep_demotions(store: Demotions) -> bool {
|
||||
DEMOTIONS.set(store).is_ok()
|
||||
}
|
||||
|
||||
/// Why a member is held at `uncertified` on a broken store.
|
||||
fn cannot_keep(e: &str) -> String {
|
||||
format!("the host cannot keep its demotions, so it grades no kernel bit-exact: {e}")
|
||||
}
|
||||
|
||||
/// A member's identity, as demotions are kept under it.
|
||||
pub fn identity_of(m: &dyn Member) -> String {
|
||||
format!("{} {}", m.id(), m.fingerprint())
|
||||
}
|
||||
|
||||
/// The reference: this crate's kernels, on this machine's CPU.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct CpuReference;
|
||||
|
|
@ -163,18 +790,35 @@ impl Member for CpuReference {
|
|||
}
|
||||
}
|
||||
|
||||
/// What an answer from a sampling kernel graded `statistical` says of the grade
|
||||
/// (§7.2): what it rests on and what it misses.
|
||||
pub const STATISTICAL_GRADE: &str = "Its sampling kernel is graded statistical: its final states passed the battery's test (§7.2) on the battery's workloads alone. A fault at a count of cells the battery does not run, or a law near the exact one (blocks of three drawn at a temperature 10% off pass the test), can keep the grade, and the host does not audit it.";
|
||||
|
||||
/// A member with the parity the host found for each kernel.
|
||||
pub struct Certified<'a> {
|
||||
member: &'a dyn Member,
|
||||
settle: Parity,
|
||||
sample: Parity,
|
||||
capacity: Capacity,
|
||||
/// Whether the host certified it, or runs it by name ([`named`]).
|
||||
certified: bool,
|
||||
/// Where a demotion is remembered.
|
||||
store: &'a Demotions,
|
||||
/// Why the host's audit demoted it, once it has: a kernel graded
|
||||
/// `bit-exact` gave other bits than the reference's on a job (§7.3); or why
|
||||
/// the host holds it there, where its store of demotions is broken. From
|
||||
/// then on both its kernels are `uncertified`, and its label says why.
|
||||
demoted: RefCell<Option<String>>,
|
||||
/// 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<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl Certified<'static> {
|
||||
/// The CPU reference, at parity `reference` for both kernels.
|
||||
pub fn reference() -> Certified<'static> {
|
||||
static REFERENCE: CpuReference = CpuReference;
|
||||
Certified { member: &REFERENCE, settle: Parity::Reference, sample: Parity::Reference }
|
||||
Certified { member: &REFERENCE, settle: Parity::Reference, sample: Parity::Reference, capacity: Capacity::NONE, certified: true, store: demotions(), demoted: RefCell::new(None), audit_log: RefCell::new(Vec::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,9 +827,63 @@ impl<'a> Certified<'a> {
|
|||
self.member
|
||||
}
|
||||
|
||||
/// The parity the host found for a kernel.
|
||||
/// The parity the host found for a kernel, which holds within
|
||||
/// [`Certified::capacity`]: `uncertified` once the audit has demoted it.
|
||||
pub fn parity(&self, kernel: &str) -> Parity {
|
||||
if kernel == KERNEL_SETTLE { self.settle } else { self.sample }
|
||||
let p = if kernel == KERNEL_SETTLE { self.settle } else { self.sample };
|
||||
if p != Parity::Reference && self.demoted.borrow().is_some() { Parity::Uncertified } else { p }
|
||||
}
|
||||
|
||||
/// Note what the audit checked, for the answer it is part of.
|
||||
pub(super) fn log_audit(&self, v: Value) {
|
||||
self.audit_log.borrow_mut().push(v);
|
||||
}
|
||||
|
||||
/// What the audit checked since the last call: each entry a run's seed and
|
||||
/// the chains of it run again on the reference, or how much of a settle.
|
||||
pub fn take_audit(&self) -> Vec<Value> {
|
||||
std::mem::take(&mut *self.audit_log.borrow_mut())
|
||||
}
|
||||
|
||||
/// Why the host's audit demoted it, if it has.
|
||||
pub fn demoted(&self) -> Option<String> {
|
||||
self.demoted.borrow().clone()
|
||||
}
|
||||
|
||||
/// Demote it to `uncertified` for both kernels, saying why: what the host
|
||||
/// does when a kernel graded `bit-exact` gives other bits than the
|
||||
/// reference's (§7.3). The first reason stands.
|
||||
pub fn demote(&self, why: String) {
|
||||
let mut d = self.demoted.borrow_mut();
|
||||
if d.is_none() {
|
||||
let kept = self.store.record(&identity_of(self.member), &why, &self.capacity);
|
||||
*d = Some(match kept {
|
||||
Ok(()) => why,
|
||||
Err(e) => format!("{why} (the host could not keep this demotion past this process: {e})"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The capacity the member declared when the host certified it.
|
||||
pub fn capacity(&self) -> Capacity {
|
||||
self.capacity
|
||||
}
|
||||
|
||||
/// What every answer from it says of itself: its own label, the capacity its
|
||||
/// parity holds within where it declares one, and what a `statistical` grade
|
||||
/// rests on where its sampling kernel has one ([`STATISTICAL_GRADE`]).
|
||||
pub fn label(&self) -> String {
|
||||
let mut label = self.member.label();
|
||||
if let Some(w) = self.capacity.words(self.certified) {
|
||||
label = format!("{label} {w}");
|
||||
}
|
||||
if self.parity(KERNEL_SAMPLE) == Parity::Statistical {
|
||||
label = format!("{label} {STATISTICAL_GRADE}");
|
||||
}
|
||||
if let Some(d) = self.demoted() {
|
||||
label = format!("{label} The host demoted it to uncertified: {d}.");
|
||||
}
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,23 +912,45 @@ pub fn check_id(m: &dyn Member) -> Result<(), String> {
|
|||
/// it, and says so.
|
||||
pub fn named(m: &dyn Member) -> Result<Certified<'_>, String> {
|
||||
check_id(m)?;
|
||||
Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified })
|
||||
let demoted = demotions().get(&identity_of(m)).unwrap_or_else(|e| Some(cannot_keep(&e)));
|
||||
Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified, capacity: m.capacity(), certified: false, store: demotions(), demoted: RefCell::new(demoted), audit_log: RefCell::new(Vec::new()) })
|
||||
}
|
||||
|
||||
/// Certify a member: its settle kernel on every `settle` vector, its sampling
|
||||
/// kernel on every `chains` vector and, where those differ, on the battery of
|
||||
/// `statistical` workloads.
|
||||
/// Certify a member within its declared capacity: its settle kernel on every
|
||||
/// `settle` vector within it, its sampling kernel on every `chains` vector
|
||||
/// within it and, where those differ, on the battery of `statistical`
|
||||
/// workloads within it. A capacity under the floor is refused: a member cannot
|
||||
/// be certified by declaring itself too small to be held to the core set.
|
||||
pub fn certify(m: &dyn Member) -> Result<Certified<'_>, 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<Certified<'a>, String> {
|
||||
check_id(m)?;
|
||||
let settle = if conformance::settle_vectors_hold(m) { Parity::BitExact } else { Parity::Uncertified };
|
||||
let sample = if conformance::chain_vectors_hold(m) {
|
||||
let capacity = m.capacity();
|
||||
let under = capacity.under(&floor());
|
||||
if !under.is_empty() {
|
||||
return Err(format!("{} declares a capacity under the floor every member is held to: {}", m.id(), under.join("; ")));
|
||||
}
|
||||
// A demoted identity, or one the host cannot tell is not, or whose demotion it
|
||||
// could not keep: both kernels uncertified, and certified no further.
|
||||
if let Some(why) = store.get(&identity_of(m)).unwrap_or_else(|e| Some(cannot_keep(&e))) {
|
||||
return Ok(Certified { member: m, settle: Parity::Uncertified, sample: Parity::Uncertified, capacity, certified: true, store, demoted: RefCell::new(Some(why)), audit_log: RefCell::new(Vec::new()) });
|
||||
}
|
||||
// One reading of the capacity: what the floor checked, what the vectors are held to,
|
||||
// and what every answer states.
|
||||
let settle = if conformance::settle_vectors_hold_within(m, &capacity) { Parity::BitExact } else { Parity::Uncertified };
|
||||
let sample = if conformance::chain_vectors_hold_within(m, &capacity) {
|
||||
Parity::BitExact
|
||||
} else if conformance::run_battery(m).iter().all(|r| r.passed) {
|
||||
} else if conformance::run_battery_within(m, &capacity).iter().all(|r| r.passed) {
|
||||
Parity::Statistical
|
||||
} else {
|
||||
Parity::Uncertified
|
||||
};
|
||||
Ok(Certified { member: m, settle, sample })
|
||||
Ok(Certified { member: m, settle, sample, capacity, certified: true, store, demoted: RefCell::new(None), audit_log: RefCell::new(Vec::new()) })
|
||||
}
|
||||
|
||||
fn check_cells(o: &Outcome, n: usize) -> Result<(), String> {
|
||||
|
|
@ -291,9 +1011,22 @@ pub fn check_chain(o: &Outcome, w: &Workload, temperatures: &[f64]) -> Result<()
|
|||
}
|
||||
|
||||
/// What an answer says of the member that ran it.
|
||||
/// What an answer says of the member that ran it, with the capacity its parity
|
||||
/// holds within where it declares one. With none declared, as the reference's,
|
||||
/// there is no `capacity`, and the answer is the same bytes it always was.
|
||||
pub(super) fn describe(c: &Certified<'_>, kernel: &str) -> Value {
|
||||
let m = c.member();
|
||||
json!({ "id": m.id(), "class": m.class(), "kernel": kernel, "parity": c.parity(kernel).label() })
|
||||
with_capacity(c, json!({ "id": m.id(), "class": m.class(), "kernel": kernel, "parity": c.parity(kernel).label() }))
|
||||
}
|
||||
|
||||
fn with_capacity(c: &Certified<'_>, mut v: Value) -> Value {
|
||||
if c.capacity().is_declared() {
|
||||
v["capacity"] = c.capacity().json();
|
||||
}
|
||||
if let Some(d) = c.demoted() {
|
||||
v["demoted"] = json!(d);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// What an answer that may run both kernels says of the member that ran it:
|
||||
|
|
@ -306,7 +1039,7 @@ pub(super) fn describe_all(c: &Certified<'_>, kernels: &[&str]) -> Value {
|
|||
used.push(k);
|
||||
}
|
||||
}
|
||||
json!({ "id": m.id(), "class": m.class(), "kernels": used.iter().map(|k| json!({ "kernel": k, "parity": c.parity(k).label() })).collect::<Vec<_>>() })
|
||||
with_capacity(c, json!({ "id": m.id(), "class": m.class(), "kernels": used.iter().map(|k| json!({ "kernel": k, "parity": c.parity(k).label() })).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -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<Outcome, String> {
|
||||
|
|
|
|||
|
|
@ -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<Item = i64> + '_ {
|
||||
self.spans.iter().flat_map(|&(lo, hi)| lo..=hi)
|
||||
}
|
||||
|
||||
/// Every field the cells can reach, highest first.
|
||||
pub fn fields_back(&self) -> impl Iterator<Item = i64> + '_ {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use super::member::{self, Certified, Chain, KERNEL_SAMPLE, KERNEL_SETTLE};
|
|||
use super::price::{self, Mode, group, joules};
|
||||
use super::program::{Check, Graded as CheckGraded, Program, Refusal, RunLine, check_items, check_temperature, grade};
|
||||
use super::sample;
|
||||
use super::settle::{Term, Workload, state_hash};
|
||||
use super::settle::{Outcome, Term, Workload, state_hash};
|
||||
use super::truth::{Truth, exhaustive_checked, exhaustive_declared};
|
||||
use super::{CHUNK_CELLS, CHUNK_CHAINS, EXHAUSTIVE_MAX_VARS, PROTOCOL};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -317,17 +317,94 @@ fn add_reading(total: &mut Option<Option<member::Metered>>, reading: Option<memb
|
|||
};
|
||||
}
|
||||
|
||||
/// The host's audit key (§7.3): made when the audit first draws, from the
|
||||
/// operating system's randomness the standard library seeds its hash maps with,
|
||||
/// and never shown, so that no member can tell which of its chains are checked.
|
||||
/// It binds nothing a reader can check afterwards: the audit's draws are the
|
||||
/// host's, as its answers are.
|
||||
fn audit_key() -> &'static [u8; 32] {
|
||||
static KEY: std::sync::OnceLock<[u8; 32]> = std::sync::OnceLock::new();
|
||||
KEY.get_or_init(|| {
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
let mut h = blake3::Hasher::new();
|
||||
for k in 0..8u64 {
|
||||
let mut x = std::collections::hash_map::RandomState::new().build_hasher();
|
||||
x.write_u64(k);
|
||||
h.update(&x.finish().to_le_bytes());
|
||||
}
|
||||
let here = 0u8;
|
||||
h.update(&(std::ptr::addr_of!(here) as usize).to_le_bytes());
|
||||
*h.finalize().as_bytes()
|
||||
})
|
||||
}
|
||||
|
||||
/// A number for the audit's choices: keyed with the host's secret, over the
|
||||
/// job's program, the seed, a purpose and a count that every draw moves on, so
|
||||
/// that the same job drawn again draws afresh.
|
||||
fn audit_draw(job: &Job, seed: u64, purpose: u64) -> u64 {
|
||||
static DRAWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let mut h = blake3::Hasher::new_keyed(audit_key());
|
||||
h.update(job.program.source.as_bytes());
|
||||
h.update(&seed.to_le_bytes());
|
||||
h.update(&purpose.to_le_bytes());
|
||||
h.update(&DRAWS.fetch_add(1, std::sync::atomic::Ordering::Relaxed).to_le_bytes());
|
||||
let b = h.finalize();
|
||||
u64::from_le_bytes(b.as_bytes()[..8].try_into().expect("eight bytes"))
|
||||
}
|
||||
|
||||
/// The audit found a kernel graded `bit-exact` giving other bits than the
|
||||
/// reference's: the member is demoted at once, and the job goes to the reference (§7.3).
|
||||
fn demote(c: &Certified<'_>, kernel: &str, what: String) -> Refusal {
|
||||
let m = c.member();
|
||||
c.demote(format!("its {kernel} kernel, graded bit-exact, {what}"));
|
||||
Refusal::general(format!("The host's audit found that the member {} gave other bits than the reference's, and demoted it to uncertified: {}.", m.id(), c.demoted().unwrap_or_default()))
|
||||
}
|
||||
|
||||
fn execute(job: &Job, plan: Plan, seed: u64, c: &Certified<'_>) -> Result<Found, Refusal> {
|
||||
let w = &job.workload;
|
||||
let n = w.n();
|
||||
let m = c.member();
|
||||
let refused = |e: String| Refusal::general(format!("The member {} refused or failed: {e}.", m.id()));
|
||||
let declined = |e: String| Refusal::general(format!("The host declined this job for {}: {e}.", m.id()));
|
||||
match plan {
|
||||
Plan::Settle { sweeps } => {
|
||||
// The host gives a member only what it takes, however it was reached.
|
||||
m.takes(w, false).map_err(refused)?;
|
||||
let o = m.settle(w, &vec![0i8; n], sweeps).map_err(refused)?;
|
||||
member::check_settle(&o, w, sweeps).map_err(refused)?;
|
||||
c.capacity().admits_settle(w, sweeps).map_err(declined)?;
|
||||
let start = vec![0i8; n];
|
||||
let o = m.settle(w, &start, sweeps).map_err(refused)?;
|
||||
let audited = c.parity(KERNEL_SETTLE) == member::Parity::BitExact;
|
||||
if let Err(e) = member::check_settle(&o, w, sweeps) {
|
||||
return Err(if audited { demote(c, KERNEL_SETTLE, format!("gave an answer the host's checks refuse ({e})")) } else { refused(e) });
|
||||
}
|
||||
// The audit, from what the host computes alone: the reference's settle, as
|
||||
// far as a budget of work goes (the cells and couplings times the sweeps the
|
||||
// host asked for, never the sweeps the member reports). Where the reference
|
||||
// comes to rest within it, or the whole bound fits it, that is the whole
|
||||
// answer, and it is compared; past it, the whole settle one job in so many,
|
||||
// drawn with the host's key, and otherwise a member that stops sooner than
|
||||
// the reference did is wrong.
|
||||
if audited {
|
||||
let per = (w.nonzeros() + n as u64).max(1);
|
||||
let most = (member::AUDIT_SETTLE_WORK / per).clamp(1, u64::from(u32::MAX));
|
||||
let reach = u64::from(sweeps).min(most) as u32;
|
||||
let prefix = w.settle(&start, reach);
|
||||
let whole = u64::from(sweeps) <= most || prefix.term != Term::Bound;
|
||||
let (want, how) = if whole {
|
||||
(Some(prefix), "whole".to_string())
|
||||
} else if audit_draw(job, seed, 0).is_multiple_of(member::AUDIT_SETTLE_ONE_IN) {
|
||||
(Some(w.settle(&start, sweeps)), "whole, drawn".to_string())
|
||||
} else {
|
||||
(None, format!("its first {reach} sweeps"))
|
||||
};
|
||||
c.log_audit(json!({ "seed": seed.to_string(), "settle": how }));
|
||||
let wrong = match want {
|
||||
Some(want) => o != want,
|
||||
None => o.sweeps <= reach,
|
||||
};
|
||||
if wrong {
|
||||
return Err(demote(c, KERNEL_SETTLE, format!("settled a workload of {n} cells to another state than the reference's")));
|
||||
}
|
||||
}
|
||||
let energy = job.program.landscape.energy(&o.state);
|
||||
Ok(Found { energy, accumulates: o.accumulates, draws: o.draws, sweeps: u64::from(o.sweeps), term: Some(o.term), state: o.state, metered: m.meter() })
|
||||
}
|
||||
|
|
@ -353,11 +430,21 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64
|
|||
let n = w.n();
|
||||
let m = c.member();
|
||||
let refused = |e: String| Refusal::general(format!("The member {} refused or failed: {e}.", m.id()));
|
||||
m.takes(w, temperatures.iter().any(|&t| t > 0.0)).map_err(refused)?;
|
||||
let batch = (CHUNK_CELLS / n.max(1)).clamp(1, CHUNK_CHAINS);
|
||||
let declined = |e: String| Refusal::general(format!("The host declined this job for {}: {e}.", m.id()));
|
||||
let cap = c.capacity();
|
||||
cap.admits(w, temperatures).map_err(declined)?;
|
||||
// The audit of a sampling kernel graded `bit-exact`: in every call, the last
|
||||
// chain, and one chain of each seed's run among those the call holds, drawn
|
||||
// afresh with the host's key, so that what the host checks in one call says
|
||||
// nothing of the next; run again on the reference, on the audit's threads,
|
||||
// after the call and before the next.
|
||||
let audited = c.parity(KERNEL_SAMPLE) == member::Parity::BitExact;
|
||||
// No more chains in a call than the member declares it takes.
|
||||
let batch = cap.batch((CHUNK_CELLS / n.max(1)).clamp(1, CHUNK_CHAINS));
|
||||
let r = u64::from(restarts);
|
||||
// Every (seed, start) in order: seed after seed, start after start.
|
||||
let total = seeds.len() as u64 * r;
|
||||
let mut checked: Vec<Vec<u64>> = vec![Vec::new(); seeds.len()];
|
||||
let mut found: Vec<SeedRun> = vec![(None, 0, 0, None); seeds.len()];
|
||||
let mut at = 0u64;
|
||||
while at < total {
|
||||
|
|
@ -373,9 +460,32 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64
|
|||
return Err(refused(format!("it returned {} chains for {}", outcomes.len(), chains.len())));
|
||||
}
|
||||
let reading = m.meter();
|
||||
if audited {
|
||||
let mut these: Vec<u64> = ((at / r)..=((end - 1) / r))
|
||||
.map(|s| {
|
||||
let (from, to) = ((s * r).max(at), ((s + 1) * r).min(end));
|
||||
from + audit_draw(job, seeds[s as usize], 1) % (to - from)
|
||||
})
|
||||
.collect();
|
||||
these.push(end - 1);
|
||||
these.sort_unstable();
|
||||
these.dedup();
|
||||
for &k in &these {
|
||||
checked[(k / r) as usize].push(k % r);
|
||||
if let Err(e) = member::check_chain(&outcomes[(k - at) as usize], w, temperatures) {
|
||||
return Err(demote(c, KERNEL_SAMPLE, format!("gave a chain the host's checks refuse ({e})")));
|
||||
}
|
||||
}
|
||||
let places: Vec<usize> = these.iter().map(|&k| (k - at) as usize).collect();
|
||||
if let Some(i) = differs(w, temperatures, &chains, &outcomes, &places) {
|
||||
return Err(demote(c, KERNEL_SAMPLE, format!("gave chain {} of a call of {} other bits than the reference's", i + 1, chains.len())));
|
||||
}
|
||||
}
|
||||
let one_run = alone || (at / r == (end - 1) / r && at.is_multiple_of(r) && end.is_multiple_of(r));
|
||||
for (k, o) in (at..end).zip(outcomes) {
|
||||
member::check_chain(&o, w, temperatures).map_err(refused)?;
|
||||
if let Err(e) = member::check_chain(&o, w, temperatures) {
|
||||
return Err(if audited { demote(c, KERNEL_SAMPLE, format!("gave a chain the host's checks refuse ({e})")) } else { refused(e) });
|
||||
}
|
||||
let f = &mut found[(k / r) as usize];
|
||||
f.1 += o.accumulates;
|
||||
f.2 += o.draws;
|
||||
|
|
@ -389,6 +499,11 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64
|
|||
}
|
||||
at = end;
|
||||
}
|
||||
if audited {
|
||||
for (s, ks) in checked.iter().enumerate() {
|
||||
c.log_audit(json!({ "seed": seeds[s].to_string(), "chains": ks }));
|
||||
}
|
||||
}
|
||||
Ok(found
|
||||
.into_iter()
|
||||
.map(|(best, accumulates, draws, metered)| {
|
||||
|
|
@ -399,6 +514,154 @@ fn chains_for_seeds(job: &Job, temperatures: &[f64], restarts: u32, seeds: &[u64
|
|||
.collect())
|
||||
}
|
||||
|
||||
/// The threads the audit's re-runs may take beside the jobs' own, over the whole
|
||||
/// process at once: a quarter of the host's cores, at most 4 (§7.3); none on a host
|
||||
/// of fewer than 4. Each runs at a low priority ([`lower_this_thread`]). The job's
|
||||
/// own thread runs its share of the re-runs as well, at the job's priority, so an
|
||||
/// audited job takes at most its own thread and these.
|
||||
pub fn audit_threads() -> usize {
|
||||
(std::thread::available_parallelism().map_or(1, |p| p.get()) / 4).min(4)
|
||||
}
|
||||
|
||||
static AUDIT_THREADS_TAKEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
|
||||
/// As many of the audit's threads as are free, up to `want`; given back by the caller.
|
||||
fn take_audit_threads(want: usize) -> usize {
|
||||
use std::sync::atomic::Ordering;
|
||||
let most = audit_threads();
|
||||
let mut taken = AUDIT_THREADS_TAKEN.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let k = want.min(most.saturating_sub(taken));
|
||||
if k == 0 {
|
||||
return 0;
|
||||
}
|
||||
match AUDIT_THREADS_TAKEN.compare_exchange_weak(taken, taken + k, Ordering::AcqRel, Ordering::Relaxed) {
|
||||
Ok(_) => return k,
|
||||
Err(now) => taken = now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
unsafe extern "C" {
|
||||
fn getpriority(which: i32, who: u32) -> i32;
|
||||
fn setpriority(which: i32, who: u32, prio: i32) -> i32;
|
||||
}
|
||||
|
||||
/// This thread at a low priority: on Linux, where a nice value is a thread's, 10
|
||||
/// more, to 19 at most; on macOS, the background band; elsewhere as it is. Its
|
||||
/// priority before and after, as `getpriority` reads it for this thread (the nice
|
||||
/// value on Linux; on macOS nonzero in the background band), or the error
|
||||
/// `setpriority` gave.
|
||||
fn lower_this_thread() -> std::io::Result<(i32, i32)> {
|
||||
#[cfg(target_os = "linux")]
|
||||
// SAFETY: getpriority and setpriority take plain values; `PRIO_PROCESS` (0) and
|
||||
// `who` 0 name the calling thread on Linux.
|
||||
unsafe {
|
||||
let before = getpriority(0, 0);
|
||||
if setpriority(0, 0, (before + 10).min(19)) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
Ok((before, getpriority(0, 0)))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
// SAFETY: plain values; `PRIO_DARWIN_THREAD` (3) with `who` 0 names the calling
|
||||
// thread, and `PRIO_DARWIN_BG` (0x1000) puts it in the background band.
|
||||
unsafe {
|
||||
let before = getpriority(3, 0);
|
||||
if setpriority(3, 0, 0x1000) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
Ok((before, getpriority(3, 0)))
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
Ok((0, 0))
|
||||
}
|
||||
|
||||
/// An audit thread that could not be lowered says so, once a process, and runs its
|
||||
/// share at the job's priority.
|
||||
fn lowered_or_said(r: &std::io::Result<(i32, i32)>) {
|
||||
static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
if let Err(e) = r
|
||||
&& !SAID.swap(true, std::sync::atomic::Ordering::Relaxed)
|
||||
{
|
||||
eprintln!("wai-quantum landscape: an audit thread's priority could not be lowered ({e}), so the audit's threads run at the job's priority");
|
||||
}
|
||||
}
|
||||
|
||||
/// What the audit's threads did, for the test that holds their number and priority.
|
||||
#[cfg(test)]
|
||||
mod census {
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
pub static LIVE: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static MOST: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static LOWERED: Mutex<Vec<Result<(i32, i32), String>>> = Mutex::new(Vec::new());
|
||||
|
||||
/// One audit thread running, until dropped.
|
||||
pub struct Running;
|
||||
|
||||
pub fn enter(lowered: &std::io::Result<(i32, i32)>) -> Running {
|
||||
let now = LIVE.fetch_add(1, Ordering::AcqRel) + 1;
|
||||
MOST.fetch_max(now, Ordering::AcqRel);
|
||||
if let Ok(mut l) = LOWERED.lock() {
|
||||
l.push(lowered.as_ref().map(|p| *p).map_err(|e| e.to_string()));
|
||||
}
|
||||
Running
|
||||
}
|
||||
|
||||
impl Drop for Running {
|
||||
fn drop(&mut self) {
|
||||
LIVE.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The first of a call's chains at `places` whose outcome is not the
|
||||
/// reference's, each run again on the reference: on this thread, at its own
|
||||
/// priority, and on as many of the audit's threads as are free
|
||||
/// ([`audit_threads`]), each at a low priority.
|
||||
fn differs(w: &Workload, temperatures: &[f64], chains: &[Chain], outcomes: &[Outcome], places: &[usize]) -> Option<usize> {
|
||||
let wrong = |part: &[usize]| part.iter().copied().find(|&i| outcomes[i] != sample::anneal_with(w, &chains[i].start, temperatures, chains[i].seed));
|
||||
// The threads taken are given back however the re-runs end.
|
||||
struct Taken(usize);
|
||||
impl Drop for Taken {
|
||||
fn drop(&mut self) {
|
||||
AUDIT_THREADS_TAKEN.fetch_sub(self.0, std::sync::atomic::Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
let extra = Taken(take_audit_threads(places.len().saturating_sub(1)));
|
||||
let parts: Vec<&[usize]> = places.chunks(places.len().div_ceil(extra.0 + 1).max(1)).collect();
|
||||
std::thread::scope(|s| {
|
||||
let (first, rest) = parts.split_first()?;
|
||||
let others: Vec<_> = rest
|
||||
.iter()
|
||||
.map(|&part| {
|
||||
let h = std::thread::Builder::new().name("wai-audit".into()).spawn_scoped(s, move || {
|
||||
let lowered = lower_this_thread();
|
||||
lowered_or_said(&lowered);
|
||||
#[cfg(test)]
|
||||
let _running = census::enter(&lowered);
|
||||
wrong(part)
|
||||
});
|
||||
(part, h)
|
||||
})
|
||||
.collect();
|
||||
let mut found = wrong(first);
|
||||
for (part, h) in others {
|
||||
let r = match h {
|
||||
Ok(h) => h.join().unwrap_or_else(|p| std::panic::resume_unwind(p)),
|
||||
Err(_) => wrong(part),
|
||||
};
|
||||
found = match (found, r) {
|
||||
(Some(a), Some(b)) => Some(a.min(b)),
|
||||
(a, b) => a.or(b),
|
||||
};
|
||||
}
|
||||
found
|
||||
})
|
||||
}
|
||||
|
||||
/// Every seed's run of a plan, the starts of several seeds handed to the member
|
||||
/// together. A settle has no randomness: it runs once, and every seed has its answer.
|
||||
pub(crate) fn execute_seeds(job: &Job, plan: Plan, seeds: &[u64], c: &Certified<'_>) -> Result<Vec<Found>, Refusal> {
|
||||
|
|
@ -535,7 +798,6 @@ pub fn run_on(c: &Certified<'_>, job: &Job, seed: u64, temperature: Option<f64>)
|
|||
/// The answer of `extensions/quantum-landscape.md` §3, from what a run's kernels left.
|
||||
pub(crate) fn answer_of(c: &Certified<'_>, job: &Job, plan: Plan, seed: u64, found: &Found) -> Value {
|
||||
let (p, exact) = (&job.program, &job.exact);
|
||||
let m = c.member();
|
||||
let n = p.landscape.len();
|
||||
let reached = exact.truth.as_ref().map(|t| found.energy as f64 == t.lowest);
|
||||
let (declared, declared_reached, declared_energy) = match &p.declared {
|
||||
|
|
@ -569,8 +831,8 @@ pub(crate) fn answer_of(c: &Certified<'_>, job: &Job, plan: Plan, seed: u64, fou
|
|||
};
|
||||
// The label claims a reading only for a run that has one, and says what it covers.
|
||||
let label = match &found.metered {
|
||||
Some(r) => format!("{} Beside the priced figures is a reading of {}.", m.label(), r.scope.as_deref().unwrap_or("this run alone")),
|
||||
None => m.label(),
|
||||
Some(r) => format!("{} Beside the priced figures is a reading of {}.", c.label(), r.scope.as_deref().unwrap_or("this run alone")),
|
||||
None => c.label(),
|
||||
};
|
||||
json!({
|
||||
"protocol": PROTOCOL,
|
||||
|
|
@ -644,7 +906,6 @@ pub fn sweep_plans(p: &Program, temperatures: &[f64], seeds: u32) -> Result<Vec<
|
|||
/// One program at each temperature, over a run of seeds, with the same budget at each.
|
||||
pub fn sweep_on(c: &Certified<'_>, job: &Job, temperatures: &[f64], seeds: u32, first_seed: u64) -> Result<Value, Refusal> {
|
||||
let (p, exact) = (&job.program, &job.exact);
|
||||
let m = c.member();
|
||||
let plans = sweep_plans(p, temperatures, seeds)?;
|
||||
let default = p.landscape.default_temperature();
|
||||
let mut rows = Vec::with_capacity(plans.len());
|
||||
|
|
@ -685,7 +946,7 @@ pub fn sweep_on(c: &Certified<'_>, job: &Job, temperatures: &[f64], seeds: u32,
|
|||
}
|
||||
Ok(json!({
|
||||
"protocol": PROTOCOL,
|
||||
"label": m.label(),
|
||||
"label": c.label(),
|
||||
"member": member::describe_all(c, &kernels),
|
||||
"lowest": exact.truth.as_ref().map(|t| t.lowest),
|
||||
"default_temperature": default,
|
||||
|
|
@ -758,7 +1019,7 @@ pub fn compare_on(c: &Certified<'_>, job: &Job, seeds: u32, first_seed: u64, tem
|
|||
.collect();
|
||||
Ok(json!({
|
||||
"protocol": PROTOCOL,
|
||||
"label": c.member().label(),
|
||||
"label": c.label(),
|
||||
"member": member::describe_all(c, &[member::KERNEL_SAMPLE]),
|
||||
"target": target,
|
||||
"target_is_exact": exact_target,
|
||||
|
|
@ -999,4 +1260,64 @@ mod tests {
|
|||
assert_eq!(c["target_is_exact"], true);
|
||||
assert_eq!(rows[2]["classical"], true);
|
||||
}
|
||||
|
||||
/// The audit's threads (§7.3): a quarter of the host's cores, 4 at most, over the
|
||||
/// whole process at once however many audited calls run together; each lowered,
|
||||
/// `setpriority`'s result checked and the priority read back; every one given back.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
#[test]
|
||||
fn the_audit_s_threads_are_capped_over_the_process_and_run_low() {
|
||||
use std::sync::atomic::Ordering;
|
||||
let cores = std::thread::available_parallelism().map_or(1, |p| p.get());
|
||||
let cap = (cores / 4).min(4);
|
||||
assert_eq!(audit_threads(), cap, "a quarter of {cores} cores, 4 at most");
|
||||
let lowered_well = |(before, after): (i32, i32)| if cfg!(target_os = "linux") { after == (before + 10).min(19) } else { after != 0 };
|
||||
// A thread lowered on its own: setpriority succeeded, and the priority reads lowered.
|
||||
let alone = std::thread::spawn(lower_this_thread).join().unwrap();
|
||||
println!("PROBE audit threads: {cores} cores, a cap of {cap}; a thread lowered alone: {alone:?}");
|
||||
let alone = alone.expect("setpriority lowers a thread");
|
||||
assert!(lowered_well(alone), "a thread lowered alone reads {alone:?}");
|
||||
// Eight audited calls at once, each running 24 chains again.
|
||||
let mut l = crate::quantum_landscape::energy::Landscape::new(64);
|
||||
for i in 0..64 {
|
||||
l.couple(i, (i + 1) % 64, if i % 2 == 0 { 1 } else { -1 });
|
||||
}
|
||||
let w = l.lower();
|
||||
let temperatures = vec![1.0; 300];
|
||||
let chains: Vec<Chain> = (0..24)
|
||||
.map(|k| {
|
||||
let s = sample::chain_seed(7, k);
|
||||
Chain { start: sample::start(s, 64), seed: s }
|
||||
})
|
||||
.collect();
|
||||
let outcomes: Vec<Outcome> = chains.iter().map(|c| sample::anneal_with(&w, &c.start, &temperatures, c.seed)).collect();
|
||||
let places: Vec<usize> = (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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -17,15 +17,17 @@
|
|||
//! class are coupled, but the order is not the reference's, so its parity
|
||||
//! can only be statistical, and it has to pass the battery to get that.
|
||||
//!
|
||||
//! A workload whose field bound does not fit a 32-bit lane is declined before
|
||||
//! anything runs, with the reason ([`Member::takes`]): the host then runs it on
|
||||
//! the next member it was offered, and the answer says so. A cost above every
|
||||
//! field is held at the lane's top, where it does what it does at its own size.
|
||||
//! It declares its capacity ([`Member::capacity`]): a field bound of at most
|
||||
//! 2³¹ − 1, which its 32-bit lanes hold, on both sides of temperature 0. The
|
||||
//! host runs it on no workload past that, gives such a job to the next member
|
||||
//! it was offered, and the answer says so. A cost above every field is held at
|
||||
//! the lane's top, where it does what it does at its own size.
|
||||
//!
|
||||
//! The member says nothing of its own parity: the host certifies it
|
||||
//! ([`super::member::certify`]). It is native only: a page's WebGPU is
|
||||
//! asynchronous, and the member's calls block.
|
||||
|
||||
use super::capacity::{Capacity, Limits};
|
||||
use super::device::{self, MeterSlot, Reach, SETTLE_BATCH};
|
||||
use super::member::{Chain, Member, Metered};
|
||||
use super::settle::{Outcome, Term, Workload};
|
||||
|
|
@ -389,25 +391,27 @@ impl WebGpuLandscape {
|
|||
words(&s)
|
||||
}
|
||||
|
||||
/// Whether the workload fits the kernels: every field in a 32-bit lane,
|
||||
/// the workload within a binding, and, for sampling, a table of one sweep's
|
||||
/// cut points within [`device::MAX_TABLE_ENTRIES`]. Only the first can
|
||||
/// decline a workload a job gives (`extensions/quantum-landscape.md` §2.0).
|
||||
fn fits(&self, w: &Workload, sampling: bool) -> Result<(), String> {
|
||||
let bound = w.field_bound();
|
||||
if bound > i64::from(i32::MAX) {
|
||||
return Err(format!("its field bound is {bound}, and this member's 32-bit lanes hold at most {}", i32::MAX));
|
||||
/// What its kernels hold: every field in a 32-bit lane, so a field bound
|
||||
/// of at most 2³¹ − 1 on both sides of temperature 0; cells and couplings
|
||||
/// that fit a storage binding (each at most a 32nd of it, which leaves the
|
||||
/// workload's words within one); and one sweep's cut points within
|
||||
/// [`device::MAX_TABLE_ENTRIES`]. Only the first is reached by a job
|
||||
/// (`extensions/quantum-landscape.md` §2.0).
|
||||
fn declared(&self) -> Capacity {
|
||||
let lanes = Limits { field_bound: Some(i64::from(i32::MAX)), ..Limits::NONE };
|
||||
Capacity {
|
||||
cells: Some(self.max_binding / 32),
|
||||
couplings: Some(self.max_binding / 32),
|
||||
at_zero: lanes,
|
||||
above_zero: lanes,
|
||||
table_entries: Some(device::MAX_TABLE_ENTRIES),
|
||||
..Capacity::NONE
|
||||
}
|
||||
let n = w.n() as u64;
|
||||
if 3 * n * 4 > self.max_binding || (w.nonzeros() * 2 + 3 * n + 1) * 4 > self.max_binding {
|
||||
return Err("the workload is larger than a storage binding on this device".into());
|
||||
}
|
||||
if sampling { device::table_fits(&device::reach(w)) } else { Ok(()) }
|
||||
}
|
||||
|
||||
fn settle_on_device(&self, w: &Workload, start: &[i8], bound: u32) -> Result<Outcome, String> {
|
||||
self.settled()?;
|
||||
self.fits(w, false)?;
|
||||
self.declared().admits(w, &[])?;
|
||||
let n = w.n();
|
||||
let (g, at) = graph(w, &device::reach(w));
|
||||
let graph = self.buffer("graph", &words(&g), wgpu::BufferUsages::STORAGE);
|
||||
|
|
@ -445,7 +449,7 @@ impl WebGpuLandscape {
|
|||
fn chains_on_device(&self, w: &Workload, chains: &[Chain], temperatures: &[f64]) -> Result<Vec<Outcome>, String> {
|
||||
let sampling = temperatures.iter().any(|&t| t > 0.0);
|
||||
self.settled()?;
|
||||
self.fits(w, sampling)?;
|
||||
self.declared().admits(w, temperatures)?;
|
||||
let n = w.n();
|
||||
let count = chains.len();
|
||||
if count == 0 {
|
||||
|
|
@ -570,8 +574,8 @@ impl Member for WebGpuLandscape {
|
|||
device::label(self.class, &self.adapter, &format!("WebGPU ({})", self.backend), order)
|
||||
}
|
||||
|
||||
fn takes(&self, w: &Workload, sampling: bool) -> Result<(), String> {
|
||||
self.fits(w, sampling)
|
||||
fn capacity(&self) -> Capacity {
|
||||
self.declared()
|
||||
}
|
||||
|
||||
fn settle(&self, w: &Workload, start: &[i8], bound: u32) -> Result<Outcome, String> {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -501,4 +501,39 @@ fn timings() {
|
|||
let t = middle(&mut || time(|| job::call_certified(&c, "replay", &req, MAX_JOB_BYTES).unwrap()).1);
|
||||
println!(" {} {t:.3} s, the same states, energies, verdicts, work and replay digests", m.id());
|
||||
}
|
||||
// What the host's audit (§7.3) costs in wall time: each job on a member certified
|
||||
// bit-exact, so audited, against the same member run by name, so not, and against
|
||||
// the reference alone.
|
||||
let jobs = [
|
||||
("replay of 256 seeds, 8 chains each, 64 cells, 500 sweeps", "replay", program(&sparse(64, 4, 0xa0d1), "solve t0=2 restarts=8 steps=500"), 256),
|
||||
("replay of 256 seeds, 1 chain each, 64 cells, 500 sweeps", "replay", program(&sparse(64, 4, 0xa0d1), "solve t0=2 restarts=1 steps=500"), 256),
|
||||
("run of 4,096 chains, 64 cells, 200 sweeps", "run", program(&sparse(64, 4, 0xa0d2), "solve t0=2 restarts=4096 steps=200"), 1),
|
||||
("run of 1,024 chains, 1,024 cells, 200 sweeps", "run", program(&sparse(1024, 6, 0xa0d3), "solve t0=2 restarts=1024 steps=200"), 1),
|
||||
];
|
||||
println!("audit threads beside a job's own: {}", wai_quantum::quantum_landscape::run::audit_threads());
|
||||
for (what, op, text, seeds) in &jobs {
|
||||
let req = if *op == "replay" { json!({ "program": text, "seed": "1", "seeds": seeds }) } else { json!({ "program": text, "seed": "1" }) };
|
||||
let t_cpu = middle(&mut || time(|| job::call(op, &req).unwrap()).1);
|
||||
println!("audit, {what}: cpu:reference alone {t_cpu:.3} s");
|
||||
for m in &ms {
|
||||
let c = member::certify(m.as_ref()).unwrap();
|
||||
if c.parity(member::KERNEL_SAMPLE) != member::Parity::BitExact {
|
||||
continue;
|
||||
}
|
||||
let by_name = member::named(m.as_ref()).unwrap();
|
||||
// Five runs, each the middle of three of each, audited and not in turn.
|
||||
let runs: Vec<(f64, f64)> = (0..5)
|
||||
.map(|_| {
|
||||
let audited = middle(&mut || time(|| job::call_certified(&c, op, &req, MAX_JOB_BYTES).unwrap()).1);
|
||||
let unaudited = middle(&mut || time(|| job::call_certified(&by_name, op, &req, MAX_JOB_BYTES).unwrap()).1);
|
||||
(audited / unaudited - 1.0, audited / t_cpu)
|
||||
})
|
||||
.collect();
|
||||
assert!(c.demoted().is_none(), "{}", m.id());
|
||||
let (lo, hi) = runs.iter().fold((f64::MAX, f64::MIN), |(lo, hi), r| (lo.min(r.0), hi.max(r.0)));
|
||||
let most = runs.iter().map(|r| r.1).fold(0.0, f64::max);
|
||||
let each: Vec<String> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue