From 6c2eb8d2d0787977894e3571167d6669d32d3a99 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 6 Nov 2026 08:37:51 -0500 Subject: [PATCH 1/4] wai-quantum: the estimate job on the reaction-limited clock The estimate job priced arXiv:1905.09749's 2048-bit factoring at 60.7 hours against the paper's 5.1: every Toffoli took three logical steps, and only quantum_arch had the reaction-limited clock. The job now takes it: "clock": {"reaction_us": t} in a request, --reaction-us t on the command line, reaction_us= in Python, and the estimate_resources tool on both MCP servers. One rule: quantum_resource::reaction_tick(reaction, supply), the slower of the two, is the tick. quantum_arch::Timing::tick_us calls it (quantum_arch now enables quantum_resource), and Clock::Reaction takes it. On that clock estimate_isa_clocked takes the Toffolis' 3X steps out and adds X ticks to the runtime, every Toffoli after the one before as the layout runs its steps; holds the tiles' error over the whole runtime; keeps enough factories that a CCZ state (four T states) comes every tick, so the tick is the reaction time; and stretches ticks with steps when slowed. What the spec and receipts pin stays: - Without a clock, or "clock": "steps", the answer and its receipts are what they were, bit for bit: estimate_isa is estimate_isa_clocked on Clock::Steps, the same arithmetic in the same order, and a clocked job on the step clock has the job's hash and seals the same receipt. - On the reaction clock the estimate's steps are the lattice-surgery steps alone and its runtime adds the ticks, so the job is versioned: a ClockedJob, whose hash is wai:resource-job-clocked\x01 over the job's hash and the reaction time. A receipt sealed on one clock never verifies as sealed on the other. - wai.quantum.estimate/1 grows: an optional request member clock, and an optional clock member in each estimate's figures (reaction_us, tick_us, toffolis). No public struct gained a field: the clock enters through new items only. Through the job, arXiv:1905.09749's counts at its 10 us give 7.50 hours, every Toffoli after the one before: the paper's own Section 2.11 estimate on that basis is "approximately 7 hours"; its 5.1 runs its two runway pieces side by side and its lookups at half a beat, which counts alone do not carry. The published-architecture page carries these rows, and the step clock's rows are now an assumption the request can change. Tests, each failing on the code before it: quantum_resource the_step_clock_is_the_estimate_bit_for_bit_and_the_ reaction_clock_keeps_pace; quantum_resource_receipt on_the_step_clock_ a_clocked_job_is_the_job_and_on_the_reaction_clock_another; engine::estimate without_a_clock_the_answer_is_the_step_clock_s_and_ names_none, the_reaction_clock_runs_each_toffoli_in_a_tick_with_ factories_to_keep_up, a_reaction_clock_estimate_is_sealed_as_a_clocked_ job; and Python's test_an_estimate_on_the_reaction_clock_runs_each_ toffoli_in_a_tick. The resource-estimation workflow requires the five Rust tests by name. The committed tool lists and agent docs are regenerated. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-estimate.yml | 10 +- .../2026-11-06-estimate-reaction-clock.md | 20 ++ wai/wai-quantum-cli/docs/llms-full.txt | 7 + wai/wai-quantum-cli/docs/local-mcp.json | 84 ++++++++ wai/wai-quantum-cli/docs/service-mcp.json | 84 ++++++++ wai/wai-quantum-cli/src/estimate.rs | 6 + wai/wai-quantum-py/python/wai_quantum/_api.py | 7 +- wai/wai-quantum-py/tests/test_engine.py | 10 + wai/wai-quantum/Cargo.toml | 2 +- wai/wai-quantum/docs/estimate.md | 37 +++- .../docs/published-architectures.md | 37 ++-- wai/wai-quantum/schemas/estimate.v1.json | 41 ++++ wai/wai-quantum/src/engine/estimate/mod.rs | 164 ++++++++++++++-- .../src/published_architectures.rs | 44 ++++- wai/wai-quantum/src/quantum_arch.rs | 6 +- wai/wai-quantum/src/quantum_resource.rs | 182 ++++++++++++++++-- .../src/quantum_resource_receipt.rs | 138 ++++++++++++- .../tests/data/published_figures.tsv | 4 + 18 files changed, 822 insertions(+), 61 deletions(-) create mode 100644 wai/changes/2026-11-06-estimate-reaction-clock.md diff --git a/.github/workflows/wai-quantum-estimate.yml b/.github/workflows/wai-quantum-estimate.yml index fbb7b6a46..6255b373c 100644 --- a/.github/workflows/wai-quantum-estimate.yml +++ b/.github/workflows/wai-quantum-estimate.yml @@ -6,7 +6,8 @@ name: wai-quantum resource estimation # each rotation's T count against its synthesised word, the interchange # documents against the format's schema and a self-made fixture, the # frontier, the seals, and the planar model and its receipts -# (quantum_resource, quantum_resource_receipt) beside them. +# (quantum_resource, quantum_resource_receipt) beside them, on the step clock +# and the reaction-limited clock. # # The published-architecture check (src/published_architectures.rs) feeds the # stated assumptions of published resource estimates into the estimators @@ -91,7 +92,12 @@ jobs: engine::estimate::interchange::tests::the_fixture_meets_the_schema_and_its_totals_are_its_children_s \ engine::estimate::tests::an_estimate_is_sealed_verifies_and_a_changed_one_does_not \ quantum_resource::tests::the_summary_rows_are_reproduced \ - quantum_resource_receipt::tests::a_counted_program_binds_its_digest_and_its_epsilon; do + quantum_resource_receipt::tests::a_counted_program_binds_its_digest_and_its_epsilon \ + quantum_resource::tests::the_step_clock_is_the_estimate_bit_for_bit_and_the_reaction_clock_keeps_pace \ + quantum_resource_receipt::tests::on_the_step_clock_a_clocked_job_is_the_job_and_on_the_reaction_clock_another \ + engine::estimate::tests::without_a_clock_the_answer_is_the_step_clock_s_and_names_none \ + engine::estimate::tests::the_reaction_clock_runs_each_toffoli_in_a_tick_with_factories_to_keep_up \ + engine::estimate::tests::a_reaction_clock_estimate_is_sealed_as_a_clocked_job; do grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/estimate.log" || { echo "::error::$t did not run and pass"; exit 1; } done - name: Published architectures, re-estimated, and the page that prints them diff --git a/wai/changes/2026-11-06-estimate-reaction-clock.md b/wai/changes/2026-11-06-estimate-reaction-clock.md new file mode 100644 index 000000000..d52fc9a24 --- /dev/null +++ b/wai/changes/2026-11-06-estimate-reaction-clock.md @@ -0,0 +1,20 @@ +### wai-quantum: the estimate job on the reaction-limited clock + +- An estimate's request takes `"clock": {"reaction_us": t}` (`wai-quantum estimate + --reaction-us t`): each Toffoli has its Clifford fixups teleported away from the data and + takes one tick of the reaction time, its three logical steps gone, with enough factories + that a CCZ state comes every tick. The figures add `clock` (`reaction_us`, `tick_us`, + `toffolis`); `steps` are then the lattice-surgery steps alone. Without a clock, or with + `"steps"`, the answer and its receipts are what they were, bit for bit. +- The tick is one rule, `quantum_resource::reaction_tick`, which `quantum_arch::Timing` + takes too (`quantum_arch` now enables `quantum_resource`). `estimate_isa_clocked`, + `estimate_best_isa_clocked`, `Clock` and `Clocked` price it; on the step clock they are + `estimate_isa`, bit for bit. +- A reaction-clock estimate is sealed as a `ClockedJob`, whose hash is a form of its own + (`wai:resource-job-clocked\x01` over the job's hash and the reaction time), so a receipt + sealed on one clock never verifies as sealed on the other. + `ResourceReceipt::seal_clocked` and `verify_computation_clocked` take it; on the step + clock they seal and check what `seal` and `verify_computation` do. +- The published-architecture check runs arXiv:1905.09749's counts through the job at its + 10 µs: 7.50 hours, every Toffoli after the one before, against the step clock's 60.7, and + the paper's own §2.11 estimate on that basis, "approximately 7 hours". diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 4ab3f937f..0eb16c8c5 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -183,6 +183,7 @@ Parameters: - `qubit` (any): The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described. - `calibration` (string, one of exact_matching, union_find, assumed): The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one. - `max_slowdown` (integer): The most the frontier slows the program by to need fewer factories (default 256). +- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up. - `export` (string, one of counts, routine): Also give the program's counts as a logical-counts or a routine document, under export. - `name` (string): The program's name in a routine document. @@ -771,6 +772,7 @@ Parameters: - `qubit` (any): The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described. - `calibration` (string, one of exact_matching, union_find, assumed): The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one. - `max_slowdown` (integer): The most the frontier slows the program by to need fewer factories (default 256). +- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up. - `export` (string, one of counts, routine): Also give the program's counts as a logical-counts or a routine document, under export. - `name` (string): The program's name in a routine document. - `qasm` (string): The program, OpenQASM 3 or 2. Give this, counts or routine. @@ -2538,6 +2540,11 @@ Options: [default: 256] + --reaction-us + Run each Toffoli on the reaction-limited clock: its Clifford fixups teleported away, one + tick of this reaction time (µs), with enough factories to keep up, instead of three + logical steps + --export
Print the program's counts as a document instead of the report: counts (the logical-counts document) or routine (the routine document); the report goes to standard error diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index c2fa94a3e..5b0c59628 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -19010,6 +19010,29 @@ "maximum": 4096, "description": "The most the frontier slows the program by to need fewer factories (default 256)." }, + "clock": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steps" + ] + }, + { + "type": "object", + "properties": { + "reaction_us": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "required": [ + "reaction_us" + ] + } + ], + "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up." + }, "export": { "type": "string", "enum": [ @@ -19072,6 +19095,27 @@ "max_slowdown": { "type": "integer" }, + "clock": { + "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or the reaction-limited clock, a Toffoli one tick of the control system's reaction time in µs, its Clifford fixups teleported away, with enough factories to keep up.", + "anyOf": [ + { + "const": "steps" + }, + { + "type": "object", + "required": [ + "reaction_us" + ], + "properties": { + "reaction_us": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "additionalProperties": false + } + ] + }, "export": { "enum": [ "counts", @@ -19346,6 +19390,26 @@ "type": "object" } } + }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.", + "required": [ + "reaction_us", + "tick_us", + "toffolis" + ], + "properties": { + "reaction_us": { + "type": "number" + }, + "tick_us": { + "type": "number" + }, + "toffolis": { + "type": "integer" + } + } } } }, @@ -19482,6 +19546,26 @@ "type": "object" } } + }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.", + "required": [ + "reaction_us", + "tick_us", + "toffolis" + ], + "properties": { + "reaction_us": { + "type": "number" + }, + "tick_us": { + "type": "number" + }, + "toffolis": { + "type": "integer" + } + } } } }, diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index 6675ff071..75298c485 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -39661,6 +39661,29 @@ "maximum": 4096, "description": "The most the frontier slows the program by to need fewer factories (default 256)." }, + "clock": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steps" + ] + }, + { + "type": "object", + "properties": { + "reaction_us": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "required": [ + "reaction_us" + ] + } + ], + "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up." + }, "export": { "type": "string", "enum": [ @@ -39730,6 +39753,27 @@ "max_slowdown": { "type": "integer" }, + "clock": { + "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or the reaction-limited clock, a Toffoli one tick of the control system's reaction time in µs, its Clifford fixups teleported away, with enough factories to keep up.", + "anyOf": [ + { + "const": "steps" + }, + { + "type": "object", + "required": [ + "reaction_us" + ], + "properties": { + "reaction_us": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "additionalProperties": false + } + ] + }, "export": { "enum": [ "counts", @@ -40004,6 +40048,26 @@ "type": "object" } } + }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.", + "required": [ + "reaction_us", + "tick_us", + "toffolis" + ], + "properties": { + "reaction_us": { + "type": "number" + }, + "tick_us": { + "type": "number" + }, + "toffolis": { + "type": "integer" + } + } } } }, @@ -40140,6 +40204,26 @@ "type": "object" } } + }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.", + "required": [ + "reaction_us", + "tick_us", + "toffolis" + ], + "properties": { + "reaction_us": { + "type": "number" + }, + "tick_us": { + "type": "number" + }, + "toffolis": { + "type": "integer" + } + } } } }, diff --git a/wai/wai-quantum-cli/src/estimate.rs b/wai/wai-quantum-cli/src/estimate.rs index e753178e0..0f620dcfc 100644 --- a/wai/wai-quantum-cli/src/estimate.rs +++ b/wai/wai-quantum-cli/src/estimate.rs @@ -45,6 +45,9 @@ pub struct Args { /// The most the frontier slows the program by, to need fewer factories (it tries powers of two up to this) #[arg(long, default_value_t = estimate::DEFAULT_MAX_SLOWDOWN)] pub max_slowdown: u64, + /// Run each Toffoli on the reaction-limited clock: its Clifford fixups teleported away, one tick of this reaction time (µs), with enough factories to keep up, instead of three logical steps + #[arg(long, value_name = "MICROSECONDS")] + pub reaction_us: Option, /// Print the program's counts as a document instead of the report: counts (the logical-counts document) or routine (the routine document); the report goes to standard error #[arg(long, value_name = "FORM")] pub export: Option, @@ -74,6 +77,9 @@ pub fn requests(a: &Args) -> Result, String> { if let Some(e) = &a.export { r["export"] = json!(e); } + if let Some(t) = a.reaction_us { + r["clock"] = json!({ "reaction_us": t }); + } let named = a.file.as_deref().or(a.routine.as_deref()).or(a.counts.as_deref()).filter(|p| *p != Path::new("-")); if let Some(stem) = named.and_then(Path::file_stem).and_then(|s| s.to_str()) { r["name"] = json!(stem); diff --git a/wai/wai-quantum-py/python/wai_quantum/_api.py b/wai/wai-quantum-py/python/wai_quantum/_api.py index 855e078e7..9590b2a20 100644 --- a/wai/wai-quantum-py/python/wai_quantum/_api.py +++ b/wai/wai-quantum-py/python/wai_quantum/_api.py @@ -329,6 +329,7 @@ def estimate( export: Optional[str] = None, name: Optional[str] = None, key: KeyLike = None, + reaction_us: Optional[float] = None, ) -> Dict[str, Any]: """What a fault-tolerant machine would need to run ``program`` (or the logical-counts document ``counts``, or the routine document @@ -342,7 +343,9 @@ def estimate( ``union_find`` or ``assumed``), the headline estimate on ``qubit`` (a reference qubit's name, ``ns-e4`` by default, or a dict describing one) and the frontier between physical qubits and runtime (slowed up to - ``max_slowdown``). Every estimate is sealed with ``key`` in the + ``max_slowdown``). With ``reaction_us`` each Toffoli runs on the + reaction-limited clock, one tick of that reaction time in µs, instead of + three logical steps. Every estimate is sealed with ``key`` in the resource-estimate receipt family; ``verify_estimate`` checks the seals and re-runs it. ``export`` (``counts`` or ``routine``) adds the program's counts as a document, named ``name``. A request the engine @@ -360,6 +363,8 @@ def estimate( for k, v in (("budget", budget), ("qubit", qubit), ("calibration", calibration), ("max_slowdown", max_slowdown), ("export", export), ("name", name)): if v is not None: request[k] = dict(v) if isinstance(v, Mapping) else v + if reaction_us is not None: + request["clock"] = {"reaction_us": reaction_us} return _engine.estimate(request, key_of(key).seed) diff --git a/wai/wai-quantum-py/tests/test_engine.py b/wai/wai-quantum-py/tests/test_engine.py index c0faf433a..397646e5d 100644 --- a/wai/wai-quantum-py/tests/test_engine.py +++ b/wai/wai-quantum-py/tests/test_engine.py @@ -389,6 +389,16 @@ def test_an_estimate_counts_the_program_seals_every_figure_and_re_runs(): assert wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=64, key=DEMO) == e +def test_an_estimate_on_the_reaction_clock_runs_each_toffoli_in_a_tick(): + steps = wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO)["result"]["estimate"] + e = wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO, reaction_us=10) + r = e["result"] + assert r["options"]["clock"] == {"reaction_us": 10.0} + assert r["estimate"]["clock"] == {"reaction_us": 10.0, "tick_us": 10.0, "toffolis": 3} + assert "clock" not in steps and r["estimate"]["runtime_s"] < steps["runtime_s"] + assert wq.verify_estimate(e)["valid"] + + def test_an_estimate_reads_and_writes_the_interchange_documents(): doc = wq.estimate(LADDER, export="counts", key=DEMO)["result"]["export"] assert doc == {"numQubits": 5, "tCount": 1, "rotationCount": 1, "rotationDepth": 1, "cczCount": 3, "ccixCount": 0, "measurementCount": 4} diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index 2b57a2678..ebb775e50 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -132,7 +132,7 @@ quantum_gbs = [] quantum_sv = [] # Architecture-level estimation: yoked storage, magic-state cultivation, CCZ # factories and reaction-limited time, assembled into whole-machine estimates. -quantum_arch = [] +quantum_arch = ["quantum_resource"] # qLDPC memory at circuit level: bivariate-bicycle codes, their depth-8 syndrome # cycle, and BP-OSD decoding of the circuit's error model. quantum_ldpc = ["quantum_frame"] diff --git a/wai/wai-quantum/docs/estimate.md b/wai/wai-quantum/docs/estimate.md index bd5f4658d..8f143871c 100644 --- a/wai/wai-quantum/docs/estimate.md +++ b/wai/wai-quantum/docs/estimate.md @@ -74,6 +74,7 @@ receipts 5 estimates sealed by did:key:z6Mk…; they verify and re-run here | `qubit` | `ns-e4` | a reference qubit (below), or `{"name", "instruction_set": "gate"\|"measurement", "gate_ns", "meas_ns", "clifford_error", "t_error"}` | | `calibration` | `exact_matching` | the gate-based surface code's model: `exact_matching`, `union_find` (fitted to the decoders) or `assumed` (the planar model's own) | | `max_slowdown` | `256` | the frontier slows the program by 1, 2, 4, … up to this (at most 4096) | +| `clock` | `"steps"` | how long a Toffoli takes: `"steps"`, three logical steps; or `{"reaction_us": t}`, the reaction-limited clock (below) | | `export` | none | also give the counts as a `counts` or `routine` document | | `name` | `program` | the program's name in a routine document | @@ -197,12 +198,33 @@ On a physical qubit and a planar code: - enough factory copies run that the program's T states arrive within its runtime. +**The clock.** By default a Toffoli takes three logical steps, as the +planar model has it. On the reaction-limited clock, `"clock": +{"reaction_us": t}` (`wai-quantum estimate --reaction-us t`), a Toffoli has +its Clifford fixups teleported away from the data and takes one tick: the +slower of the reaction time `t` and the CCZ supply, the rule +`quantum_arch`'s clock takes too (arXiv:1905.09749 §2.11). Then: + +- the Toffolis' `3·X` steps leave the steps, and `X` ticks join the + runtime: runtime = steps × `τ(d)` + `X` × tick, every Toffoli after the + one before, as the layout runs its steps; +- the distance holds the tiles' error over the whole runtime; +- the factories are as many as the T states need over the runtime, and at + least as many as deliver a CCZ state (four T states) every tick, so the + supply keeps up and a tick is the reaction time; +- a slowed program stretches its ticks as its steps: `s` times the reaction + time. + +Without a clock, or with `"steps"`, the answer and its receipts are what +they were before the clock was named, bit for bit. + | Field | Meaning | |---|---| | `qubit`, `code`, `distance`, `slowdown` | the qubit, the code (with its calibration), `d`, and how many times slower than the fewest steps the program runs | | `physical_qubits` = `algorithm_qubits` + `factory_qubits` | tiles × qubits per tile, and the factories' | | `factories`, `factory_fraction` | factory copies, and their share of the physical qubits | -| `runtime_s`, `steps`, `step_us` | wall-clock time = steps × the step time `τ(d)` | +| `runtime_s`, `steps`, `step_us` | wall-clock time = steps × the step time `τ(d)`; on the reaction-limited clock, `steps` are the lattice-surgery steps alone and the runtime adds the ticks | +| `clock` | on the reaction-limited clock only: `reaction_us`, `tick_us` (at this slowdown) and the `toffolis` run on ticks | | `tiles`, `tile_qubits` | `Q` and the physical qubits of one tile at `d` | | `logical_error` | `P(d)`, per tile per step | | `t_states`, `t_target` | the T states and the error each must meet | @@ -299,8 +321,11 @@ origin, its digest, every count, the ancillas, the rotations' `T`, the depths, the budget, the synthesis share, each rotation's share, the bound and the layout), the qubit, the code with its calibrated constants as exact IEEE-754 bits, and the slowdown; its result hash binds every figure of the -estimate down to each factory round. The frontier's receipts name the -headline's as their parent. The whole result is then sealed in a job's +estimate down to each factory round. On the reaction-limited clock the job +is a clocked job (`quantum_resource_receipt::ClockedJob`): its hash is a +form of its own, `wai:resource-job-clocked\x01` over the job's hash and the +reaction time, so a receipt sealed on one clock never verifies as sealed on +the other. The frontier's receipts name the headline's as their parent. The whole result is then sealed in a job's form, so any surface's `verify` checks it as it checks a job. `engine::estimate::verify` (`wai-quantum verify`, `POST /v1/verify`, @@ -321,7 +346,7 @@ estimates bit for bit, and the same figures, counts and notes. | `QE05` | nothing to estimate: no qubits, or no measurement, `T` gate, rotation or Toffoli (no logical step) | | `QE06` | the physical model refuses: the qubit's error is at or above the code's threshold, no distance up to 51 meets the logical share, or no factory meets the T target | | `QE07` | a document that does not read: a key or a value that is not one, a symbolic value, a repetition, a schema violation | -| `QE08` | a request that does not read: the program, the budget, the qubit, the calibration, the slowdown or the export | +| `QE08` | a request that does not read: the program, the budget, the qubit, the calibration, the slowdown, the clock or the export | ## What it assumes @@ -333,7 +358,9 @@ estimates bit for bit, and the same figures, counts and notes. uncomputation in a ladder is another Toffoli (measurement-based uncomputation, which needs none, is not assumed). - Classical control (the latency of a branch on a measured bit) is not - modelled. + modelled, except as the reaction-limited clock's tick when that clock is + asked for. On either clock every Toffoli follows the one before: a + program's Toffoli depth is not read. - A rotation under two or more controls (`ctrl(2) @ rz(θ)`, and `rx`, `ry`, `u` the same) is counted as the reader writes it, through its phase polynomial, not through ancillas. diff --git a/wai/wai-quantum/docs/published-architectures.md b/wai/wai-quantum/docs/published-architectures.md index 83bb3577b..625a1c6a3 100644 --- a/wai/wai-quantum/docs/published-architectures.md +++ b/wai/wai-quantum/docs/published-architectures.md @@ -105,14 +105,20 @@ Toffoli count. Its title's 8 hours is the re-run's 7.31 hours expected across retries, taken up to one digit. Its hand-worked §2.14 (23 million qubits) rounds as it goes and says so. Its §2.12 reads a 6.4% distillation error from an earlier paper's spreadsheet where its own script's formula gives 0.6%. Our -estimate job, given the same counts, finds the same code distance but takes 12 -times as long: it prices the planar architecture of arXiv:2211.07629, three -logical steps of `d` cycles a Toffoli, and has no reaction-limited clock. That -clock is in `quantum_arch` (**our limit** in the job). +estimate job, given the same counts, finds the same code distance. On its +default step clock, three logical steps of `d` cycles a Toffoli as the planar +model of arXiv:2211.07629 has it, it takes 12 times as long. On its +reaction-limited clock at the paper's 10 µs, the rule `quantum_arch` takes, it +takes 7.50 hours: every Toffoli after the one before, which is the basis of the +paper's own §2.11 estimate, "approximately 7 hours". The paper's 5.1 hours run +its two runway pieces' additions side by side and its lookups at half a beat an +entry, which counts alone do not carry (**our limit**). **FeMoCo.** Its floorplan, factory, Toffoli rate and 3 days come back on the same clock; its §IV.C.5 says 3.5 days where §IV.C.4 says 3. The estimate job -again runs at the beat. +runs at the beat on its step clock; on its reaction-limited clock it keeps +enough factories to tick at 10 µs, where the paper's four factories set its +pace at 38.75 µs. **RSA-2048 (2025).** As `quantum_arch` documents: its stated durations give 10.6 hours a shot where it prints 12.07, its hot-storage counts disagree with @@ -122,10 +128,9 @@ its own formula, and so on. Its Toffoli count comes back. ## What this does not do -- **The estimate job has no reaction-limited clock.** It prices the planar - architecture, and a laid-out machine that teleports its fixups takes about - a tenth of the time the job gives. `quantum_arch` has that clock; the job - does not offer it. +- **The estimate job runs every Toffoli after the one before.** On either + clock it reads no Toffoli depth, so a program whose Toffolis run side by side + takes less time than it gives. - **The bicycle machine is not scheduled.** A rotation's time is the mean of its parts, a few percent under the paper's compiled critical path, and repeated in-module measurements are not removed. @@ -309,8 +314,10 @@ its own formula, and so on. Its Toffoli count comes back. | physical qubits, job, assumed code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 18,906,836 | -5.47% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | | physical qubits, job, fitted code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 18,906,836 | -5.47% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | | physical qubits, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 27,571,456 | +37.86% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | -| hours per run, job, assumed code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 60.71 | ×11.904 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. | -| hours per run, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 69.71 | ×13.668 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. | +| hours per run, job, assumed code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 60.71 | ×11.904 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | +| hours per run, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 69.71 | ×13.668 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | +| hours per run, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 7.50 | +46.97% | our limit | The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step: the source's own §2.11 estimate on that basis is "approximately 7 hours". Its cost script runs the two runway pieces' additions side by side and its lookups at half a beat an entry, which counts alone do not carry. | +| physical qubits, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 20,050,780 | +0.25% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. On this clock the job keeps enough factories (129) that a CCZ state comes every tick. | | factories, job | arXiv:1905.09749 §2.10; its counts in the estimate job | 14 CCZ per piece, 28 in all | 16 T factories | — | our limit | A CCZ state costs four T states in the planar model. | | 1024 bits: megaqubits, re-run | arXiv:1905.09749 Table 3 | 9.7 | 9.62 | -0.78% | agrees | Its script takes each figure up to two digits. | | 1024 bits: hours per run, re-run | arXiv:1905.09749 Table 3 | 1.3 | 1.26 | -2.79% | agrees | Its script takes each figure up to two digits. | @@ -342,10 +349,12 @@ its own formula, and so on. Its Toffoli count comes back. | code distance, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 31 | 29 | -6.45% | assumption | Its rule is `0.1·(100p)^((d+1)/2)` a cycle (§IV.C.5: "Λ of 10"); the planar model's `0.03·(p/0.01)^((d+1)/2)` a step of `d` cycles, lower by `d/0.3`, one step of distance at this size. | | physical qubits, job, Table III's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 8,419,152 | ×2.105 | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | | physical qubits, job, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 3,529,152 | -11.77% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | -| days, job, Table III's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 5.34 | +77.89% | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. | -| days, job, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 6.75 | ×2.250 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. | +| days, job, Table III's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 5.34 | +77.89% | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | +| days, job, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 6.75 | ×2.250 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | | physical qubits, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | "one million" | 968,200 | -3.18% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | -| days, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | 1.5 | 3.49 | ×2.328 | our limit | The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does. | +| days, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | 1.5 | 3.49 | ×2.328 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | +| days, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 0.78 | ×0.259 | assumption | The job keeps enough factories (157) that a CCZ state comes every 10 µs tick; the source chose four, whose supply sets its pace at 38.75 µs (25 kHz). The job's frontier slows the ticks to need fewer. | +| physical qubits, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 10,799,568 | ×2.700 | assumption | As above: more factories, to keep up with the reaction time. | ### RSA-2048 on yoked surface codes (arXiv:2505.15917, 2025) diff --git a/wai/wai-quantum/schemas/estimate.v1.json b/wai/wai-quantum/schemas/estimate.v1.json index 9218de03a..41439c047 100644 --- a/wai/wai-quantum/schemas/estimate.v1.json +++ b/wai/wai-quantum/schemas/estimate.v1.json @@ -43,6 +43,27 @@ "max_slowdown": { "type": "integer" }, + "clock": { + "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or the reaction-limited clock, a Toffoli one tick of the control system's reaction time in µs, its Clifford fixups teleported away, with enough factories to keep up.", + "anyOf": [ + { + "const": "steps" + }, + { + "type": "object", + "required": [ + "reaction_us" + ], + "properties": { + "reaction_us": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "additionalProperties": false + } + ] + }, "export": { "enum": [ "counts", @@ -304,6 +325,26 @@ }, "receipt": { "$ref": "#/$defs/resource_receipt" + }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: its reaction time, one tick at this estimate's slowdown, and the Toffolis run on ticks. Then steps are the lattice-surgery steps alone, and runtime_s adds the ticks.", + "required": [ + "reaction_us", + "tick_us", + "toffolis" + ], + "properties": { + "reaction_us": { + "type": "number" + }, + "tick_us": { + "type": "number" + }, + "toffolis": { + "type": "integer" + } + } } } }, diff --git a/wai/wai-quantum/src/engine/estimate/mod.rs b/wai/wai-quantum/src/engine/estimate/mod.rs index 8cc350a06..159ec6ef7 100644 --- a/wai/wai-quantum/src/engine/estimate/mod.rs +++ b/wai/wai-quantum/src/engine/estimate/mod.rs @@ -28,9 +28,18 @@ //! re-runs the request and holds every receipt and figure to it. //! //! The request (`{"qasm"}`, `{"counts"}` or `{"routine"}`, with `budget`, -//! `qubit`, `calibration`, `max_slowdown` and `export`, each optional) is -//! [`Options::read`]'s; `docs/estimate.md` explains every number of the -//! answer and what it assumes. +//! `qubit`, `calibration`, `max_slowdown`, `clock` and `export`, each +//! optional) is [`Options::read`]'s and [`clock_of`]'s; `docs/estimate.md` +//! explains every number of the answer and what it assumes. +//! +//! **The clock.** A Toffoli takes three logical steps of `d` cycles unless the +//! request names the reaction-limited clock, `"clock": {"reaction_us": t}`: +//! then its Clifford fixups are teleported away from the data and it takes one +//! tick of `t` µs, the factories keeping up (`quantum_resource::Clock`, the +//! rule `quantum_arch` takes too). A request without a clock is answered and +//! sealed as before, bit for bit; one with the reaction clock is sealed as a +//! clocked job (`quantum_resource_receipt::ClockedJob`), whose hash is a form +//! of its own. pub mod calibration; pub mod count; @@ -39,9 +48,9 @@ pub mod interchange; use self::calibration::Calibration; use self::interchange::{Imported, RoutineCounts}; use super::synth::rz; -use crate::quantum_resource::{Estimate, EstimateConfig, EstimateError, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, SearchLimits, UnitKind, layout}; +use crate::quantum_resource::{Clock, Estimate, EstimateConfig, EstimateError, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, SearchLimits, UnitKind, layout}; #[cfg(feature = "quantum_receipt")] -use crate::quantum_resource_receipt::{Counted, Origin, Program, ResourceJob, ResourceReceipt}; +use crate::quantum_resource_receipt::{ClockedJob, Counted, Origin, Program, ResourceJob, ResourceReceipt}; use serde_json::{Value, json}; /// The answer's format. @@ -75,6 +84,22 @@ pub fn preset(name: &str) -> Option { }) } +/// The clock a request asks for: `"clock": "steps"` (the default, three +/// logical steps a Toffoli) or `{"reaction_us": t}` (a Toffoli a tick of the +/// control system's reaction time, `t` µs, its fixups teleported away). +pub fn clock_of(request: &Value) -> Result { + let refused = || "\"clock\" is \"steps\" (the default: three logical steps a Toffoli) or {\"reaction_us\": t}: a Toffoli a tick of the reaction time, t µs, above 0 [QE08]".to_string(); + match request.get("clock") { + None | Some(Value::Null) => Ok(Clock::Steps), + Some(Value::String(s)) if s == "steps" => Ok(Clock::Steps), + Some(Value::Object(o)) if o.len() == 1 => { + let t = o.get("reaction_us").and_then(Value::as_f64).filter(|t| *t > 0.0 && *t < 1e9).ok_or_else(refused)?; + Ok(Clock::Reaction { reaction_ns: t * 1e3 }) + } + _ => Err(refused()), + } +} + /// The angles a document's rotations are costed at, which it does not give: /// `(2k + 1)·π/17` for `k = 0..8`, none a multiple of `π/4`. A rotation then /// costs the most `T` gates any of them needs. @@ -508,6 +533,17 @@ pub fn figures(e: &Estimate, slowdown: u64) -> Value { }) } +/// [`figures`] on `clock`: on the reaction-limited clock they add the clock's, +/// `{"reaction_us", "tick_us", "toffolis"}`, and `steps` are the +/// lattice-surgery steps alone. +pub fn figures_clocked(e: &Estimate, slowdown: u64, clock: Clock, toffolis: u64) -> Value { + let mut v = figures(e, slowdown); + if let Clock::Reaction { reaction_ns } = clock { + v["clock"] = json!({ "reaction_us": reaction_ns / 1e3, "tick_us": reaction_ns * slowdown as f64 / 1e3, "toffolis": toffolis }); + } + v +} + /// One estimate of the frontier: its code, slowdown and figures. #[derive(Clone, Debug, PartialEq)] pub struct Point { @@ -521,14 +557,19 @@ pub struct Point { /// `max_slowdown` on each code that no other beats on both physical qubits /// and runtime, fastest first. The headline is one of them. pub fn frontier(isa: &IsaCounts, qubit: &PhysicalQubit, codes: &[QecModel], budget: f64, max_slowdown: u64) -> Result<(Vec, usize), String> { + frontier_clocked(isa, 0, qubit, codes, budget, max_slowdown, Clock::Steps) +} + +/// [`frontier`] on `clock`, the program's `toffolis` among its counts. +pub fn frontier_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubit, codes: &[QecModel], budget: f64, max_slowdown: u64, clock: Clock) -> Result<(Vec, usize), String> { let cfg = |s: u64| EstimateConfig { error_budget: budget, slowdown: s, limits: SearchLimits::default() }; let mut all: Vec = Vec::new(); let mut last = None; for code in codes { let mut s = 1; while s <= max_slowdown { - match crate::quantum_resource::estimate_isa(isa, qubit, code, &cfg(s)) { - Ok(e) => all.push(Point { code: code.clone(), slowdown: s, estimate: e }), + match crate::quantum_resource::estimate_isa_clocked(isa, toffolis, qubit, code, &cfg(s), clock) { + Ok(c) => all.push(Point { code: code.clone(), slowdown: s, estimate: c.estimate }), Err(e) => last = Some(e), } s *= 2; @@ -578,12 +619,20 @@ pub struct Evaluation { /// answer they make (not yet sealed); or why it is refused, with a code. pub fn evaluate(request: &Value) -> Result { let options = Options::read(request)?; + let clock = clock_of(request)?; let derived = derive(request, options.budget)?; let codes = options.calibration.codes(&options.qubit); - let (points, headline) = frontier(&derived.isa, &options.qubit, &codes, options.budget, options.max_slowdown)?; + let toffolis = derived.logical.ccz; + let (points, headline) = frontier_clocked(&derived.isa, toffolis, &options.qubit, &codes, options.budget, options.max_slowdown, clock)?; let mut notes = derived.notes.clone(); notes.push("Each rotation is a Clifford+T word found by exact search (about 3·log₂(1/ε) T gates); synthesis that measures and falls back reaches fewer T gates and is not assumed.".into()); notes.push("The physical model is the planar one of quantum_resource (parallel-synthesis sequential Pauli computation, 15-to-1 factories found by exhaustive search), with its assumptions: uniform, independent circuit noise; free transport of T states; smooth T consumption.".into()); + if let Clock::Reaction { reaction_ns } = clock { + notes.push(format!( + "On the reaction-limited clock: each of the {toffolis} Toffoli(s) has its Clifford fixups teleported away from the data and takes one tick of the reaction time, {} µs (stretched as the steps are, slowed), one after another, with enough factories that a CCZ state comes every tick; the other operations take their logical steps. The steps counted are the lattice-surgery steps alone.", + reaction_ns / 1e3 + )); + } let cal = options.calibration.describe(&options.qubit); if cal["applies"] == true && cal["extrapolated"] == true { notes.push(format!( @@ -602,10 +651,13 @@ pub fn evaluate(request: &Value) -> Result { "epsilon": derived.epsilon, "calibration": cal, "options": options.to_json(), - "estimate": figures(&h.estimate, h.slowdown), - "frontier": points.iter().map(|p| figures(&p.estimate, p.slowdown)).collect::>(), + "estimate": figures_clocked(&h.estimate, h.slowdown, clock, toffolis), + "frontier": points.iter().map(|p| figures_clocked(&p.estimate, p.slowdown, clock, toffolis)).collect::>(), "notes": notes, }); + if let Clock::Reaction { reaction_ns } = clock { + result["options"]["clock"] = json!({ "reaction_us": reaction_ns / 1e3 }); + } if derived.rotations.is_array() { result["rotations"] = derived.rotations.clone(); } @@ -646,6 +698,11 @@ impl Evaluation { config: EstimateConfig { error_budget: self.options.budget, slowdown: self.points[i].slowdown, limits: SearchLimits::default() }, } } + + /// [`Evaluation::job`] on `clock`: on the step clock, the job itself. + pub fn clocked_job(&self, i: usize, clock: Clock) -> ClockedJob { + ClockedJob { job: self.job(i), clock } + } } /// `request` estimated and sealed with `key`: `{"request", "result"}`, each @@ -655,13 +712,14 @@ impl Evaluation { #[cfg(feature = "quantum_receipt")] pub fn sealed(request: &Value, key: &ed25519_dalek::SigningKey) -> Result { let ev = evaluate(request)?; + let clock = clock_of(request)?; let did = crate::quantum_receipt::did_key(&key.verifying_key()); let unmetered = |run: &mut dyn FnMut()| { run(); 0 }; let receipt_of = |r: &ResourceReceipt| serde_json::from_str::(&r.to_json()).expect("a receipt's JSON reads"); - let head = ResourceReceipt::seal(key, did.clone(), &ev.job(ev.headline), None, unmetered).map_err(|e| model_refusal(&e))?; + let head = ResourceReceipt::seal_clocked(key, did.clone(), &ev.clocked_job(ev.headline, clock), None, unmetered).map_err(|e| model_refusal(&e))?; let mut result = ev.result.clone(); result["estimate"]["receipt"] = receipt_of(&head); for i in 0..ev.points.len() { @@ -669,7 +727,7 @@ pub fn sealed(request: &Value, key: &ed25519_dalek::SigningKey) -> Result Result { return Err(format!("not an estimate: its result's format is not {FORMAT}")); } let ev = evaluate(request)?; + let clock = clock_of(request)?; let parse = |v: &Value| ResourceReceipt::from_json(&v.to_string()); let key_bytes = key.as_str().and_then(|k| (0..32).map(|i| u8::from_str_radix(k.get(2 * i..2 * i + 2)?, 16).ok()).collect::>>()); let mine = |r: &ResourceReceipt| key_bytes.as_deref() == Some(r.signer_pubkey.as_slice()) && r.verify(); @@ -698,7 +757,7 @@ pub fn verify(body: &Value) -> Result { let mut receipts = frontier.len() == ev.points.len(); let head = parse(&result["estimate"]["receipt"]); match &head { - Some(h) => receipts &= mine(h) && h.parent_receipt_hash.is_none() && h.computation_matches(&ev.job(ev.headline)), + Some(h) => receipts &= mine(h) && h.parent_receipt_hash.is_none() && h.computation_matches_clocked(&ev.clocked_job(ev.headline, clock)), None => receipts = false, } for (i, f) in frontier.iter().enumerate().take(ev.points.len()) { @@ -707,7 +766,7 @@ pub fn verify(body: &Value) -> Result { continue; } receipts &= match (parse(&f["receipt"]), &head) { - (Some(r), Some(h)) => mine(&r) && r.parent_receipt_hash == Some(h.receipt_hash()) && r.computation_matches(&ev.job(i)), + (Some(r), Some(h)) => mine(&r) && r.parent_receipt_hash == Some(h.receipt_hash()) && r.computation_matches_clocked(&ev.clocked_job(i, clock)), _ => false, }; } @@ -755,6 +814,10 @@ pub fn properties() -> Value { ], "description": "The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described." }, "calibration": { "type": "string", "enum": ["exact_matching", "union_find", "assumed"], "description": "The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one." }, "max_slowdown": { "type": "integer", "minimum": 1, "maximum": MAX_SLOWDOWN, "description": "The most the frontier slows the program by to need fewer factories (default 256)." }, + "clock": { "anyOf": [ + { "type": "string", "enum": ["steps"] }, + { "type": "object", "properties": { "reaction_us": { "type": "number", "exclusiveMinimum": 0 } }, "required": ["reaction_us"] } + ], "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up." }, "export": { "type": "string", "enum": ["counts", "routine"], "description": "Also give the program's counts as a logical-counts or a routine document, under export." }, "name": { "type": "string", "description": "The program's name in a routine document." } }) @@ -819,6 +882,79 @@ mod tests { assert_eq!(u.points[u.headline].code.threshold, Calibration::UnionFind.fit().unwrap().threshold); } + #[test] + fn without_a_clock_the_answer_is_the_step_clock_s_and_names_none() { + let req = json!({ "counts": { "numQubits": 100, "cczCount": 1_000_000 }, "qubit": "us-e4" }); + let a = call(&req).unwrap(); + let mut steps = req.clone(); + steps["clock"] = json!("steps"); + assert_eq!(call(&steps).unwrap(), a); + assert!(a["estimate"].get("clock").is_none() && a["options"].get("clock").is_none()); + for bad in [json!("fast"), json!({ "reaction_us": 0 }), json!({ "reaction_us": 10, "x": 1 }), json!({ "reaction": 10 }), json!(10)] { + let mut r = req.clone(); + r["clock"] = bad.clone(); + assert!(call(&r).unwrap_err().contains("[QE08]"), "{bad}"); + } + } + + /// arXiv:1905.09749's counts (its re-run's 2.698·10⁹ Toffolis on 6,189 + /// logical qubits) on its 1 µs, 10⁻³ qubit: the step clock gives about 61 + /// hours, the reaction clock the Toffolis one after another at 10 µs. + fn rsa_2021(clock: Option) -> Value { + let mut r = json!({ + "counts": { "numQubits": 6189, "cczCount": 2_698_000_000u64 }, + "qubit": { "name": "1 µs cycle", "instruction_set": "gate", "gate_ns": 1000.0 / 6.0, "meas_ns": 1000.0 / 6.0, "clifford_error": 1e-3, "t_error": 1e-3 }, + "budget": 0.31, + "calibration": "assumed", + "max_slowdown": 4, + }); + if let Some(c) = clock { + r["clock"] = c; + } + r + } + + #[test] + fn the_reaction_clock_runs_each_toffoli_in_a_tick_with_factories_to_keep_up() { + let (a, b) = (call(&rsa_2021(None)).unwrap(), call(&rsa_2021(Some(json!({ "reaction_us": 10 })))).unwrap()); + let hours = |v: &Value| v["estimate"]["runtime_s"].as_f64().unwrap() / 3600.0; + assert!(hours(&a) > 50.0, "{}", hours(&a)); + // 2.698·10⁹ ticks of 10 µs: 7.49 hours, and no other step. + assert_eq!(b["estimate"]["steps"], 0); + assert!((hours(&b) - 2.698e9 * 10e-6 / 3600.0).abs() < 1e-9, "{}", hours(&b)); + assert_eq!(b["estimate"]["clock"], json!({ "reaction_us": 10.0, "tick_us": 10.0, "toffolis": 2_698_000_000u64 })); + assert_eq!(b["options"]["clock"], json!({ "reaction_us": 10.0 })); + // More factories than the step clock's, to keep up with the ticks + // (quantum_resource's test holds the supply to the tick). + assert!(b["estimate"]["factories"].as_u64().unwrap() > a["estimate"]["factories"].as_u64().unwrap()); + // Slowed, the ticks stretch with the steps. + for p in b["frontier"].as_array().unwrap() { + let s = p["slowdown"].as_f64().unwrap(); + assert_eq!(p["clock"]["tick_us"].as_f64().unwrap(), 10.0 * s); + } + assert!(b["notes"].as_array().unwrap().iter().any(|n| n.as_str().unwrap().starts_with("On the reaction-limited clock"))); + } + + #[cfg(feature = "quantum_receipt")] + #[test] + fn a_reaction_clock_estimate_is_sealed_as_a_clocked_job() { + let req = rsa_2021(Some(json!({ "reaction_us": 10 }))); + let body = sealed(&req, &key()).unwrap(); + let v = verify(&body).unwrap(); + assert_eq!((v["valid"].clone(), v["receipts"].clone(), v["reproduced"].clone()), (json!(true), json!(true), json!(true))); + let ev = evaluate(&req).unwrap(); + let r = ResourceReceipt::from_json(&body["result"]["estimate"]["receipt"].to_string()).unwrap(); + assert!(r.computation_matches_clocked(&ev.clocked_job(ev.headline, clock_of(&req).unwrap()))); + assert!(!r.computation_matches(&ev.job(ev.headline)), "not the step clock's job"); + // The step clock's request seals what it sealed before clocks were named. + let plain = rsa_2021(None); + let ev = evaluate(&plain).unwrap(); + let body = sealed(&plain, &key()).unwrap(); + let r = ResourceReceipt::from_json(&body["result"]["estimate"]["receipt"].to_string()).unwrap(); + assert!(r.computation_matches(&ev.job(ev.headline))); + assert_eq!(r.job_hash, ev.job(ev.headline).job_hash()); + } + #[test] fn the_frontier_is_pareto_and_holds_the_headline() { for q in QUBITS { diff --git a/wai/wai-quantum/src/published_architectures.rs b/wai/wai-quantum/src/published_architectures.rs index f88fccdfd..72d276f14 100644 --- a/wai/wai-quantum/src/published_architectures.rs +++ b/wai/wai-quantum/src/published_architectures.rs @@ -562,13 +562,21 @@ struct Planar { } fn job(counts: Value, budget: f64, p: f64, calibration: &str) -> Result { - let request = json!({ + job_on(counts, budget, p, calibration, None) +} + +/// [`job`] on the reaction-limited clock, `reaction_us` a tick, when given. +fn job_on(counts: Value, budget: f64, p: f64, calibration: &str, reaction_us: Option) -> Result { + let mut request = json!({ "counts": counts, "budget": budget, "qubit": { "name": "1 µs cycle", "instruction_set": "gate", "gate_ns": 1000.0 / 6.0, "meas_ns": 1000.0 / 6.0, "clifford_error": p, "t_error": p }, "calibration": calibration, "max_slowdown": 1, }); + if let Some(t) = reaction_us { + request["clock"] = json!({ "reaction_us": t }); + } let answer = estimate::call(&request)?; let e = &answer["estimate"]; Ok(Planar { @@ -589,8 +597,8 @@ fn one_us(p: f64) -> PhysicalQubit { PhysicalQubit { name: "1 µs cycle".into(), instruction_set: InstructionSet::GateBased, gate_ns: 1000.0 / 6.0, meas_ns: 1000.0 / 6.0, clifford_error: p, t_error: p } } -/// What the estimate job leaves out against a laid-out surface-code machine. -const JOB_TIME: &str = "The estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs an addition at one 10 µs tick a Toffoli. The job has no such clock; quantum_arch's (the re-run rows) does."; +/// The estimate job on its default clock, against a laid-out machine. +const JOB_TIME: &str = "On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below)."; const JOB_SPACE: &str = "The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand."; fn rsa_2021() -> Result { @@ -628,8 +636,18 @@ fn rsa_2021() -> Result { r.num("r21.job.qubits.assumed", "physical qubits, job, assumed code model", assumed.physical, F::I, Class::OurLimit, JOB_SPACE); r.num("r21.job.qubits.fitted", "physical qubits, job, fitted code model", fitted.physical, F::I, Class::OurLimit, JOB_SPACE); r.num("r21.job.qubits.source_rule", "physical qubits, the planar model on the source's patch and error rule", source_noise.physical_qubits as f64, F::I, Class::OurLimit, JOB_SPACE); - r.num("r21.job.hours.assumed", "hours per run, job, assumed code model", assumed.runtime_h, F::D(2), Class::OurLimit, JOB_TIME); - r.num("r21.job.hours.source_rule", "hours per run, the planar model on the source's patch and error rule", source_noise.runtime_ns / 3.6e12, F::D(2), Class::OurLimit, JOB_TIME); + r.num("r21.job.hours.assumed", "hours per run, job, assumed code model", assumed.runtime_h, F::D(2), Class::Assumption, JOB_TIME); + r.num("r21.job.hours.source_rule", "hours per run, the planar model on the source's patch and error rule", source_noise.runtime_ns / 3.6e12, F::D(2), Class::Assumption, JOB_TIME); + let fast = job_on(json!({ "numQubits": 6189, "cczCount": toffolis }), 0.31, 1e-3, "assumed", Some(10.0))?; + r.num( + "r21.job.hours.reaction", + "hours per run, job, reaction-limited clock at 10 µs", + fast.runtime_h, + F::D(2), + Class::OurLimit, + "The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step: the source's own §2.11 estimate on that basis is \"approximately 7 hours\". Its cost script runs the two runway pieces' additions side by side and its lookups at half a beat an entry, which counts alone do not carry.", + ); + r.num("r21.job.qubits.reaction", "physical qubits, job, reaction-limited clock at 10 µs", fast.physical, F::I, Class::OurLimit, format!("{JOB_SPACE} On this clock the job keeps enough factories ({}) that a CCZ state comes every tick.", fast.factories)); r.text("r21.job.factories", "factories, job", format!("{} T factories", assumed.factories), Class::OurLimit, "A CCZ state costs four T states in the planar model."); for n in [1024, 3072, 4096, 8192, 12288, 16384] { @@ -667,10 +685,20 @@ fn femoco() -> Result { r.num("femoco.job.distance", "code distance, its layout's counts", b.distance as f64, F::I, Class::Assumption, "Its rule is `0.1·(100p)^((d+1)/2)` a cycle (§IV.C.5: \"Λ of 10\"); the planar model's `0.03·(p/0.01)^((d+1)/2)` a step of `d` cycles, lower by `d/0.3`, one step of distance at this size."); r.num("femoco.job.qubits.table", "physical qubits, job, Table III's counts", a.physical, F::I, Class::OurLimit, JOB_SPACE); r.num("femoco.job.qubits.layout", "physical qubits, job, its layout's counts", b.physical, F::I, Class::OurLimit, JOB_SPACE); - r.num("femoco.job.days.table", "days, job, Table III's counts", a.runtime_h / 24.0, F::D(2), Class::OurLimit, JOB_TIME); - r.num("femoco.job.days.layout", "days, job, its layout's counts", b.runtime_h / 24.0, F::D(2), Class::OurLimit, JOB_TIME); + r.num("femoco.job.days.table", "days, job, Table III's counts", a.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME); + r.num("femoco.job.days.layout", "days, job, its layout's counts", b.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME); r.num("femoco.job.qubits.e4", "physical qubits, job, p = 10⁻⁴", low.physical, F::I, Class::OurLimit, JOB_SPACE); - r.num("femoco.job.days.e4", "days, job, p = 10⁻⁴", low.runtime_h / 24.0, F::D(2), Class::OurLimit, JOB_TIME); + r.num("femoco.job.days.e4", "days, job, p = 10⁻⁴", low.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME); + let fast = job_on(json!({ "numQubits": 700, "cczCount": toffolis as u64 }), 0.01, 1e-3, "assumed", Some(10.0))?; + r.num( + "femoco.job.days.reaction", + "days, job, its layout's counts, reaction-limited clock at 10 µs", + fast.runtime_h / 24.0, + F::D(2), + Class::Assumption, + format!("The job keeps enough factories ({}) that a CCZ state comes every 10 µs tick; the source chose four, whose supply sets its pace at 38.75 µs (25 kHz). The job's frontier slows the ticks to need fewer.", fast.factories), + ); + r.num("femoco.job.qubits.reaction", "physical qubits, job, its layout's counts, reaction-limited clock at 10 µs", fast.physical, F::I, Class::Assumption, "As above: more factories, to keep up with the reaction time."); Ok(section("FeMoCo on the surface code (arXiv:2011.03494, PRX Quantum 2, 030305, 2021)", r)) } diff --git a/wai/wai-quantum/src/quantum_arch.rs b/wai/wai-quantum/src/quantum_arch.rs index 55d75e9e1..3d354e397 100644 --- a/wai/wai-quantum/src/quantum_arch.rs +++ b/wai/wai-quantum/src/quantum_arch.rs @@ -795,9 +795,11 @@ impl Timing { } /// One tick: the slower of the CCZ supply and the reaction time. A Toffoli /// whose Clifford fixups are teleported away from the data takes one tick - /// and no lattice-surgery beat, unlike a [`Timing::step_us`]. + /// and no lattice-surgery beat, unlike a [`Timing::step_us`]. The rule is + /// [`crate::quantum_resource::reaction_tick`], which the estimate job's + /// reaction-limited clock takes too. pub fn tick_us(&self) -> f64 { - self.ccz_period_us().max(self.reaction_us) + crate::quantum_resource::reaction_tick(self.reaction_us, self.ccz_period_us()) } /// A ripple-carry addition over `n` qubits, its Toffolis at one tick each: /// `2n − 1` ticks, a sweep and its return. diff --git a/wai/wai-quantum/src/quantum_resource.rs b/wai/wai-quantum/src/quantum_resource.rs index 3b4a00777..64c20ee44 100644 --- a/wai/wai-quantum/src/quantum_resource.rs +++ b/wai/wai-quantum/src/quantum_resource.rs @@ -94,8 +94,29 @@ //! uniform, independent circuit noise; free transport of T states; and //! smooth T consumption. //! - **Planar codes only.** Surface and Floquet-honeycomb patches with lattice -//! surgery. Block (qLDPC) codes, magic-state cultivation and reaction-time- -//! limited scheduling need models of their own. +//! surgery. Block (qLDPC) codes and magic-state cultivation need models of +//! their own. +//! +//! # Clocks +//! +//! A Toffoli takes three logical steps of `d` cycles in the source's model: +//! the step clock, [`Clock::Steps`], every estimate's unless another is asked +//! for. On the reaction-limited clock, [`Clock::Reaction`] (arXiv:1905.09749 +//! §2.11), a Toffoli whose Clifford fixups are teleported away from the data +//! takes one tick, the slower of the control system's reaction time and the +//! CCZ supply ([`reaction_tick`], the rule `quantum_arch::Timing` takes too), +//! and no lattice-surgery step. [`estimate_isa_clocked`] prices both: +//! +//! - the Toffolis' `3·R_CCZ` steps leave the step count, and `R_CCZ` ticks +//! join the runtime: `runtime = steps·τ(d) + R_CCZ·tick`, all of them one +//! after another, as the model lays its steps out; +//! - the tiles' logical error runs over the whole runtime, in steps of `τ(d)`; +//! - the factories are as many as the T states need over the runtime, and at +//! least as many as deliver a CCZ state (four T states) every tick, so the +//! supply keeps up with the reaction time and the tick is the reaction time; +//! - a slowdown `s` stretches both: `s·C` steps, ticks of `s` reaction times. +//! +//! On the step clock it is [`estimate_isa`], bit for bit. //! - **Rounding.** The source prints its times truncated (`163.8 ms` as //! `163 ms`). This module reports exact values; the tests compare at the //! printed precision. @@ -586,6 +607,39 @@ fn search( } } +/// How long a Toffoli takes. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Clock { + /// Three logical steps of `d` cycles: the planar model's own, and every + /// estimate's unless another clock is asked for. + Steps, + /// One tick a Toffoli, its Clifford fixups teleported away from the data: + /// the slower of the reaction time (ns) and the CCZ supply. + Reaction { reaction_ns: f64 }, +} + +/// One tick of the reaction-limited clock: the slower of the control system's +/// reaction time and the CCZ supply's period, in one unit. A Toffoli whose +/// Clifford fixups are teleported away from the data takes one tick +/// (arXiv:1905.09749 §2.11). `quantum_arch::Timing::tick_us` and +/// [`Clock::Reaction`] both take it from here. +pub fn reaction_tick(reaction: f64, supply_period: f64) -> f64 { + reaction.max(supply_period) +} + +/// An estimate on a clock. Under [`Clock::Reaction`] the estimate's `steps` +/// are the lattice-surgery steps alone, and its `runtime_ns` adds the +/// Toffolis' ticks. +#[derive(Clone, Debug, PartialEq)] +pub struct Clocked { + pub estimate: Estimate, + pub clock: Clock, + /// Toffolis run on ticks: none on the step clock. + pub ticks: u64, + /// One tick, ns: `0` on the step clock. + pub tick_ns: f64, +} + /// How fast to run: at the minimum step count, or slowed by a factor to /// trade factories for time. #[derive(Clone, Copy, Debug, PartialEq)] @@ -665,9 +719,26 @@ pub fn estimate(c: &LogicalCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg: & /// Estimate from ISA-level counts directly. pub fn estimate_isa(isa: &IsaCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg: &EstimateConfig) -> Result { + estimate_isa_clocked(isa, 0, qubit, qec, cfg, Clock::Steps).map(|c| c.estimate) +} + +/// [`estimate_isa`] on `clock`, the program's `toffolis` among its counts +/// (the layout gives each three of `isa.min_steps` and four of its T states). +/// On [`Clock::Steps`] `toffolis` is not read and the estimate is +/// [`estimate_isa`]'s, bit for bit. +pub fn estimate_isa_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubit, qec: &QecModel, cfg: &EstimateConfig, clock: Clock) -> Result { if isa.tiles == 0 || isa.min_steps == 0 || cfg.slowdown == 0 || !(cfg.error_budget > 0.0 && cfg.error_budget < 1.0) { return Err(EstimateError::Invalid); } + let (ticks, reaction_ns) = match clock { + Clock::Steps => (0, 0.0), + Clock::Reaction { reaction_ns } => { + if !(reaction_ns > 0.0 && reaction_ns.is_finite()) || toffolis.checked_mul(3).is_none_or(|t| t > isa.min_steps) { + return Err(EstimateError::Invalid); + } + (toffolis, reaction_ns) + } + }; if qec.instruction_set != qubit.instruction_set { return Err(EstimateError::Mismatch); } @@ -684,31 +755,46 @@ pub fn estimate_isa(isa: &IsaCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg: } else { Some(find_factory(qubit, qec, t_target, cfg.limits).ok_or(EstimateError::NoFactory)?) }; - let mut steps = isa.min_steps.saturating_mul(cfg.slowdown); + // A tick of a slowed program: the reaction time, stretched as its steps are. + let stretched = reaction_ns * cfg.slowdown as f64; + let mut steps = (isa.min_steps - 3 * ticks).saturating_mul(cfg.slowdown); loop { - let distance = distance_for(qec, qubit, isa.tiles, steps, budget.logical, cfg.limits.max_distance)?; + let distance = if ticks == 0 { + distance_for(qec, qubit, isa.tiles, steps, budget.logical, cfg.limits.max_distance)? + } else { + distance_for_ticks(qec, qubit, isa.tiles, steps, ticks as f64 * stretched, budget.logical, cfg.limits.max_distance)? + }; let step_ns = qec.step_ns(qubit, distance); - let runtime_ns = step_ns * steps as f64; + let runtime_ns = if ticks == 0 { step_ns * steps as f64 } else { step_ns * steps as f64 + stretched * ticks as f64 }; // The program must outlast one factory run. if let Some(f) = &factory && runtime_ns < f.duration_ns { - let need = ceil_pos(f.duration_ns / step_ns); + let need = if ticks == 0 { ceil_pos(f.duration_ns / step_ns) } else { steps + ceil_pos((f.duration_ns - runtime_ns) / step_ns) }; if need > steps { steps = need; continue; } } - let (factories, factory_qubits) = match &factory { + let (factories, factory_qubits, tick_ns) = match &factory { Some(f) => { - let n = ceil_pos(isa.t_states as f64 * f.duration_ns / (f.outputs as f64 * runtime_ns)); - (n, n * f.qubits) + let mut n = ceil_pos(isa.t_states as f64 * f.duration_ns / (f.outputs as f64 * runtime_ns)); + let mut tick_ns = 0.0; + if ticks > 0 { + // A CCZ state is four T states: enough factories that one + // comes every tick, so the supply keeps up with the + // reaction time. + let per_factory = 4.0 * f.duration_ns / f.outputs as f64; + n = n.max(ceil_pos(per_factory / stretched)); + tick_ns = reaction_tick(stretched, per_factory / n as f64); + } + (n, n * f.qubits, tick_ns) } - None => (0, 0), + None => (0, 0, if ticks > 0 { stretched } else { 0.0 }), }; let tile_qubits = qec.tile_qubits(distance); let algorithm_qubits = isa.tiles * tile_qubits; - return Ok(Estimate { + let estimate = Estimate { qubit: qubit.name.clone(), code: qec.name.clone(), isa: *isa, @@ -725,10 +811,25 @@ pub fn estimate_isa(isa: &IsaCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg: algorithm_qubits, factory_qubits, physical_qubits: algorithm_qubits + factory_qubits, - }); + }; + return Ok(Clocked { estimate, clock, ticks, tick_ns }); } } +/// Smallest odd distance with `tiles · (steps + tick_time/τ(d)) · P(d) ≤ +/// budget`: the tiles wait out the ticks too, a step's error every `τ(d)`. +fn distance_for_ticks(qec: &QecModel, qubit: &PhysicalQubit, tiles: u64, steps: u64, tick_time_ns: f64, budget: f64, max: u32) -> Result { + let mut d = 1; + while d <= max { + let volume = tiles as f64 * (steps as f64 + tick_time_ns / qec.step_ns(qubit, d)); + if volume * qec.logical_error(qubit.clifford_error, d) <= budget { + return Ok(d); + } + d += 2; + } + Err(EstimateError::DistanceExceeded) +} + /// Smallest odd distance with `tiles · steps · P(d) ≤ budget`. fn distance_for(qec: &QecModel, qubit: &PhysicalQubit, tiles: u64, steps: u64, budget: f64, max: u32) -> Result { let volume = tiles as f64 * steps as f64; @@ -749,6 +850,24 @@ pub fn estimate_best(c: &LogicalCounts, qubit: &PhysicalQubit, cfg: &EstimateCon estimate_best_isa(&isa, qubit, cfg) } +/// [`estimate_best_isa`] on `clock`: the cheapest by space-time volume over +/// every code the qubit supports. +pub fn estimate_best_isa_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubit, cfg: &EstimateConfig, clock: Clock) -> Result { + let mut best: Option = None; + let mut last_err = EstimateError::Mismatch; + for qec in QecModel::for_qubit(qubit) { + match estimate_isa_clocked(isa, toffolis, qubit, &qec, cfg, clock) { + Ok(e) => { + if best.as_ref().is_none_or(|b| e.estimate.volume() < b.estimate.volume()) { + best = Some(e); + } + } + Err(e) => last_err = e, + } + } + best.ok_or(last_err) +} + /// [`estimate_best`] from ISA-level counts. pub fn estimate_best_isa(isa: &IsaCounts, qubit: &PhysicalQubit, cfg: &EstimateConfig) -> Result { let mut best: Option = None; @@ -922,6 +1041,45 @@ mod tests { /// The summary rows for factoring and chemistry: distance and factory /// count exactly; physical qubits and runtime to the printed figures. + #[test] + fn the_step_clock_is_the_estimate_bit_for_bit_and_the_reaction_clock_keeps_pace() { + let c = LogicalCounts { qubits: 12_581, measurements: 1_080_000_000, rotations: 12, rotation_depth: 12, t_gates: 12, ccz: 3_730_000_000 }; + let isa = layout(&c, 1.0 / 3.0); + let mut reacted = 0; + for q in [PhysicalQubit::ns_e4(), PhysicalQubit::ns_e3(), PhysicalQubit::us_e3()] { + for s in [1, 4] { + let cfg = EstimateConfig { slowdown: s, ..EstimateConfig::new(1.0 / 3.0) }; + for qec in QecModel::for_qubit(&q) { + let plain = estimate_isa(&isa, &q, &qec, &cfg); + let clocked = estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Steps); + assert_eq!(plain, clocked.clone().map(|x| x.estimate)); + if let Ok(x) = &clocked { + assert_eq!((x.ticks, x.tick_ns), (0, 0.0)); + } + let Ok(r) = estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns: 10_000.0 }) else { continue }; + let e = &r.estimate; + assert_eq!((r.ticks, r.tick_ns), (c.ccz, 10_000.0 * s as f64)); + assert_eq!(e.runtime_ns, e.step_ns * e.steps as f64 + r.tick_ns * r.ticks as f64); + assert_eq!(e.steps, (isa.min_steps - 3 * c.ccz) * s); + let f = e.factory.as_ref().unwrap(); + assert!(4.0 * f.duration_ns / (f.outputs as f64 * e.factories as f64) <= r.tick_ns, "the supply keeps up with the reaction time"); + assert!(e.runtime_ns < plain.as_ref().unwrap().runtime_ns, "a tick is shorter than three steps on these qubits"); + reacted += 1; + } + } + } + assert!(reacted >= 6, "{reacted}"); + // Too many Toffolis for the counts, or no reaction time: refused. + let cfg = EstimateConfig::new(1.0 / 3.0); + let q = PhysicalQubit::ns_e4(); + let qec = QecModel::surface_gate(); + assert_eq!(estimate_isa_clocked(&isa, isa.min_steps, &q, &qec, &cfg, Clock::Reaction { reaction_ns: 1.0 }), Err(EstimateError::Invalid)); + assert_eq!(estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns: 0.0 }), Err(EstimateError::Invalid)); + // The rule quantum_arch's clock takes: the slower of the two. + assert_eq!(reaction_tick(10.0, 33.75), 33.75); + assert_eq!(reaction_tick(10.0, 9.6), 10.0); + } + #[test] fn the_summary_rows_are_reproduced() { const YEAR: f64 = 365.25 * DAY; diff --git a/wai/wai-quantum/src/quantum_resource_receipt.rs b/wai/wai-quantum/src/quantum_resource_receipt.rs index 27b9bf4a3..26e2e7e3b 100644 --- a/wai/wai-quantum/src/quantum_resource_receipt.rs +++ b/wai/wai-quantum/src/quantum_resource_receipt.rs @@ -22,11 +22,20 @@ //! The receipt is a JWP profile like the crate's others (Merkle root over leaf //! hashes, Ed25519). It can carry an energy-class label //! (`quantum_energy::Labelled`). +//! +//! **Clocks.** A job runs on the step clock unless a [`ClockedJob`] names +//! another. On the step clock a clocked job is the job itself: the same hash, +//! the same estimate, the same receipt as before clocks were named. On the +//! reaction-limited clock the estimate's `steps` are the lattice-surgery steps +//! alone and its runtime adds the Toffolis' ticks, so the job takes a hash of +//! its own form, `wai:resource-job-clocked\x01` over the job's hash and the +//! clock, and a receipt sealed on one clock never verifies as sealed on the +//! other. use crate::merkle::merkle_root; use crate::quantum_resource::{ - estimate_best, estimate_best_isa, estimate, estimate_isa, Budget, Estimate, EstimateConfig, EstimateError, - Factory, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, Round, SearchLimits, UnitKind, + estimate_best, estimate_best_isa, estimate_best_isa_clocked, estimate, estimate_isa, estimate_isa_clocked, layout, Budget, Clock, Clocked, Estimate, + EstimateConfig, EstimateError, Factory, InstructionSet, IsaCounts, LogicalCounts, PhysicalQubit, QecModel, Round, SearchLimits, UnitKind, }; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use serde_json::{json, Value}; @@ -35,6 +44,7 @@ const DOMAIN_RECEIPT: &[u8] = b"wai:resource-receipt\x01"; const DOMAIN_RECEIPT_ID: &[u8] = b"wai:resource-receipt-id\x01"; const DOMAIN_JOB: &[u8] = b"wai:resource-job\x01"; const DOMAIN_RESULT: &[u8] = b"wai:resource-estimate\x01"; +const DOMAIN_JOB_CLOCKED: &[u8] = b"wai:resource-job-clocked\x01"; /// The program being estimated, at the level it is known. #[derive(Clone, Debug, PartialEq)] @@ -183,6 +193,63 @@ impl ResourceJob { } } +/// A job on a clock: [`ResourceJob`] on [`Clock::Steps`] is the job itself. +#[derive(Clone, Debug, PartialEq)] +pub struct ClockedJob { + pub job: ResourceJob, + pub clock: Clock, +} + +impl ClockedJob { + /// The program's Toffolis, where its counts give them. + pub fn toffolis(&self) -> Option { + match &self.job.program { + Program::Logical(c) => Some(c.ccz), + Program::Counted(c) => Some(c.logical.ccz), + Program::Isa(_) => None, + } + } + + /// The estimate on the clock. Layout-level counts name no Toffolis, so + /// only the step clock takes them. + pub fn run(&self) -> Result { + let j = &self.job; + if self.clock == Clock::Steps { + return j.run().map(|estimate| Clocked { estimate, clock: Clock::Steps, ticks: 0, tick_ns: 0.0 }); + } + let toffolis = self.toffolis().ok_or(EstimateError::Invalid)?; + let isa = match &j.program { + Program::Logical(c) => { + if c.qubits == 0 || j.config.error_budget.is_nan() || j.config.error_budget <= 0.0 { + return Err(EstimateError::Invalid); + } + layout(c, j.config.error_budget) + } + Program::Isa(i) => *i, + Program::Counted(c) => c.isa, + }; + match &j.code { + Some(q) => estimate_isa_clocked(&isa, toffolis, &j.qubit, q, &j.config, self.clock), + None => estimate_best_isa_clocked(&isa, toffolis, &j.qubit, &j.config, self.clock), + } + } + + /// The job's hash on the step clock; on another, BLAKE3 over + /// `wai:resource-job-clocked\x01`, the job's hash and the clock. + pub fn job_hash(&self) -> [u8; 32] { + match self.clock { + Clock::Steps => self.job.job_hash(), + Clock::Reaction { reaction_ns } => { + let mut o = DOMAIN_JOB_CLOCKED.to_vec(); + o.extend_from_slice(&self.job.job_hash()); + o.push(2); + f(reaction_ns, &mut o); + *blake3::hash(&o).as_bytes() + } + } + } +} + fn u(x: u64, o: &mut Vec) { o.extend_from_slice(&x.to_be_bytes()); } @@ -371,6 +438,44 @@ impl ResourceReceipt { Ok(r) } + /// [`ResourceReceipt::seal`] for a job on a clock: on the step clock the + /// same receipt, bit for bit. + pub fn seal_clocked( + signer: &SigningKey, + signer_id: impl Into, + job: &ClockedJob, + parent_receipt_hash: Option<[u8; 32]>, + meter: impl FnOnce(&mut dyn FnMut()) -> u64, + ) -> Result { + let mut out = None; + let joules_micro = meter(&mut || out = Some(job.run())); + let estimate = out.ok_or(EstimateError::NotRun)??.estimate; + let mut r = ResourceReceipt { + job_hash: job.job_hash(), + result_hash: result_hash(&estimate), + estimate, + merkle_root: [0; 32], + joules_micro, + parent_receipt_hash, + signer_pubkey: signer.verifying_key().to_bytes(), + signer_id: signer_id.into(), + sig: [0; 64], + }; + r.merkle_root = merkle_root(&r.leaves()); + r.sig = signer.sign(&r.payload_with(None)).to_bytes(); + Ok(r) + } + + /// The re-run half of [`Self::verify_computation_clocked`]. + pub(crate) fn computation_matches_clocked(&self, job: &ClockedJob) -> bool { + job.job_hash() == self.job_hash && job.run().is_ok_and(|c| result_hash(&c.estimate) == self.result_hash) + } + + /// [`Self::verify_computation`] for a job on a clock. + pub fn verify_computation_clocked(&self, job: &ClockedJob) -> bool { + self.computation_matches_clocked(job) && self.verify() + } + fn body_verifies(&self) -> bool { result_hash(&self.estimate) == self.result_hash && merkle_root(&self.leaves()) == self.merkle_root } @@ -606,6 +711,35 @@ mod tests { assert_eq!(r.joules_micro, 1_700); } + #[test] + fn on_the_step_clock_a_clocked_job_is_the_job_and_on_the_reaction_clock_another() { + let j = ResourceJob::default(); + let steps = ClockedJob { job: j.clone(), clock: Clock::Steps }; + assert_eq!(steps.job_hash(), j.job_hash()); + let a = ResourceReceipt::seal(&key(4), "sink", &j, None, unmetered).unwrap(); + let b = ResourceReceipt::seal_clocked(&key(4), "sink", &steps, None, unmetered).unwrap(); + assert_eq!(a, b, "the step clock seals the same receipt, bit for bit"); + let fast = ClockedJob { job: j.clone(), clock: Clock::Reaction { reaction_ns: 10_000.0 } }; + assert_ne!(fast.job_hash(), j.job_hash()); + let r = ResourceReceipt::seal_clocked(&key(4), "sink", &fast, None, unmetered).unwrap(); + assert!(r.verify() && r.verify_computation_clocked(&fast)); + assert!(!r.verify_computation(&j), "a reaction-clock receipt is not the step clock's"); + assert!(!r.verify_computation_clocked(&steps)); + assert!(!a.verify_computation_clocked(&fast)); + let slower = ClockedJob { clock: Clock::Reaction { reaction_ns: 20_000.0 }, ..fast.clone() }; + assert!(!r.verify_computation_clocked(&slower), "the reaction time is bound"); + // The Toffolis leave the steps and run on ticks of the reaction time. + let c = fast.run().unwrap(); + let e = &c.estimate; + assert_eq!((c.ticks, c.tick_ns), (3_730_000_000, 10_000.0)); + assert_eq!(e.runtime_ns, e.step_ns * e.steps as f64 + c.tick_ns * c.ticks as f64); + assert_eq!(e.steps, 1_080_000_000 + 12 + 12 + e.isa.t_per_rotation * 12); + // Layout-level counts name no Toffolis: only the step clock takes them. + let isa = ResourceJob { program: Program::Isa(e.isa), ..j.clone() }; + assert_eq!(ClockedJob { job: isa.clone(), clock: fast.clock }.run(), Err(EstimateError::Invalid)); + assert!(ClockedJob { job: isa, clock: Clock::Steps }.run().is_ok()); + } + #[test] fn the_json_round_trips_to_the_same_bytes() { // With a factory, and a Clifford-only program without one. diff --git a/wai/wai-quantum/tests/data/published_figures.tsv b/wai/wai-quantum/tests/data/published_figures.tsv index 2bbd77606..d63026c0f 100644 --- a/wai/wai-quantum/tests/data/published_figures.tsv +++ b/wai/wai-quantum/tests/data/published_figures.tsv @@ -142,6 +142,8 @@ r21.job.qubits.fitted arXiv:1905.09749 Table 3; its counts in the estimate job 2 r21.job.qubits.source_rule arXiv:1905.09749 Table 3; its counts in the estimate job 20 (megaqubits) 20000000.0 guard 2.757e7 r21.job.hours.assumed arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 6.071e1 r21.job.hours.source_rule arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 6.971e1 +r21.job.hours.reaction arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 7.495e0 +r21.job.qubits.reaction arXiv:1905.09749 Table 3; its counts in the estimate job 20 (megaqubits) 20000000.0 guard 2.005e7 r21.job.factories arXiv:1905.09749 §2.10; its counts in the estimate job 14 CCZ per piece, 28 in all text r21.table3.1024.megaqubits arXiv:1905.09749 Table 3 9.7 9.7 up 0.1 r21.table3.1024.hours arXiv:1905.09749 Table 3 1.3 1.3 up 0.1 @@ -172,6 +174,8 @@ femoco.job.days.table arXiv:2011.03494 §IV.C.4; its counts in the estimate job femoco.job.days.layout arXiv:2011.03494 §IV.C.4; its counts in the estimate job 3 3.0 guard 6.751e0 femoco.job.qubits.e4 arXiv:2011.03494 §IV.C.5; its counts in the estimate job "one million" 1000000.0 guard 9.682e5 femoco.job.days.e4 arXiv:2011.03494 §IV.C.5; its counts in the estimate job 1.5 1.5 guard 3.492e0 +femoco.job.days.reaction arXiv:2011.03494 §IV.C.4; its counts in the estimate job 3 3.0 guard 7.759e-1 +femoco.job.qubits.reaction arXiv:2011.03494 §IV.C.4; its counts in the estimate job ≈ 4·10⁶ 4000000.0 guard 1.080e7 r25.qubits arXiv:2505.15917 §3.2 897,864 897864 guard 9.585e5 r25.hours arXiv:2505.15917 §3.2 12.07 12.07 guard 1.058e1 r25.days arXiv:2505.15917 §3.2 4.96 4.96 guard 4.305e0 -- 2.54.0 (Apple Git-157) From 20890225f065d4ccfae627c8929c8f9abf92fdcf Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 03:38:59 -0500 Subject: [PATCH 2/4] wai-quantum: the reaction clock, as its review asked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What the source says. - arXiv:1905.09749 §2.11's "approximately 7 hours" is not a serial figure. Its 37 ms a lookup addition is the parallel basis of the paper's 5.1 hours: the lookup at half a beat an entry, and one runway piece's addition with the pieces side by side. It reaches 7 by rounding. Eq. (1)'s 503,808 lookup additions become 0.1·n_e·n, then 3.7·n_e·n ms becomes 4·n_e·n, which is 6.99 hours; unrounded, 37 ms gives 5.18. - The paper prints no serial figure. Its equations, at 10 µs a Toffoli one after another, give 7.3 hours (5,207 Toffolis a lookup addition), and the row, the page and the change note give that as our derivation. - The job's 20.05 M qubits are at d = 25 (a new row). They are near the paper's 20 M only by chance. - Of FeMoCo's 157 T factories against four, the tick is ×3.8 and the kind of factory ×10. At the paper's 38.75 µs the job keeps 41 15-to-1 units, each 1,568 µs to a CCZ state against its 155. What the engine takes. - A reaction includes the logical measurement it reacts to (App. B.1). A reaction time shorter than the qubit's measurement is refused (QE08), and the 100 µs qubits have no 10 µs tick. - `--qubit all` lists such a qubit as refused, with the reason, beside the others. - A machine whose qubits pass 64 bits is refused (checked arithmetic; ceil_pos saturates), where it once wrapped into a signed receipt. - The tool's schema and estimate.v1.json give the clock the engine's bounds: below 10⁹ µs, and no other member. The refusal message says so. - The answer echoes the reaction time as the request wrote it, since t·10³/10³ is not always t. What a reader sees. - A reaction-clock receipt names its clock: `tick_time_ns` and a `clock` member, read from the figures it signs. A step-clock receipt is byte-identical to before. from_json refuses a `clock` member that disagrees with the figures, and compares bits, since the crate's JSON parser need not round floats back. - `wai-quantum verify` prints the clock, and so do the estimate report and its `--qubit all` table. - `Estimate::steps` says what it counts on the reaction clock. - The README row, the request lists and the tool's description name the clock. Tidying. - The Rounding bullet is back under Honest boundaries. - The clock test has its own doc comment, and the summary-rows test has its comment back. New tests, each failing on the code before it, with the CLI's and the schema check's new calls: - a_reaction_time_is_never_shorter_than_the_qubit_s_measurement - a_machine_past_sixty_four_bits_is_refused_and_not_wrapped - a_reaction_clock_receipt_names_its_clock_and_a_step_clock_receipt_is_unchanged - a_reaction_time_shorter_than_a_measurement_is_refused - the_answer_echoes_the_reaction_time_as_written - the_schemas_say_of_the_clock_what_the_engine_takes - section_2_11_is_the_parallel_basis_rounded_and_the_serial_figure_is_ours - the_reaction_rows_say_what_they_rest_on - the_reaction_clock_reaches_the_request_and_the_report_names_it The existing clock test now counts four reaction runs where it allowed six or more: the 100 µs qubit's are refused. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-estimate.yml | 10 +- .../2026-11-06-estimate-reaction-clock.md | 20 ++- wai/quantum-studio/src/engine.js | 2 +- wai/wai-quantum-cli/docs/llms-full.txt | 12 +- wai/wai-quantum-cli/docs/local-mcp.json | 55 +++++++- wai/wai-quantum-cli/docs/service-mcp.json | 55 +++++++- wai/wai-quantum-cli/src/estimate.rs | 90 ++++++++++++- wai/wai-quantum-cli/src/lab.rs | 10 ++ wai/wai-quantum-py/python/wai_quantum/_api.py | 5 +- wai/wai-quantum-py/tests/test_engine.py | 10 +- wai/wai-quantum-service/README.md | 2 +- wai/wai-quantum/ci/check_schemas.py | 1 + wai/wai-quantum/docs/estimate.md | 16 ++- .../docs/published-architectures.md | 34 +++-- wai/wai-quantum/schemas/estimate.v1.json | 24 +++- wai/wai-quantum/src/engine/estimate/mod.rs | 114 +++++++++++++--- .../src/published_architectures.rs | 126 +++++++++++++++++- wai/wai-quantum/src/quantum_resource.rs | 78 ++++++++--- .../src/quantum_resource_receipt.rs | 94 ++++++++++++- .../tests/data/published_figures.tsv | 1 + wai/wai-web/src/quantum_engine.rs | 2 +- 21 files changed, 668 insertions(+), 93 deletions(-) diff --git a/.github/workflows/wai-quantum-estimate.yml b/.github/workflows/wai-quantum-estimate.yml index 016b840b2..209780c2e 100644 --- a/.github/workflows/wai-quantum-estimate.yml +++ b/.github/workflows/wai-quantum-estimate.yml @@ -97,7 +97,13 @@ jobs: quantum_resource_receipt::tests::on_the_step_clock_a_clocked_job_is_the_job_and_on_the_reaction_clock_another \ engine::estimate::tests::without_a_clock_the_answer_is_the_step_clock_s_and_names_none \ engine::estimate::tests::the_reaction_clock_runs_each_toffoli_in_a_tick_with_factories_to_keep_up \ - engine::estimate::tests::a_reaction_clock_estimate_is_sealed_as_a_clocked_job; do + engine::estimate::tests::a_reaction_clock_estimate_is_sealed_as_a_clocked_job \ + quantum_resource::tests::a_reaction_time_is_never_shorter_than_the_qubit_s_measurement \ + quantum_resource::tests::a_machine_past_sixty_four_bits_is_refused_and_not_wrapped \ + quantum_resource_receipt::tests::a_reaction_clock_receipt_names_its_clock_and_a_step_clock_receipt_is_unchanged \ + engine::estimate::tests::a_reaction_time_shorter_than_a_measurement_is_refused \ + engine::estimate::tests::the_answer_echoes_the_reaction_time_as_written \ + engine::estimate::tests::the_schemas_say_of_the_clock_what_the_engine_takes; do grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/estimate.log" || { echo "::error::$t did not run and pass"; exit 1; } done - name: Published architectures, re-estimated, and the page that prints them @@ -110,6 +116,8 @@ jobs: for t in published_architectures::tests::the_page_is_what_the_estimators_give \ published_architectures::tests::every_row_holds_to_its_published_figure \ published_architectures::tests::the_ours_column_holds_only_ours_and_the_text_says_what_the_sources_say \ + published_architectures::tests::section_2_11_is_the_parallel_basis_rounded_and_the_serial_figure_is_ours \ + published_architectures::tests::the_reaction_rows_say_what_they_rest_on \ quantum_bicycle::tests::the_cost_tables_are_what_the_search_finds \ quantum_bicycle::tests::the_source_s_mean_costs_come_from_taking_z_by_the_inverse \ quantum_bicycle::tests::the_fits_give_the_printed_rates \ diff --git a/wai/changes/2026-11-06-estimate-reaction-clock.md b/wai/changes/2026-11-06-estimate-reaction-clock.md index d52fc9a24..20080a349 100644 --- a/wai/changes/2026-11-06-estimate-reaction-clock.md +++ b/wai/changes/2026-11-06-estimate-reaction-clock.md @@ -4,8 +4,12 @@ --reaction-us t`): each Toffoli has its Clifford fixups teleported away from the data and takes one tick of the reaction time, its three logical steps gone, with enough factories that a CCZ state comes every tick. The figures add `clock` (`reaction_us`, `tick_us`, - `toffolis`); `steps` are then the lattice-surgery steps alone. Without a clock, or with - `"steps"`, the answer and its receipts are what they were, bit for bit. + `toffolis`); `steps` are then the lattice-surgery steps alone. A reaction includes the + measurement it reacts to (arXiv:1905.09749 App. B.1), so a reaction time shorter than a + measurement on the qubit is refused (`QE08`); `--qubit all` shows such a qubit as refused + beside the others. A machine whose qubits pass 64 bits is refused where it would have + wrapped. Without a clock, or with `"steps"`, the answer and its receipts are what they + were, bit for bit. - The tick is one rule, `quantum_resource::reaction_tick`, which `quantum_arch::Timing` takes too (`quantum_arch` now enables `quantum_resource`). `estimate_isa_clocked`, `estimate_best_isa_clocked`, `Clock` and `Clocked` price it; on the step clock they are @@ -14,7 +18,13 @@ (`wai:resource-job-clocked\x01` over the job's hash and the reaction time), so a receipt sealed on one clock never verifies as sealed on the other. `ResourceReceipt::seal_clocked` and `verify_computation_clocked` take it; on the step - clock they seal and check what `seal` and `verify_computation` do. + clock they seal and check what `seal` and `verify_computation` do. Such a receipt names + its clock itself: `ResourceReceipt::tick_time_ns` and a `clock` member, read from the + figures it signs, give the runtime its steps do not account for, and `wai-quantum verify` + prints it. `wai-quantum estimate` names the clock in its report and its `--qubit all` + table. - The published-architecture check runs arXiv:1905.09749's counts through the job at its - 10 µs: 7.50 hours, every Toffoli after the one before, against the step clock's 60.7, and - the paper's own §2.11 estimate on that basis, "approximately 7 hours". + 10 µs: 7.50 hours at d = 25, every Toffoli after the one before, against the step clock's + 60.7 and the paper's 5.1. The paper's §2.11 "approximately 7 hours" is not on that basis: + it is the 5.1 hours' parallel basis, rounded. The paper prints no serial figure; its + equations, at 10 µs a Toffoli one after another, give 7.3 hours, which is our derivation. diff --git a/wai/quantum-studio/src/engine.js b/wai/quantum-studio/src/engine.js index 5927b7a46..0c070f5ad 100644 --- a/wai/quantum-studio/src/engine.js +++ b/wai/quantum-studio/src/engine.js @@ -76,7 +76,7 @@ export class TabEngine { targets() { return this.call('targets'); } target(spec) { return this.call('target', { spec }); } targetPlace(qasm, target) { return this.call('target_place', { qasm, target }); } - // A resource estimate ({qasm, budget, qubit, calibration, max_slowdown}): every + // A resource estimate ({qasm, budget, qubit, calibration, max_slowdown, clock}): every // estimate sealed with this browser's key; estimateVerify checks the seals and re-runs it. estimate(request) { return this.call('estimate', { request }); } estimateVerify(body) { return this.call('estimate_verify', { body }); } diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 12e176683..512135f59 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -172,7 +172,7 @@ Parameters: ### `estimate_resources`: Estimate a program's fault-tolerant resources -What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it. +What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it. Parameters: - `qasm` (string): The program, OpenQASM 3 or 2, or a generator (integer input, const, for loops) with its values in "inputs". @@ -183,7 +183,7 @@ Parameters: - `qubit` (any): The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described. - `calibration` (string, one of exact_matching, union_find, assumed): The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one. - `max_slowdown` (integer): The most the frontier slows the program by to need fewer factories (default 256). -- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up. +- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up. - `export` (string, one of counts, routine): Also give the program's counts as a logical-counts or a routine document, under export. - `name` (string): The program's name in a routine document. @@ -764,7 +764,7 @@ Parameters: ### `estimate_resources`: Estimate a quantum program's fault-tolerant resources -What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command. +What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command. Parameters: - `counts` (object): A logical-counts document instead of a program: numQubits, tCount, rotationCount, rotationDepth, cczCount, ccixCount, measurementCount. @@ -773,7 +773,7 @@ Parameters: - `qubit` (any): The physical qubit: a reference qubit (ns-e4, the default: 50 ns gates, 1e-4 error; us-* are 100 µs gates, meas-* measurement-based), or one described. - `calibration` (string, one of exact_matching, union_find, assumed): The gate-based surface code's model: fitted to this engine's exact-matching decoder (default) or its union-find decoder, or the planar model's assumed one. - `max_slowdown` (integer): The most the frontier slows the program by to need fewer factories (default 256). -- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up. +- `clock` (any): How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {"reaction_us": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up. - `export` (string, one of counts, routine): Also give the program's counts as a logical-counts or a routine document, under export. - `name` (string): The program's name in a routine document. - `qasm` (string): The program, OpenQASM 3 or 2. Give this, counts or routine. @@ -2547,8 +2547,8 @@ Options: --reaction-us Run each Toffoli on the reaction-limited clock: its Clifford fixups teleported away, one - tick of this reaction time (µs), with enough factories to keep up, instead of three - logical steps + tick of this reaction time (µs, no shorter than a measurement on the qubit), with enough + factories to keep up, instead of three logical steps --export Print the program's counts as a document instead of the report: counts (the logical-counts diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index 1b1cf2972..7853b3c5d 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -19193,7 +19193,7 @@ { "name": "estimate_resources", "title": "Estimate a program's fault-tolerant resources", - "description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it.", + "description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with this user's key; verify_quantum_result checks the seals and re-runs it.", "inputSchema": { "type": "object", "properties": { @@ -19293,15 +19293,17 @@ "properties": { "reaction_us": { "type": "number", - "exclusiveMinimum": 0 + "exclusiveMinimum": 0, + "exclusiveMaximum": 1000000000.0 } }, "required": [ "reaction_us" - ] + ], + "additionalProperties": false } ], - "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up." + "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up." }, "export": { "type": "string", @@ -19379,7 +19381,8 @@ "properties": { "reaction_us": { "type": "number", - "exclusiveMinimum": 0 + "exclusiveMinimum": 0, + "exclusiveMaximum": 1000000000 } }, "additionalProperties": false @@ -19620,6 +19623,27 @@ "kind": { "const": "quantum-resource" }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.", + "required": [ + "kind", + "tick_time_ns_bits", + "tick_time_s" + ], + "properties": { + "kind": { + "const": "reaction" + }, + "tick_time_ns_bits": { + "type": "string" + }, + "tick_time_s": { + "type": "number" + } + }, + "additionalProperties": false + }, "estimate": { "type": "object" }, @@ -19776,6 +19800,27 @@ "kind": { "const": "quantum-resource" }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.", + "required": [ + "kind", + "tick_time_ns_bits", + "tick_time_s" + ], + "properties": { + "kind": { + "const": "reaction" + }, + "tick_time_ns_bits": { + "type": "string" + }, + "tick_time_s": { + "type": "number" + } + }, + "additionalProperties": false + }, "estimate": { "type": "object" }, diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index 537a421e5..cf8ac6a0a 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -40434,7 +40434,7 @@ { "name": "estimate_resources", "title": "Estimate a quantum program's fault-tolerant resources", - "description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command.", + "description": "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate. Sealed with the service's key; verify_quantum_receipt checks the seals and re-runs it. A program with more than 256 distinct rotation angles, or a budget too fine for its rotations, is refused with the local command.", "inputSchema": { "type": "object", "properties": { @@ -40523,15 +40523,17 @@ "properties": { "reaction_us": { "type": "number", - "exclusiveMinimum": 0 + "exclusiveMinimum": 0, + "exclusiveMaximum": 1000000000.0 } }, "required": [ "reaction_us" - ] + ], + "additionalProperties": false } ], - "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up." + "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up." }, "export": { "type": "string", @@ -40616,7 +40618,8 @@ "properties": { "reaction_us": { "type": "number", - "exclusiveMinimum": 0 + "exclusiveMinimum": 0, + "exclusiveMaximum": 1000000000 } }, "additionalProperties": false @@ -40857,6 +40860,27 @@ "kind": { "const": "quantum-resource" }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.", + "required": [ + "kind", + "tick_time_ns_bits", + "tick_time_s" + ], + "properties": { + "kind": { + "const": "reaction" + }, + "tick_time_ns_bits": { + "type": "string" + }, + "tick_time_s": { + "type": "number" + } + }, + "additionalProperties": false + }, "estimate": { "type": "object" }, @@ -41013,6 +41037,27 @@ "kind": { "const": "quantum-resource" }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.", + "required": [ + "kind", + "tick_time_ns_bits", + "tick_time_s" + ], + "properties": { + "kind": { + "const": "reaction" + }, + "tick_time_ns_bits": { + "type": "string" + }, + "tick_time_s": { + "type": "number" + } + }, + "additionalProperties": false + }, "estimate": { "type": "object" }, diff --git a/wai/wai-quantum-cli/src/estimate.rs b/wai/wai-quantum-cli/src/estimate.rs index 0f620dcfc..3817499b2 100644 --- a/wai/wai-quantum-cli/src/estimate.rs +++ b/wai/wai-quantum-cli/src/estimate.rs @@ -45,7 +45,7 @@ pub struct Args { /// The most the frontier slows the program by, to need fewer factories (it tries powers of two up to this) #[arg(long, default_value_t = estimate::DEFAULT_MAX_SLOWDOWN)] pub max_slowdown: u64, - /// Run each Toffoli on the reaction-limited clock: its Clifford fixups teleported away, one tick of this reaction time (µs), with enough factories to keep up, instead of three logical steps + /// Run each Toffoli on the reaction-limited clock: its Clifford fixups teleported away, one tick of this reaction time (µs, no shorter than a measurement on the qubit), with enough factories to keep up, instead of three logical steps #[arg(long, value_name = "MICROSECONDS")] pub reaction_us: Option, /// Print the program's counts as a document instead of the report: counts (the logical-counts document) or routine (the routine document); the report goes to standard error @@ -121,6 +121,37 @@ fn headline(qubit: &str, e: &Value) -> String { ) } +/// What an answer on the reaction-limited clock must say of it: the clock +/// moves the runtime and the factories more than any other option. +fn clock_line(body: &Value) -> Option { + let t = body["result"]["options"]["clock"]["reaction_us"].as_f64()?; + Some(format!("reaction-limited: each Toffoli one tick of {t} µs, with enough factories to keep up, not three logical steps")) +} + +/// The sealed answers, and each qubit that stood aside with its reason. +type Sealed = (Vec, Vec<(String, String)>); + +/// The requests sealed. With several and a reaction time, a reference qubit +/// that time cannot run on stands aside with its reason, so `--qubit all` +/// still shows the others; with one, or if none can, the refusal stands. +fn sealed_each(reqs: &[Value], key: &wai_quantum::ed25519_dalek::SigningKey) -> Result { + let (mut bodies, mut refused) = (Vec::new(), Vec::new()); + for q in reqs { + if reqs.len() > 1 { + let options = estimate::Options::read(q)?; + if let Some(why) = estimate::clock_refusal(estimate::clock_of(q)?, &options.qubit) { + refused.push((options.qubit_name, why)); + continue; + } + } + bodies.push(estimate::sealed(q, key)?); + } + match refused.first() { + Some((_, why)) if bodies.is_empty() => Err(why.clone()), + _ => Ok((bodies, refused)), + } +} + /// The estimate as a reader takes it in. pub fn report(body: &Value) -> String { let r = &body["result"]; @@ -163,6 +194,9 @@ pub fn report(body: &Value) -> String { } else { format!("calibration {}\n", s(&cal["decoder"])) }; + if let Some(line) = clock_line(body) { + out += &format!("clock {line}\n"); + } out += &format!("estimate {}\n", headline(&s(&r["options"]["qubit"]["name"]), &r["estimate"])); let f = r["frontier"].as_array().cloned().unwrap_or_default(); if f.len() > 1 { @@ -179,7 +213,7 @@ pub fn main(a: Args) { let r = (|| -> Result<(), String> { let reqs = requests(&a)?; let k = super::load_key(&a.key); - let bodies: Vec = reqs.iter().map(|q| estimate::sealed(q, &k.key)).collect::>()?; + let (bodies, refused) = sealed_each(&reqs, &k.key)?; let write = |text: &str| -> Result<(), String> { match &a.out { Some(p) => std::fs::write(p, text).map_err(|e| format!("{}: {e}", p.display())), @@ -190,7 +224,10 @@ pub fn main(a: Args) { } }; if a.json { - let v = if bodies.len() == 1 { bodies[0].clone() } else { Value::Array(bodies) }; + for (q, why) in &refused { + eprintln!("{q}: {why}"); + } + let v = if reqs.len() == 1 { bodies[0].clone() } else { Value::Array(bodies) }; return write(&format!("{v}\n")); } if a.export.is_some() { @@ -198,12 +235,18 @@ pub fn main(a: Args) { eprint!("{}", report(&bodies[0])); return Ok(()); } - if bodies.len() > 1 { + if reqs.len() > 1 { + if let Some(line) = clock_line(&bodies[0]) { + println!("clock: {line}"); + } println!("{:<10} {:<36} {:>4} {:>9} {:>15} runtime", "qubit", "code", "d", "factories", "physical"); for b in &bodies { let e = &b["result"]["estimate"]; println!("{:<10} {:<36} {:>4} {:>9} {:>15} {}", s(&b["result"]["options"]["qubit"]["name"]), s(&e["code"]), s(&e["distance"]), s(&e["factories"]), s(&e["physical_qubits"]), secs(&e["runtime_s"])); } + for (q, why) in &refused { + println!("{q:<10} refused: {why}"); + } return Ok(()); } print!("{}", report(&bodies[0])); @@ -250,4 +293,43 @@ mod tests { assert!(super::super::Cli::try_parse_from(["wai-quantum", "estimate", "p.qasm", "--counts", "c.json"]).is_err()); assert!(super::super::Cli::try_parse_from(["wai-quantum", "estimate", "--routine", "r.json", "--export", "counts"]).is_ok()); } + + /// `--reaction-us` reaches the request, the report names the clock, and + /// on every qubit the time cannot run on, the table says why. + #[test] + fn the_reaction_clock_reaches_the_request_and_the_report_names_it() { + let path = std::env::temp_dir().join(format!("wai-quantum-estimate-clock-{}.json", std::process::id())); + std::fs::write(&path, r#"{"numQubits": 100, "cczCount": 1000000}"#).unwrap(); + let args = |rest: &[&str]| { + let mut v = vec!["wai-quantum", "estimate", "--counts", path.to_str().unwrap(), "--max-slowdown", "1"]; + v.extend_from_slice(rest); + let c = super::super::Cli::try_parse_from(v).unwrap(); + let super::super::Command::Estimate(a) = c.command else { panic!("not estimate") }; + requests(&a).unwrap() + }; + let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[4; 32]); + + let fast = args(&["--qubit", "ns-e4", "--reaction-us", "10"]); + assert_eq!(fast[0]["clock"], json!({ "reaction_us": 10.0 })); + let body = estimate::sealed(&fast[0], &key).unwrap(); + let text = report(&body); + assert!(text.contains("clock reaction-limited: each Toffoli one tick of 10 µs"), "{text}"); + let plain = args(&["--qubit", "ns-e4"]); + assert!(plain[0].get("clock").is_none()); + assert!(!report(&estimate::sealed(&plain[0], &key).unwrap()).contains("clock ")); + + // The receipt alone says so too, as `wai-quantum verify` prints it. + let r = wai_quantum::quantum_resource_receipt::ResourceReceipt::from_json(&body["result"]["estimate"]["receipt"].to_string()).unwrap(); + assert!(super::super::lab::resource_clock_line(&r).unwrap().starts_with("reaction-limited")); + + // The 100 µs qubits stand aside; the others are estimated. + let (bodies, refused) = sealed_each(&args(&["--qubit", "all", "--reaction-us", "10"]), &key).unwrap(); + assert_eq!(refused.iter().map(|r| r.0.as_str()).collect::>(), ["us-e3", "us-e4"]); + assert_eq!(bodies.len() + refused.len(), estimate::QUBITS.len()); + assert!(refused[0].1.contains("a reaction includes the measurement"), "{}", refused[0].1); + // One qubit alone is refused outright, and so is a time none can run. + assert!(sealed_each(&args(&["--qubit", "us-e3", "--reaction-us", "10"]), &key).is_err()); + assert!(sealed_each(&args(&["--qubit", "all", "--reaction-us", "0.001"]), &key).is_err()); + std::fs::remove_file(&path).ok(); + } } diff --git a/wai/wai-quantum-cli/src/lab.rs b/wai/wai-quantum-cli/src/lab.rs index 37be92be7..9a1d6317e 100644 --- a/wai/wai-quantum-cli/src/lab.rs +++ b/wai/wai-quantum-cli/src/lab.rs @@ -457,6 +457,13 @@ fn estimate_json(e: &qr::Estimate) -> String { ) } +/// What a resource receipt's signed figures say of its clock: on the +/// reaction-limited clock the runtime is more than its steps. +pub(crate) fn resource_clock_line(r: &ResourceReceipt) -> Option { + let ticks = r.tick_time_ns(); + (ticks > 0.0).then(|| format!("reaction-limited: {} of the runtime is Toffoli ticks, not steps", human_time(ticks))) +} + /// Check a receipt of any kind this tool or the library signs: `src` is its /// JSON. Prints what it binds, and exits 1 when it does not verify. pub fn verify_receipt(src: &str) { @@ -473,6 +480,9 @@ pub fn verify_receipt(src: &str) { println!("qubit {} ({}, d = {})", e.qubit, e.code, e.distance); println!("physical {} qubits", e.physical_qubits); println!("runtime {}", human_time(e.runtime_ns)); + if let Some(clock) = resource_clock_line(r) { + println!("clock {clock}"); + } println!("factories {}", e.factories); println!("joules {}", joules_line(r.joules_micro)); println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro)); diff --git a/wai/wai-quantum-py/python/wai_quantum/_api.py b/wai/wai-quantum-py/python/wai_quantum/_api.py index c70e8dbc2..ba8d30723 100644 --- a/wai/wai-quantum-py/python/wai_quantum/_api.py +++ b/wai/wai-quantum-py/python/wai_quantum/_api.py @@ -353,8 +353,9 @@ def estimate( reference qubit's name, ``ns-e4`` by default, or a dict describing one) and the frontier between physical qubits and runtime (slowed up to ``max_slowdown``). With ``reaction_us`` each Toffoli runs on the - reaction-limited clock, one tick of that reaction time in µs, instead of - three logical steps. Every estimate is sealed with ``key`` in the + reaction-limited clock, one tick of that reaction time in µs (no shorter + than a measurement on the qubit), instead of three logical steps. Every + estimate is sealed with ``key`` in the resource-estimate receipt family; ``verify_estimate`` checks the seals and re-runs it. ``export`` (``counts`` or ``routine``) adds the program's counts as a document, named ``name``. A request the engine diff --git a/wai/wai-quantum-py/tests/test_engine.py b/wai/wai-quantum-py/tests/test_engine.py index 219bc565c..8d0fee9ef 100644 --- a/wai/wai-quantum-py/tests/test_engine.py +++ b/wai/wai-quantum-py/tests/test_engine.py @@ -392,12 +392,16 @@ def test_an_estimate_counts_the_program_seals_every_figure_and_re_runs(): def test_an_estimate_on_the_reaction_clock_runs_each_toffoli_in_a_tick(): steps = wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO)["result"]["estimate"] - e = wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO, reaction_us=10) + e = wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO, reaction_us=100) r = e["result"] - assert r["options"]["clock"] == {"reaction_us": 10.0} - assert r["estimate"]["clock"] == {"reaction_us": 10.0, "tick_us": 10.0, "toffolis": 3} + assert r["options"]["clock"] == {"reaction_us": 100.0} + assert r["estimate"]["clock"] == {"reaction_us": 100.0, "tick_us": 100.0, "toffolis": 3} assert "clock" not in steps and r["estimate"]["runtime_s"] < steps["runtime_s"] + assert r["estimate"]["receipt"]["clock"]["kind"] == "reaction" assert wq.verify_estimate(e)["valid"] + # A reaction includes a measurement, and this qubit's takes 100 µs. + with pytest.raises(wq.EngineError): + wq.estimate(LADDER, qubit="us-e3", budget=1e-4, max_slowdown=1, key=DEMO, reaction_us=10) def test_an_estimate_reads_and_writes_the_interchange_documents(): diff --git a/wai/wai-quantum-service/README.md b/wai/wai-quantum-service/README.md index db996b0c9..9a94d8a72 100644 --- a/wai/wai-quantum-service/README.md +++ b/wai/wai-quantum-service/README.md @@ -153,7 +153,7 @@ Counts are always seeded samples. | `POST /v1/shortcuts` | `{"qasm"}`: each exact classical route to simulating the program, priced on it, and the cheapest for each question | | `POST /v1/compile` | `{"qasm", "epsilon", "level", "basis"}`: the program compiled through checked passes (to Clifford+T within `epsilon`, by default `1e-6`), with a certificate of every pass and the distance, sealed with the service's key in a job's form so `POST /v1/verify` checks it ([the compiler page](../wai-quantum/docs/compiler.md)) | | `GET /v1/targets` | the built-in device targets (synthetic), each with its qubits, pairs, gates, errors and hash | -| `POST /v1/estimate` | `{"qasm"}` (or `{"counts"}`, `{"routine"}`), with `budget`, `qubit`, `calibration`, `max_slowdown` and `export`, each optional: what a fault-tolerant machine would need to run the program, its logical counts, the estimate and the frontier, each estimate sealed with the service's key in the resource-estimate receipt family and the answer in a job's form; `POST /v1/verify` checks the seals and re-runs it ([the estimate page](../wai-quantum/docs/estimate.md)) | +| `POST /v1/estimate` | `{"qasm"}` (or `{"counts"}`, `{"routine"}`), with `budget`, `qubit`, `calibration`, `max_slowdown`, `clock` and `export`, each optional: what a fault-tolerant machine would need to run the program, its logical counts, the estimate and the frontier, each estimate sealed with the service's key in the resource-estimate receipt family and the answer in a job's form; `POST /v1/verify` checks the seals and re-runs it ([the estimate page](../wai-quantum/docs/estimate.md)) | | `POST /v1/repair` | `{"qasm", "proposed"?, "max_iterations"?, "dyadic_bits"?, "sizes"?}`: the program repaired on the lines its diagnostics flag alone, a proposed repair taken only if it changes no other line, and what needs a person | | `GET /.well-known/wai-quantum-receipt-keys` | every key receipts are signed with, each with its validity window, a retired one's transition to its successor signed by both | | `POST /v1/qec/code` | `{"code", "certify"?, "bound"?, "certificate"?}`: an error-correcting code (`surface:D`, `toric:L`, `steane`, `color:D`, `bb:72` to `bb:288`, `gross`), its checks, `k`, layout and decoders, and its distance certificate, or a given certificate checked against it | diff --git a/wai/wai-quantum/ci/check_schemas.py b/wai/wai-quantum/ci/check_schemas.py index aa28ec2be..b19657a8d 100644 --- a/wai/wai-quantum/ci/check_schemas.py +++ b/wai/wai-quantum/ci/check_schemas.py @@ -185,6 +185,7 @@ def main() -> int: ("qasm_diagnostics", {"code": "Q103"}), ("qasm_diagnostics", {"code": "J021"}), ("estimate_resources", {"qasm": rot, "qubit": "us-e4", "budget": 1e-3, "export": "counts"}), + ("estimate_resources", {"qasm": rot, "qubit": "ns-e4", "budget": 1e-3, "clock": {"reaction_us": 10}}), ("debug_branches", {"qasm": tele}), ("debug_step", {"qasm": tele, "line": 14, "outcomes": [1, 1], "probe": {"qubits": [2]}}), ("list_targets", {}), diff --git a/wai/wai-quantum/docs/estimate.md b/wai/wai-quantum/docs/estimate.md index 8f143871c..507d30440 100644 --- a/wai/wai-quantum/docs/estimate.md +++ b/wai/wai-quantum/docs/estimate.md @@ -74,7 +74,7 @@ receipts 5 estimates sealed by did:key:z6Mk…; they verify and re-run here | `qubit` | `ns-e4` | a reference qubit (below), or `{"name", "instruction_set": "gate"\|"measurement", "gate_ns", "meas_ns", "clifford_error", "t_error"}` | | `calibration` | `exact_matching` | the gate-based surface code's model: `exact_matching`, `union_find` (fitted to the decoders) or `assumed` (the planar model's own) | | `max_slowdown` | `256` | the frontier slows the program by 1, 2, 4, … up to this (at most 4096) | -| `clock` | `"steps"` | how long a Toffoli takes: `"steps"`, three logical steps; or `{"reaction_us": t}`, the reaction-limited clock (below) | +| `clock` | `"steps"` | how long a Toffoli takes: `"steps"`, three logical steps; or `{"reaction_us": t}`, the reaction-limited clock (below), `t` above 0, below 10⁹ and no shorter than a measurement on the qubit | | `export` | none | also give the counts as a `counts` or `routine` document | | `name` | `program` | the program's name in a routine document | @@ -213,7 +213,12 @@ slower of the reaction time `t` and the CCZ supply, the rule least as many as deliver a CCZ state (four T states) every tick, so the supply keeps up and a tick is the reaction time; - a slowed program stretches its ticks as its steps: `s` times the reaction - time. + time; +- a reaction includes the logical measurement it reacts to (arXiv:1905.09749 + App. B.1), so a reaction time shorter than a measurement on the qubit is + refused (`QE08`): the 100 µs qubits have no 10 µs tick. `wai-quantum estimate + --qubit all --reaction-us t` shows such a qubit as refused, with the reason, + beside the others. Without a clock, or with `"steps"`, the answer and its receipts are what they were before the clock was named, bit for bit. @@ -325,7 +330,10 @@ estimate down to each factory round. On the reaction-limited clock the job is a clocked job (`quantum_resource_receipt::ClockedJob`): its hash is a form of its own, `wai:resource-job-clocked\x01` over the job's hash and the reaction time, so a receipt sealed on one clock never verifies as sealed on -the other. The frontier's receipts name the headline's as their parent. The whole result is then sealed in a job's +the other. Such a receipt also says so itself: its `clock` member gives the +runtime its steps do not account for (`tick_time_ns_bits`, `tick_time_s`), read +from the figures it signs, and a step-clock receipt has none. How long one tick +was is the job's. The frontier's receipts name the headline's as their parent. The whole result is then sealed in a job's form, so any surface's `verify` checks it as it checks a job. `engine::estimate::verify` (`wai-quantum verify`, `POST /v1/verify`, @@ -346,7 +354,7 @@ estimates bit for bit, and the same figures, counts and notes. | `QE05` | nothing to estimate: no qubits, or no measurement, `T` gate, rotation or Toffoli (no logical step) | | `QE06` | the physical model refuses: the qubit's error is at or above the code's threshold, no distance up to 51 meets the logical share, or no factory meets the T target | | `QE07` | a document that does not read: a key or a value that is not one, a symbolic value, a repetition, a schema violation | -| `QE08` | a request that does not read: the program, the budget, the qubit, the calibration, the slowdown, the clock or the export | +| `QE08` | a request that does not read: the program, the budget, the qubit, the calibration, the slowdown, the clock or the export; or a reaction time shorter than a measurement on the qubit | ## What it assumes diff --git a/wai/wai-quantum/docs/published-architectures.md b/wai/wai-quantum/docs/published-architectures.md index 625a1c6a3..c87897236 100644 --- a/wai/wai-quantum/docs/published-architectures.md +++ b/wai/wai-quantum/docs/published-architectures.md @@ -105,20 +105,29 @@ Toffoli count. Its title's 8 hours is the re-run's 7.31 hours expected across retries, taken up to one digit. Its hand-worked §2.14 (23 million qubits) rounds as it goes and says so. Its §2.12 reads a 6.4% distillation error from an earlier paper's spreadsheet where its own script's formula gives 0.6%. Our -estimate job, given the same counts, finds the same code distance. On its +estimate job, given the same counts, finds the same code distance on its default step clock, three logical steps of `d` cycles a Toffoli as the planar -model of arXiv:2211.07629 has it, it takes 12 times as long. On its +model of arXiv:2211.07629 has it, and takes 12 times as long. On its reaction-limited clock at the paper's 10 µs, the rule `quantum_arch` takes, it -takes 7.50 hours: every Toffoli after the one before, which is the basis of the -paper's own §2.11 estimate, "approximately 7 hours". The paper's 5.1 hours run -its two runway pieces' additions side by side and its lookups at half a beat an -entry, which counts alone do not carry (**our limit**). +takes 7.50 hours at d = 25: every Toffoli after the one before. The paper does +not run them so, and counts alone do not carry how it does (**our limit**): its +5.1 hours and its §2.11 "approximately 7 hours" are one parallel basis, 37 ms a +lookup addition, with its two runway pieces' additions side by side and its +lookups at half a beat an entry. §2.11 reaches 7 by rounding its lookup additions +up to 0.1·n_e·n and then 3.7·n_e·n ms up to 4·n_e·n; 37 ms on the unrounded count +is 5.18 hours. The paper prints no serial figure. Its equations, at 10 µs a +Toffoli one after another, give 7.3 hours, a figure of ours, not the paper's. The +job's 20.05 million qubits are near the paper's 20 million by chance: 129 T +factories at d = 25, not its CCZ factories at d = 27. **FeMoCo.** Its floorplan, factory, Toffoli rate and 3 days come back on the same clock; its §IV.C.5 says 3.5 days where §IV.C.4 says 3. The estimate job runs at the beat on its step clock; on its reaction-limited clock it keeps -enough factories to tick at 10 µs, where the paper's four factories set its -pace at 38.75 µs. +enough T factories to tick at 10 µs, 0.78 days on 10.8 million qubits, where the +paper's four CCZ factories set its pace at 38.75 µs. Most of that factory count +is the kind of factory, not the tick: at the paper's 38.75 µs the job keeps 41 +of its 15-to-1 units, each about ten times slower to a CCZ state than the +paper's. **RSA-2048 (2025).** As `quantum_arch` documents: its stated durations give 10.6 hours a shot where it prints 12.07, its hot-storage counts disagree with @@ -316,8 +325,9 @@ its own formula, and so on. Its Toffoli count comes back. | physical qubits, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 27,571,456 | +37.86% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | | hours per run, job, assumed code model | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 60.71 | ×11.904 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | | hours per run, the planar model on the source's patch and error rule | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 69.71 | ×13.668 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | -| hours per run, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 7.50 | +46.97% | our limit | The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step: the source's own §2.11 estimate on that basis is "approximately 7 hours". Its cost script runs the two runway pieces' additions side by side and its lookups at half a beat an entry, which counts alone do not carry. | -| physical qubits, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 20,050,780 | +0.25% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. On this clock the job keeps enough factories (129) that a CCZ state comes every tick. | +| code distance, job, reaction-limited clock at 10 µs | arXiv:1905.09749 §2.13 (d = 27); its counts in the estimate job | 27 | 25 | -7.41% | our limit | The tiles wait out 7.50 hours here and 60.71 on the step clock, so a smaller distance meets the same budget. | +| hours per run, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 5.1 | 7.50 | +46.97% | our limit | The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step; the source does not, and counts alone do not carry how it does. Its 5.1 hours and its §2.11 "approximately 7 hours" are one parallel basis, 37 ms a lookup addition, with the two runway pieces' additions side by side and the lookups at half a beat an entry. §2.11 reaches 7 by rounding: eq. (1)'s 503,808 lookup additions as 0.1·n_e·n, then 3.7·n_e·n ms as 4·n_e·n, which is 6.99 hours; 37 ms on the unrounded count is 5.18. The source prints no serial figure. 5,207 Toffolis a lookup addition at 10 µs each would be 7.3 hours: that is our derivation from its equations, not its figure. | +| physical qubits, job, reaction-limited clock at 10 µs | arXiv:1905.09749 Table 3; its counts in the estimate job | 20 (megaqubits) | 20,050,780 | +0.25% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. On this clock the job keeps enough factories (129) that a CCZ state comes every tick, at d = 25: near the source's count by chance, a different machine from its d = 27 and CCZ factories. | | factories, job | arXiv:1905.09749 §2.10; its counts in the estimate job | 14 CCZ per piece, 28 in all | 16 T factories | — | our limit | A CCZ state costs four T states in the planar model. | | 1024 bits: megaqubits, re-run | arXiv:1905.09749 Table 3 | 9.7 | 9.62 | -0.78% | agrees | Its script takes each figure up to two digits. | | 1024 bits: hours per run, re-run | arXiv:1905.09749 Table 3 | 1.3 | 1.26 | -2.79% | agrees | Its script takes each figure up to two digits. | @@ -353,8 +363,8 @@ its own formula, and so on. Its Toffoli count comes back. | days, job, its layout's counts | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 6.75 | ×2.250 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | | physical qubits, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | "one million" | 968,200 | -3.18% | our limit | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. | | days, job, p = 10⁻⁴ | arXiv:2011.03494 §IV.C.5; its counts in the estimate job | 1.5 | 3.49 | ×2.328 | assumption | On its default step clock the estimate job prices the planar architecture of arXiv:2211.07629: three logical steps of `d` cycles for each Toffoli. The source teleports a Toffoli's fixups away and runs it in one tick; the job does so on its reaction-limited clock (the rows below). | -| days, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 0.78 | ×0.259 | assumption | The job keeps enough factories (157) that a CCZ state comes every 10 µs tick; the source chose four, whose supply sets its pace at 38.75 µs (25 kHz). The job's frontier slows the ticks to need fewer. | -| physical qubits, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 10,799,568 | ×2.700 | assumption | As above: more factories, to keep up with the reaction time. | +| days, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | 3 | 0.78 | ×0.259 | assumption | The job ticks at 10 µs where the source's four CCZ factories set a pace of 38.75 µs (25 kHz), and that ratio is this delta. It keeps 157 T factories to do so, and the two counts are different machines: each of the job's is a 15-to-1 unit taking 1,568 µs a CCZ state, each of the source's 155. At the source's own 38.75 µs the job keeps 41, so of the ×39 against four the tick is ×3.8 and the kind of factory ×10. The job's frontier slows the ticks to need fewer. | +| physical qubits, job, its layout's counts, reaction-limited clock at 10 µs | arXiv:2011.03494 §IV.C.4; its counts in the estimate job | ≈ 4·10⁶ | 10,799,568 | ×2.700 | assumption | The job lays out `2N + √(8N) + 1` tiles of `2d²` qubits and T factories from 15-to-1; the source a board of `2(d+1)²`-qubit patches and CCZ factories, placed by hand. As above: most of this is the T factories that keep up with the tick; at the source's own 38.75 µs the job needs 4,410,288 qubits. | ### RSA-2048 on yoked surface codes (arXiv:2505.15917, 2025) diff --git a/wai/wai-quantum/schemas/estimate.v1.json b/wai/wai-quantum/schemas/estimate.v1.json index 41439c047..af040d7cd 100644 --- a/wai/wai-quantum/schemas/estimate.v1.json +++ b/wai/wai-quantum/schemas/estimate.v1.json @@ -57,7 +57,8 @@ "properties": { "reaction_us": { "type": "number", - "exclusiveMinimum": 0 + "exclusiveMinimum": 0, + "exclusiveMaximum": 1000000000 } }, "additionalProperties": false @@ -361,6 +362,27 @@ "kind": { "const": "quantum-resource" }, + "clock": { + "type": "object", + "description": "On the reaction-limited clock only: the runtime the estimate's steps do not account for, read from its signed figures. How long one tick was is the job's, whose hash binds the reaction time.", + "required": [ + "kind", + "tick_time_ns_bits", + "tick_time_s" + ], + "properties": { + "kind": { + "const": "reaction" + }, + "tick_time_ns_bits": { + "type": "string" + }, + "tick_time_s": { + "type": "number" + } + }, + "additionalProperties": false + }, "estimate": { "type": "object" }, diff --git a/wai/wai-quantum/src/engine/estimate/mod.rs b/wai/wai-quantum/src/engine/estimate/mod.rs index a78916e61..dc7d3ad47 100644 --- a/wai/wai-quantum/src/engine/estimate/mod.rs +++ b/wai/wai-quantum/src/engine/estimate/mod.rs @@ -36,7 +36,8 @@ //! request names the reaction-limited clock, `"clock": {"reaction_us": t}`: //! then its Clifford fixups are teleported away from the data and it takes one //! tick of `t` µs, the factories keeping up (`quantum_resource::Clock`, the -//! rule `quantum_arch` takes too). A request without a clock is answered and +//! rule `quantum_arch` takes too), `t` no shorter than a measurement on the +//! qubit ([`clock_refusal`]). A request without a clock is answered and //! sealed as before, bit for bit; one with the reaction clock is sealed as a //! clocked job (`quantum_resource_receipt::ClockedJob`), whose hash is a form //! of its own. @@ -88,7 +89,7 @@ pub fn preset(name: &str) -> Option { /// logical steps a Toffoli) or `{"reaction_us": t}` (a Toffoli a tick of the /// control system's reaction time, `t` µs, its fixups teleported away). pub fn clock_of(request: &Value) -> Result { - let refused = || "\"clock\" is \"steps\" (the default: three logical steps a Toffoli) or {\"reaction_us\": t}: a Toffoli a tick of the reaction time, t µs, above 0 [QE08]".to_string(); + let refused = || "\"clock\" is \"steps\" (the default: three logical steps a Toffoli) or {\"reaction_us\": t}: a Toffoli a tick of the reaction time, t µs, above 0 and below 1e9 [QE08]".to_string(); match request.get("clock") { None | Some(Value::Null) => Ok(Clock::Steps), Some(Value::String(s)) if s == "steps" => Ok(Clock::Steps), @@ -100,6 +101,26 @@ pub fn clock_of(request: &Value) -> Result { } } +/// Why `clock` cannot run on `qubit`, if it cannot. A reaction includes the +/// logical measurement it reacts to (arXiv:1905.09749 App. B.1), so a reaction +/// time shorter than one measurement on the qubit describes no machine. +pub fn clock_refusal(clock: Clock, qubit: &PhysicalQubit) -> Option { + match clock { + Clock::Reaction { reaction_ns } if reaction_ns < qubit.meas_ns => Some(format!( + "a reaction time of {} µs is shorter than a measurement on this qubit, {} µs, and a reaction includes the measurement it reacts to [QE08]", + reaction_ns / 1e3, + qubit.meas_ns / 1e3 + )), + _ => None, + } +} + +/// The reaction time as the request wrote it, in µs: what the answer echoes. +/// (`t·10³/10³` is not always `t`.) +fn reaction_us_of(request: &Value) -> Option { + request.get("clock")?.get("reaction_us")?.as_f64() +} + /// The angles a document's rotations are costed at, which it does not give: /// `(2k + 1)·π/17` for `k = 0..8`, none a multiple of `π/4`. A rotation then /// costs the most `T` gates any of them needs. @@ -166,7 +187,7 @@ impl Options { pub fn read(v: &Value) -> Result { let mut o = Options::default(); if !v.is_object() { - return Err("an estimate's request is an object: {\"qasm\" | \"counts\" | \"routine\", \"budget\", \"qubit\", \"calibration\", \"max_slowdown\", \"export\"} [QE08]".into()); + return Err("an estimate's request is an object: {\"qasm\" | \"counts\" | \"routine\", \"budget\", \"qubit\", \"calibration\", \"max_slowdown\", \"clock\", \"export\"} [QE08]".into()); } if let Some(b) = v.get("budget").filter(|x| !x.is_null()) { o.budget = b.as_f64().filter(|x| *x > 0.0 && *x < 1.0).ok_or("\"budget\" is the probability the run may fail, between 0 and 1 (exclusive) [QE08]")?; @@ -488,7 +509,7 @@ fn model_refusal(e: &EstimateError) -> String { EstimateError::AboveThreshold => "the qubit's error rate is at or above the code's threshold, so no distance helps", EstimateError::DistanceExceeded => "no code distance up to 51 meets the logical share of the budget", EstimateError::NoFactory => "no T factory of up to three rounds and distance 51 meets the per-state target", - EstimateError::Invalid => "the counts or the budget are not ones the model takes", + EstimateError::Invalid => "the counts, the budget or the clock are not ones the model takes, or the machine's qubits do not fit in 64 bits", EstimateError::Mismatch => "the code and the qubit use different instruction sets", EstimateError::NotRun => "the estimate was not run", }; @@ -533,13 +554,14 @@ pub fn figures(e: &Estimate, slowdown: u64) -> Value { }) } -/// [`figures`] on `clock`: on the reaction-limited clock they add the clock's, +/// [`figures`], with the reaction time `reaction_us` the request wrote if it +/// names the reaction-limited clock: they then add the clock's, /// `{"reaction_us", "tick_us", "toffolis"}`, and `steps` are the /// lattice-surgery steps alone. -pub fn figures_clocked(e: &Estimate, slowdown: u64, clock: Clock, toffolis: u64) -> Value { +pub fn figures_clocked(e: &Estimate, slowdown: u64, reaction_us: Option, toffolis: u64) -> Value { let mut v = figures(e, slowdown); - if let Clock::Reaction { reaction_ns } = clock { - v["clock"] = json!({ "reaction_us": reaction_ns / 1e3, "tick_us": reaction_ns * slowdown as f64 / 1e3, "toffolis": toffolis }); + if let Some(t) = reaction_us { + v["clock"] = json!({ "reaction_us": t, "tick_us": t * slowdown as f64, "toffolis": toffolis }); } v } @@ -620,6 +642,10 @@ pub struct Evaluation { pub fn evaluate(request: &Value) -> Result { let options = Options::read(request)?; let clock = clock_of(request)?; + if let Some(refusal) = clock_refusal(clock, &options.qubit) { + return Err(refusal); + } + let reaction_us = reaction_us_of(request); let derived = derive(request, options.budget)?; let codes = options.calibration.codes(&options.qubit); let toffolis = derived.logical.ccz; @@ -627,10 +653,10 @@ pub fn evaluate(request: &Value) -> Result { let mut notes = derived.notes.clone(); notes.push("Each rotation is a Clifford+T word found by exact search (about 3·log₂(1/ε) T gates); synthesis that measures and falls back reaches fewer T gates and is not assumed.".into()); notes.push("The physical model is the planar one of quantum_resource (parallel-synthesis sequential Pauli computation, 15-to-1 factories found by exhaustive search), with its assumptions: uniform, independent circuit noise; free transport of T states; smooth T consumption.".into()); - if let Clock::Reaction { reaction_ns } = clock { + if let Some(t) = reaction_us { notes.push(format!( "On the reaction-limited clock: each of the {toffolis} Toffoli(s) has its Clifford fixups teleported away from the data and takes one tick of the reaction time, {} µs (stretched as the steps are, slowed), one after another, with enough factories that a CCZ state comes every tick; the other operations take their logical steps. The steps counted are the lattice-surgery steps alone.", - reaction_ns / 1e3 + t )); } let cal = options.calibration.describe(&options.qubit); @@ -651,12 +677,12 @@ pub fn evaluate(request: &Value) -> Result { "epsilon": derived.epsilon, "calibration": cal, "options": options.to_json(), - "estimate": figures_clocked(&h.estimate, h.slowdown, clock, toffolis), - "frontier": points.iter().map(|p| figures_clocked(&p.estimate, p.slowdown, clock, toffolis)).collect::>(), + "estimate": figures_clocked(&h.estimate, h.slowdown, reaction_us, toffolis), + "frontier": points.iter().map(|p| figures_clocked(&p.estimate, p.slowdown, reaction_us, toffolis)).collect::>(), "notes": notes, }); - if let Clock::Reaction { reaction_ns } = clock { - result["options"]["clock"] = json!({ "reaction_us": reaction_ns / 1e3 }); + if let Some(t) = reaction_us { + result["options"]["clock"] = json!({ "reaction_us": t }); } if derived.rotations.is_array() { result["rotations"] = derived.rotations.clone(); @@ -817,15 +843,15 @@ pub fn properties() -> Value { "max_slowdown": { "type": "integer", "minimum": 1, "maximum": MAX_SLOWDOWN, "description": "The most the frontier slows the program by to need fewer factories (default 256)." }, "clock": { "anyOf": [ { "type": "string", "enum": ["steps"] }, - { "type": "object", "properties": { "reaction_us": { "type": "number", "exclusiveMinimum": 0 } }, "required": ["reaction_us"] } - ], "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs, with enough factories to keep up." }, + { "type": "object", "properties": { "reaction_us": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1e9 } }, "required": ["reaction_us"], "additionalProperties": false } + ], "description": "How long a Toffoli takes: steps (the default: three logical steps of d cycles), or {\"reaction_us\": t}, the reaction-limited clock: its Clifford fixups teleported away, one tick of the control system's reaction time t µs (never shorter than a measurement on the qubit), with enough factories to keep up." }, "export": { "type": "string", "enum": ["counts", "routine"], "description": "Also give the program's counts as a logical-counts or a routine document, under export." }, "name": { "type": "string", "description": "The program's name in a routine document." } }) } /// What an agent is told of the `estimate_resources` tool, on both MCP servers. -pub const DESCRIPTION: &str = "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit and the budget, and call them an estimate."; +pub const DESCRIPTION: &str = "What a fault-tolerant quantum computer would need to run a program. First its logical counts: T gates, Toffolis, rotations (each costed at the T count of its own synthesised Clifford+T word, within its share of the budget), measurements, T depth and total depth, and logical qubits with the ancillas that gates under several controls need. Then, on a physical qubit, the planar model's code distance (the gate-based surface code's model fitted to this engine's own decoders), T factories, physical qubits and runtime, and the frontier of estimates trading qubits for time. Takes a program, or a logical-counts or routine document. Every estimate is sealed in the resource-estimate receipt family, binding the program's digest and the ε budget; the answer's notes state the model's assumptions. Quote the figures with the qubit, the budget and the clock, and call them an estimate."; /// The service's form, unsealed: the answer, or why it is refused. A host /// with a key seals with [`sealed`] instead. @@ -891,13 +917,63 @@ mod tests { steps["clock"] = json!("steps"); assert_eq!(call(&steps).unwrap(), a); assert!(a["estimate"].get("clock").is_none() && a["options"].get("clock").is_none()); - for bad in [json!("fast"), json!({ "reaction_us": 0 }), json!({ "reaction_us": 10, "x": 1 }), json!({ "reaction": 10 }), json!(10)] { + for bad in [json!("fast"), json!({ "reaction_us": 0 }), json!({ "reaction_us": 1e9 }), json!({ "reaction_us": 10, "x": 1 }), json!({ "reaction": 10 }), json!(10)] { let mut r = req.clone(); r["clock"] = bad.clone(); assert!(call(&r).unwrap_err().contains("[QE08]"), "{bad}"); } } + /// A reaction includes a measurement: the 100 µs qubit takes no 10 µs + /// reaction time, and is refused for that reason, not sealed with a tick + /// shorter than anything it can do. + #[test] + fn a_reaction_time_shorter_than_a_measurement_is_refused() { + let mut req = json!({ "counts": { "numQubits": 100, "cczCount": 1_000_000 }, "qubit": "us-e3", "clock": { "reaction_us": 10 } }); + let refusal = call(&req).unwrap_err(); + assert!(refusal.contains("[QE08]") && refusal.contains("a reaction includes the measurement"), "{refusal}"); + assert!(sealed(&req, &key()).is_err(), "nothing is sealed"); + req["clock"] = json!({ "reaction_us": 100 }); + assert_eq!(call(&req).unwrap()["options"]["clock"], json!({ "reaction_us": 100.0 })); + // Past the floor, on a qubit described to allow it, a machine too big + // for a count is refused where it once wrapped into a signed receipt. + let mut far = rsa_2021(Some(json!({ "reaction_us": 1e-15 }))); + far["qubit"]["meas_ns"] = json!(1e-13); + far["max_slowdown"] = json!(1); + let refusal = sealed(&far, &key()).unwrap_err(); + assert!(refusal.contains("do not fit in 64 bits"), "{refusal}"); + } + + /// The answer says the reaction time the request wrote: `t·10³/10³` is not + /// always `t`. + #[test] + fn the_answer_echoes_the_reaction_time_as_written() { + let t = 42.6591; + assert_ne!(t * 1e3 / 1e3, t, "a time that does not survive the conversion"); + let a = call(&rsa_2021(Some(json!({ "reaction_us": t })))).unwrap(); + let e = &a["estimate"]; + assert_eq!(a["options"]["clock"]["reaction_us"].as_f64(), Some(t)); + assert_eq!(e["clock"]["reaction_us"].as_f64(), Some(t)); + assert_eq!(e["clock"]["tick_us"].as_f64(), Some(t * e["slowdown"].as_f64().unwrap())); + assert!(a["notes"].as_array().unwrap().iter().any(|n| n.as_str().unwrap().contains("42.6591 µs")), "{}", a["notes"]); + } + + /// The tool's schema, the answer's and `clock_of` say one thing. + #[test] + fn the_schemas_say_of_the_clock_what_the_engine_takes() { + let committed: Value = serde_json::from_str(include_str!("../../../schemas/estimate.v1.json")).unwrap(); + let tool = properties(); + for object in [&tool["clock"]["anyOf"][1], &committed["properties"]["request"]["properties"]["clock"]["anyOf"][1]] { + assert_eq!(object["additionalProperties"], false, "{object}"); + let t = &object["properties"]["reaction_us"]; + assert_eq!((t["exclusiveMinimum"].as_f64(), t["exclusiveMaximum"].as_f64()), (Some(0.0), Some(1e9)), "{object}"); + } + for (t, taken) in [(0.0, false), (1e-3, true), (999_999_999.0, true), (1e9, false)] { + assert_eq!(clock_of(&json!({ "clock": { "reaction_us": t } })).is_ok(), taken, "{t}"); + } + assert!(clock_of(&json!({ "clock": { "reaction_us": 1e9 } })).unwrap_err().contains("below 1e9")); + } + /// arXiv:1905.09749's counts (its re-run's 2.698·10⁹ Toffolis on 6,189 /// logical qubits) on its 1 µs, 10⁻³ qubit: the step clock gives about 61 /// hours, the reaction clock the Toffolis one after another at 10 µs. @@ -941,6 +1017,7 @@ mod tests { fn a_reaction_clock_estimate_is_sealed_as_a_clocked_job() { let req = rsa_2021(Some(json!({ "reaction_us": 10 }))); let body = sealed(&req, &key()).unwrap(); + assert_eq!(body["result"]["estimate"]["receipt"]["clock"]["kind"], "reaction", "the receipt says so itself"); let v = verify(&body).unwrap(); assert_eq!((v["valid"].clone(), v["receipts"].clone(), v["reproduced"].clone()), (json!(true), json!(true), json!(true))); let ev = evaluate(&req).unwrap(); @@ -951,6 +1028,7 @@ mod tests { let plain = rsa_2021(None); let ev = evaluate(&plain).unwrap(); let body = sealed(&plain, &key()).unwrap(); + assert!(body["result"]["estimate"]["receipt"].get("clock").is_none()); let r = ResourceReceipt::from_json(&body["result"]["estimate"]["receipt"].to_string()).unwrap(); assert!(r.computation_matches(&ev.job(ev.headline))); assert_eq!(r.job_hash, ev.job(ev.headline).job_hash()); diff --git a/wai/wai-quantum/src/published_architectures.rs b/wai/wai-quantum/src/published_architectures.rs index 68d4564ac..62fe00e38 100644 --- a/wai/wai-quantum/src/published_architectures.rs +++ b/wai/wai-quantum/src/published_architectures.rs @@ -559,6 +559,8 @@ struct Planar { physical: f64, factories: u64, runtime_h: f64, + /// One factory's time for a CCZ state, four T states, µs. + ccz_us: f64, } fn job(counts: Value, budget: f64, p: f64, calibration: &str) -> Result { @@ -584,9 +586,44 @@ fn job_on(counts: Value, budget: f64, p: f64, calibration: &str, reaction_us: Op physical: e["physical_qubits"].as_f64().unwrap_or(0.0), factories: e["factories"].as_u64().unwrap_or(0), runtime_h: e["runtime_s"].as_f64().unwrap_or(0.0) / 3600.0, + ccz_us: 4.0 * e["factory"]["duration_us"].as_f64().unwrap_or(0.0), }) } +/// arXiv:1905.09749 §2.11's arithmetic at `n = 2048`, `n_e = 1.5·n`, on its +/// §2.8's settings. Its 37 ms a lookup addition is a lookup at half a beat an +/// entry and one runway piece's addition, the two pieces side by side: the +/// parallel basis of its 5.1 hours. Its "approximately 7 hours" is that basis +/// rounded. It prints no serial figure; `serial_h` is ours. +struct Section211 { + /// Eq. (1) without its `≈ 0.1·n_e·n`. + lookup_additions: f64, + /// 37 ms on that count. + unrounded_h: f64, + /// Eq. (5)'s `4·n_e·n` ms, as §2.11 takes it. + rounded_h: f64, + /// Toffolis a lookup addition, from §2.8. It prints the lookup's term as + /// `2^(c_exp + c_pad)`; the lookup is `2^(c_exp + c_mul)` Toffolis (§2.5), + /// which its depth, eq. (3), has too. + toffolis_each: f64, + /// Our derivation: those Toffolis one after another, 10 µs each. + serial_h: f64, +} + +fn section_2_11() -> Section211 { + let (n, n_e, c_exp, c_mul, c_sep) = (2048.0_f64, 3072.0_f64, 5.0_f64, 5.0_f64, 1024.0_f64); + let c_pad = 2.0 * n.log2() + n_e.log2() + 10.0; + let lookup_additions = 2.0 * n * n_e / (c_exp * c_mul) * (c_sep + 1.0) / c_sep; + let toffolis_each = 2.0 * n + n * c_pad / c_sep + 2.0_f64.powf(c_exp + c_mul); + Section211 { + lookup_additions, + unrounded_h: lookup_additions * 37.0 / 3.6e6, + rounded_h: 4.0 * n_e * n / 3.6e6, + toffolis_each, + serial_h: lookup_additions * toffolis_each * 10.0 / 3.6e9, + } +} + /// The planar model on the source's own assumptions: `2(d+1)²` qubits a patch, /// and `0.1·(100p)^((d+1)/2)` per cycle, at the distance the source uses. fn their_noise(at_distance: f64) -> QecModel { @@ -639,15 +676,38 @@ fn rsa_2021() -> Result { r.num("r21.job.hours.assumed", "hours per run, job, assumed code model", assumed.runtime_h, F::D(2), Class::Assumption, JOB_TIME); r.num("r21.job.hours.source_rule", "hours per run, the planar model on the source's patch and error rule", source_noise.runtime_ns / 3.6e12, F::D(2), Class::Assumption, JOB_TIME); let fast = job_on(json!({ "numQubits": 6189, "cczCount": toffolis }), 0.31, 1e-3, "assumed", Some(10.0))?; + let s = section_2_11(); + r.num( + "r21.job.distance.reaction", + "code distance, job, reaction-limited clock at 10 µs", + fast.distance as f64, + F::I, + Class::OurLimit, + format!("The tiles wait out {} hours here and {} on the step clock, so a smaller distance meets the same budget.", print(fast.runtime_h, F::D(2)), print(assumed.runtime_h, F::D(2))), + ); r.num( "r21.job.hours.reaction", "hours per run, job, reaction-limited clock at 10 µs", fast.runtime_h, F::D(2), Class::OurLimit, - "The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step: the source's own §2.11 estimate on that basis is \"approximately 7 hours\". Its cost script runs the two runway pieces' additions side by side and its lookups at half a beat an entry, which counts alone do not carry.", + format!( + "The job runs each Toffoli in one 10 µs tick, all of them one after another, as its layout runs every step; the source does not, and counts alone do not carry how it does. Its 5.1 hours and its §2.11 \"approximately 7 hours\" are one parallel basis, 37 ms a lookup addition, with the two runway pieces' additions side by side and the lookups at half a beat an entry. §2.11 reaches 7 by rounding: eq. (1)'s {} lookup additions as 0.1·n_e·n, then 3.7·n_e·n ms as 4·n_e·n, which is {} hours; 37 ms on the unrounded count is {}. The source prints no serial figure. {} Toffolis a lookup addition at 10 µs each would be {} hours: that is our derivation from its equations, not its figure.", + print(s.lookup_additions, F::I), + print(s.rounded_h, F::D(2)), + print(s.unrounded_h, F::D(2)), + print(s.toffolis_each, F::I), + print(s.serial_h, F::D(1)) + ), + ); + r.num( + "r21.job.qubits.reaction", + "physical qubits, job, reaction-limited clock at 10 µs", + fast.physical, + F::I, + Class::OurLimit, + format!("{JOB_SPACE} On this clock the job keeps enough factories ({}) that a CCZ state comes every tick, at d = {}: near the source's count by chance, a different machine from its d = 27 and CCZ factories.", fast.factories, fast.distance), ); - r.num("r21.job.qubits.reaction", "physical qubits, job, reaction-limited clock at 10 µs", fast.physical, F::I, Class::OurLimit, format!("{JOB_SPACE} On this clock the job keeps enough factories ({}) that a CCZ state comes every tick.", fast.factories)); r.text("r21.job.factories", "factories, job", format!("{} T factories", assumed.factories), Class::OurLimit, "A CCZ state costs four T states in the planar model."); for n in [1024, 3072, 4096, 8192, 12288, 16384] { @@ -689,16 +749,32 @@ fn femoco() -> Result { r.num("femoco.job.days.layout", "days, job, its layout's counts", b.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME); r.num("femoco.job.qubits.e4", "physical qubits, job, p = 10⁻⁴", low.physical, F::I, Class::OurLimit, JOB_SPACE); r.num("femoco.job.days.e4", "days, job, p = 10⁻⁴", low.runtime_h / 24.0, F::D(2), Class::Assumption, JOB_TIME); - let fast = job_on(json!({ "numQubits": 700, "cczCount": toffolis as u64 }), 0.01, 1e-3, "assumed", Some(10.0))?; + let on_clock = |t: f64| job_on(json!({ "numQubits": 700, "cczCount": toffolis as u64 }), 0.01, 1e-3, "assumed", Some(t)); + let (fast, paced) = (on_clock(10.0)?, on_clock(38.75)?); r.num( "femoco.job.days.reaction", "days, job, its layout's counts, reaction-limited clock at 10 µs", fast.runtime_h / 24.0, F::D(2), Class::Assumption, - format!("The job keeps enough factories ({}) that a CCZ state comes every 10 µs tick; the source chose four, whose supply sets its pace at 38.75 µs (25 kHz). The job's frontier slows the ticks to need fewer.", fast.factories), + format!( + "The job ticks at 10 µs where the source's four CCZ factories set a pace of 38.75 µs (25 kHz), and that ratio is this delta. It keeps {} T factories to do so, and the two counts are different machines: each of the job's is a 15-to-1 unit taking {} µs a CCZ state, each of the source's 155. At the source's own 38.75 µs the job keeps {}, so of the ×{} against four the tick is ×{} and the kind of factory ×{}. The job's frontier slows the ticks to need fewer.", + fast.factories, + print(fast.ccz_us, F::I), + paced.factories, + print(fast.factories as f64 / 4.0, F::D(0)), + print(fast.factories as f64 / paced.factories as f64, F::D(1)), + print(paced.factories as f64 / 4.0, F::D(0)) + ), + ); + r.num( + "femoco.job.qubits.reaction", + "physical qubits, job, its layout's counts, reaction-limited clock at 10 µs", + fast.physical, + F::I, + Class::Assumption, + format!("{JOB_SPACE} As above: most of this is the T factories that keep up with the tick; at the source's own 38.75 µs the job needs {} qubits.", print(paced.physical, F::I)), ); - r.num("femoco.job.qubits.reaction", "physical qubits, job, its layout's counts, reaction-limited clock at 10 µs", fast.physical, F::I, Class::Assumption, "As above: more factories, to keep up with the reaction time."); Ok(section("FeMoCo on the surface code (arXiv:2011.03494, PRX Quantum 2, 030305, 2021)", r)) } @@ -867,6 +943,46 @@ mod tests { assert!(page[start..stop] == want, "the page differs from the estimators: run the example and read the difference"); } + /// §2.11's 37 ms is the 5.1 hours' own parallel basis, and it reaches its + /// "approximately 7 hours" by rounding. The source prints no serial figure, + /// so the one here is ours, and the text must say so. + #[test] + fn section_2_11_is_the_parallel_basis_rounded_and_the_serial_figure_is_ours() { + let s = section_2_11(); + assert_eq!(print(s.lookup_additions, F::I), "503,808"); + assert_eq!(print(s.toffolis_each, F::I), "5,207"); + assert_eq!(print(s.unrounded_h, F::D(2)), "5.18", "the same basis, unrounded: its 5.1 hours"); + assert_eq!(print(s.rounded_h, F::D(2)), "6.99", "eq. (5): its 7 hours"); + assert_eq!(print(s.serial_h, F::D(1)), "7.3"); + // The page's prose, which the row test does not reach. + let page: String = include_str!("../docs/published-architectures.md").split_whitespace().collect::>().join(" "); + assert!(!page.contains("is the basis of the paper's own")); + assert!(page.contains("are one parallel basis")); + assert!(page.contains("7.3 hours, a figure of ours, not the paper's")); + } + + /// What the reaction-clock rows lean on is on the page: which basis the + /// source's figures are on, the job's distance, and how much of the + /// FeMoCo factory count is the tick. + #[test] + #[cfg_attr(debug_assertions, ignore = "the search over 4¹¹ Paulis takes minutes in a debug build")] + fn the_reaction_rows_say_what_they_rest_on() { + let sections = report().unwrap(); + let rows: BTreeMap<&str, &Row> = sections.iter().flat_map(|s| &s.rows).map(|r| (r.id.as_str(), r)).collect(); + let hours = &rows["r21.job.hours.reaction"].why; + assert!(!hours.contains("on that basis"), "{hours}"); + assert!(hours.contains("one parallel basis") && hours.contains("7.3 hours: that is our derivation"), "{hours}"); + // On the reaction clock the job's machine is not the source's. + assert_eq!(rows["r21.job.distance.assumed"].values.unwrap().1, 27.0); + assert_eq!(rows["r21.job.distance.reaction"].values.unwrap().1, 25.0); + assert!(rows["r21.job.qubits.reaction"].why.contains("at d = 25: near the source's count by chance")); + // The tick is the smaller part of the factory gap. + let days = &rows["femoco.job.days.reaction"].why; + assert!(days.contains("It keeps 157 T factories") && days.contains("taking 1,568 µs a CCZ state") && days.contains("the job keeps 41"), "{days}"); + assert!(days.contains("the tick is ×3.8 and the kind of factory ×10"), "{days}"); + assert!(rows["femoco.job.qubits.reaction"].why.contains("T factories from 15-to-1")); + } + #[test] #[cfg_attr(debug_assertions, ignore = "the search over 4¹¹ Paulis takes minutes in a debug build")] fn every_row_holds_to_its_published_figure() { diff --git a/wai/wai-quantum/src/quantum_resource.rs b/wai/wai-quantum/src/quantum_resource.rs index 64c20ee44..fd5af676b 100644 --- a/wai/wai-quantum/src/quantum_resource.rs +++ b/wai/wai-quantum/src/quantum_resource.rs @@ -96,6 +96,9 @@ //! - **Planar codes only.** Surface and Floquet-honeycomb patches with lattice //! surgery. Block (qLDPC) codes and magic-state cultivation need models of //! their own. +//! - **Rounding.** The source prints its times truncated (`163.8 ms` as +//! `163 ms`). This module reports exact values; the tests compare at the +//! printed precision. //! //! # Clocks //! @@ -114,12 +117,11 @@ //! - the factories are as many as the T states need over the runtime, and at //! least as many as deliver a CCZ state (four T states) every tick, so the //! supply keeps up with the reaction time and the tick is the reaction time; -//! - a slowdown `s` stretches both: `s·C` steps, ticks of `s` reaction times. +//! - a slowdown `s` stretches both: `s·C` steps, ticks of `s` reaction times; +//! - a reaction includes the logical measurement it reacts to (its App. B.1), +//! so a reaction time shorter than the qubit's measurement is refused. //! //! On the step clock it is [`estimate_isa`], bit for bit. -//! - **Rounding.** The source prints its times truncated (`163.8 ms` as -//! `163 ms`). This module reports exact values; the tests compare at the -//! printed precision. use crate::repro::ln; @@ -365,7 +367,7 @@ pub fn layout(c: &LogicalCounts, total_budget: f64) -> IsaCounts { fn ceil_pos(x: f64) -> u64 { let t = x as u64; - if (t as f64) < x { t + 1 } else { t } + if (t as f64) < x { t.saturating_add(1) } else { t } } /// The two 15-to-1 distillation units. @@ -614,7 +616,9 @@ pub enum Clock { /// estimate's unless another clock is asked for. Steps, /// One tick a Toffoli, its Clifford fixups teleported away from the data: - /// the slower of the reaction time (ns) and the CCZ supply. + /// the slower of the reaction time (ns) and the CCZ supply. The reaction + /// time includes a logical measurement, so it is never shorter than the + /// qubit's: [`estimate_isa_clocked`] refuses one that is. Reaction { reaction_ns: f64 }, } @@ -667,7 +671,9 @@ pub struct Estimate { pub distance: u32, pub tile_qubits: u64, pub step_ns: f64, - /// Logical steps actually run. + /// Logical steps actually run: on [`Clock::Reaction`] the lattice-surgery + /// steps alone, the Toffolis being ticks, so the runtime is more than + /// `steps · step_ns`. pub steps: u64, pub runtime_ns: f64, /// Per-tile, per-step logical error at the chosen distance. @@ -701,7 +707,9 @@ pub enum EstimateError { DistanceExceeded, /// No factory within the search limits meets the T-state target. NoFactory, - /// An empty program, a zero budget or a zero slowdown. + /// An empty program, a zero budget, a zero slowdown, a reaction time + /// shorter than the qubit's measurement, or a machine whose qubits do not + /// fit in 64 bits. Invalid, /// The code and the qubit use different instruction sets. Mismatch, @@ -733,7 +741,7 @@ pub fn estimate_isa_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubi let (ticks, reaction_ns) = match clock { Clock::Steps => (0, 0.0), Clock::Reaction { reaction_ns } => { - if !(reaction_ns > 0.0 && reaction_ns.is_finite()) || toffolis.checked_mul(3).is_none_or(|t| t > isa.min_steps) { + if !(reaction_ns > 0.0 && reaction_ns.is_finite()) || reaction_ns < qubit.meas_ns || toffolis.checked_mul(3).is_none_or(|t| t > isa.min_steps) { return Err(EstimateError::Invalid); } (toffolis, reaction_ns) @@ -788,12 +796,13 @@ pub fn estimate_isa_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubi n = n.max(ceil_pos(per_factory / stretched)); tick_ns = reaction_tick(stretched, per_factory / n as f64); } - (n, n * f.qubits, tick_ns) + (n, n.checked_mul(f.qubits).ok_or(EstimateError::Invalid)?, tick_ns) } None => (0, 0, if ticks > 0 { stretched } else { 0.0 }), }; let tile_qubits = qec.tile_qubits(distance); - let algorithm_qubits = isa.tiles * tile_qubits; + let algorithm_qubits = isa.tiles.checked_mul(tile_qubits).ok_or(EstimateError::Invalid)?; + let physical_qubits = algorithm_qubits.checked_add(factory_qubits).ok_or(EstimateError::Invalid)?; let estimate = Estimate { qubit: qubit.name.clone(), code: qec.name.clone(), @@ -810,7 +819,7 @@ pub fn estimate_isa_clocked(isa: &IsaCounts, toffolis: u64, qubit: &PhysicalQubi factories, algorithm_qubits, factory_qubits, - physical_qubits: algorithm_qubits + factory_qubits, + physical_qubits, }; return Ok(Clocked { estimate, clock, ticks, tick_ns }); } @@ -1039,8 +1048,8 @@ mod tests { } } - /// The summary rows for factoring and chemistry: distance and factory - /// count exactly; physical qubits and runtime to the printed figures. + /// On the step clock the clocked estimate is the plain one; on the + /// reaction clock the Toffolis run on ticks and the supply keeps up. #[test] fn the_step_clock_is_the_estimate_bit_for_bit_and_the_reaction_clock_keeps_pace() { let c = LogicalCounts { qubits: 12_581, measurements: 1_080_000_000, rotations: 12, rotation_depth: 12, t_gates: 12, ccz: 3_730_000_000 }; @@ -1068,7 +1077,10 @@ mod tests { } } } - assert!(reacted >= 6, "{reacted}"); + // The 100 µs qubit's measurement is longer than a 10 µs reaction, so + // it has no such tick (the test below): the two 50 ns qubits, once + // each slowdown. + assert_eq!(reacted, 4); // Too many Toffolis for the counts, or no reaction time: refused. let cfg = EstimateConfig::new(1.0 / 3.0); let q = PhysicalQubit::ns_e4(); @@ -1080,6 +1092,42 @@ mod tests { assert_eq!(reaction_tick(10.0, 9.6), 10.0); } + /// A reaction is the time to trigger a logical measurement, collect and + /// correct its result, and choose the next basis (arXiv:1905.09749 App. + /// B.1), so one measurement on the qubit bounds it from below: the 100 µs + /// qubit has no 10 µs tick. + #[test] + fn a_reaction_time_is_never_shorter_than_the_qubit_s_measurement() { + let c = LogicalCounts { qubits: 100, ccz: 1_000_000, ..Default::default() }; + let (isa, cfg, qec) = (layout(&c, 1e-3), EstimateConfig::new(1e-3), QecModel::surface_gate()); + let q = PhysicalQubit::us_e3(); + assert_eq!(q.meas_ns, 100_000.0); + let run = |reaction_ns| estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns }); + assert_eq!(run(10_000.0).map(|x| x.tick_ns), Err(EstimateError::Invalid)); + assert_eq!(run(q.meas_ns).map(|x| x.tick_ns), Ok(q.meas_ns)); + } + + /// The factories keep up however short the tick, so a short enough one + /// asks for more qubits than a count holds: refused, never wrapped. + #[test] + fn a_machine_past_sixty_four_bits_is_refused_and_not_wrapped() { + assert_eq!((ceil_pos(2.5), ceil_pos(3.0), ceil_pos(1e300)), (3, 3, u64::MAX)); + let c = LogicalCounts { qubits: 6189, ccz: 2_698_000_000, ..Default::default() }; + let (isa, cfg, qec) = (layout(&c, 0.31), EstimateConfig::new(0.31), QecModel::surface_gate()); + // A qubit whose measurement allows any of these reaction times. + let q = PhysicalQubit { meas_ns: 1e-300, ..PhysicalQubit::ns_e4() }; + let run = |reaction_ns| estimate_isa_clocked(&isa, c.ccz, &q, &qec, &cfg, Clock::Reaction { reaction_ns }); + for reaction_ns in [1e-12, 1e-300] { + assert_eq!(run(reaction_ns).map(|x| x.estimate.physical_qubits), Err(EstimateError::Invalid), "{reaction_ns}"); + } + let held = run(1.0).unwrap(); + let e = &held.estimate; + assert_eq!(e.physical_qubits, e.algorithm_qubits + e.factories * e.factory.as_ref().unwrap().qubits); + assert_eq!(held.tick_ns, 1.0, "the supply keeps up and the tick is the reaction time"); + } + + /// The summary rows for factoring and chemistry: distance and factory + /// count exactly; physical qubits and runtime to the printed figures. #[test] fn the_summary_rows_are_reproduced() { const YEAR: f64 = 365.25 * DAY; diff --git a/wai/wai-quantum/src/quantum_resource_receipt.rs b/wai/wai-quantum/src/quantum_resource_receipt.rs index 26e2e7e3b..a0547bbd8 100644 --- a/wai/wai-quantum/src/quantum_resource_receipt.rs +++ b/wai/wai-quantum/src/quantum_resource_receipt.rs @@ -480,6 +480,24 @@ impl ResourceReceipt { result_hash(&self.estimate) == self.result_hash && merkle_root(&self.leaves()) == self.merkle_root } + /// The runtime its steps do not account for: on the reaction-limited clock + /// the Toffolis' ticks, on the step clock exactly none (the runtime is + /// `step_ns · steps`, in that arithmetic). It comes from figures the + /// receipt signs, so it needs no field of its own, and it is how a receipt + /// says which clock it is on. How long one tick was is the job's: its hash + /// binds the reaction time. + pub fn tick_time_ns(&self) -> f64 { + let e = &self.estimate; + e.runtime_ns - e.step_ns * e.steps as f64 + } + + /// What [`Self::to_json`] says of the clock: nothing on the step clock, so + /// that receipt is what it always was. + fn clock_json(&self) -> Option { + let ticks = self.tick_time_ns(); + (ticks > 0.0).then(|| json!({ "kind": "reaction", "tick_time_ns_bits": bits(ticks), "tick_time_s": ticks / 1e9 })) + } + /// The receipt is internally consistent and its signature is valid. Does /// not re-run the job: see [`Self::verify_computation`]. pub fn verify(&self) -> bool { @@ -515,7 +533,8 @@ impl ResourceReceipt { /// Canonical JSON, `kind` first and then keys sorted. Every floating-point /// figure appears as its exact bits (`*_bits`), which are what a verifier - /// reads back; the headline runtime also appears as a readable number. + /// reads back; the headline runtime also appears as a readable number. On + /// the reaction-limited clock `clock` names it ([`Self::tick_time_ns`]). pub fn to_json(&self) -> String { let e = &self.estimate; let factory = e.factory.as_ref().map(|fa| { @@ -574,7 +593,19 @@ impl ResourceReceipt { }); // The crate's serde_json keeps insertion order, and keys are written in // sorted order after `kind`, so this is canonical. - v.to_string() + match (self.clock_json(), v) { + (Some(clock), Value::Object(fields)) => { + let mut o = serde_json::Map::new(); + for (k, x) in fields { + if k == "estimate" { + o.insert("clock".into(), clock.clone()); + } + o.insert(k, x); + } + Value::Object(o).to_string() + } + (_, v) => v.to_string(), + } } pub fn from_json(s: &str) -> Option { @@ -648,7 +679,7 @@ impl ResourceReceipt { Some(Value::String(h)) => Some(unhex::<32>(h)?), _ => None, }; - Some(ResourceReceipt { + let receipt = ResourceReceipt { job_hash: unhex(s("job_hash")?)?, estimate, result_hash: unhex(s("result_hash")?)?, @@ -658,7 +689,22 @@ impl ResourceReceipt { signer_pubkey: unhex(s("signer_pubkey")?)?, signer_id: s("signer_id")?.to_string(), sig: unhex(s("sig")?)?, - }) + }; + // `clock` is read from the signed figures, so one that disagrees with + // them, or is missing where they name one, is not this receipt. Its + // bits are compared; `tick_time_s` is the readable form, as + // `runtime_s` is, and a parser need not round it back exactly. + let agrees = match (v.get("clock"), receipt.clock_json()) { + (None, None) => true, + (Some(have), Some(want)) => { + have.get("kind") == want.get("kind") + && have.get("tick_time_ns_bits") == want.get("tick_time_ns_bits") + && have.get("tick_time_s").is_some_and(Value::is_number) + && have.as_object().is_some_and(|o| o.len() == 3) + } + _ => false, + }; + agrees.then_some(receipt) } } @@ -740,6 +786,46 @@ mod tests { assert!(ClockedJob { job: isa, clock: Clock::Steps }.run().is_ok()); } + /// A reader with the receipt alone can tell the clocks apart, from figures + /// the signature covers; a step-clock receipt reads as it always did. + #[test] + fn a_reaction_clock_receipt_names_its_clock_and_a_step_clock_receipt_is_unchanged() { + let j = ResourceJob::default(); + let step = ResourceReceipt::seal(&key(4), "sink", &j, None, unmetered).unwrap(); + assert_eq!(step.tick_time_ns(), 0.0); + let plain: Value = serde_json::from_str(&step.to_json()).unwrap(); + assert!(plain.get("clock").is_none()); + + let fast = ClockedJob { job: j, clock: Clock::Reaction { reaction_ns: 10_000.0 } }; + let r = ResourceReceipt::seal_clocked(&key(4), "sink", &fast, None, unmetered).unwrap(); + let c = fast.run().unwrap(); + assert_eq!(r.tick_time_ns(), c.estimate.runtime_ns - c.estimate.step_ns * c.estimate.steps as f64); + assert!(r.tick_time_ns() > 0.0); + let text = r.to_json(); + let v: Value = serde_json::from_str(&text).unwrap(); + assert_eq!(v["clock"]["kind"], "reaction"); + let keys: Vec<&String> = v.as_object().unwrap().keys().collect(); + assert_eq!((keys[0].as_str(), keys[1].as_str(), keys[2].as_str()), ("kind", "clock", "estimate"), "canonical: kind, then sorted"); + let back = ResourceReceipt::from_json(&text).unwrap(); + assert_eq!(back, r); + assert_eq!(back.to_json(), text); + assert!(back.verify()); + + // The member repeats signed figures: changed or removed, it is refused. + let mut wrong = v.clone(); + wrong["clock"]["tick_time_ns_bits"] = json!(bits(1.0)); + assert!(ResourceReceipt::from_json(&wrong.to_string()).is_none()); + let mut named = v.clone(); + named["clock"]["kind"] = json!("steps"); + assert!(ResourceReceipt::from_json(&named.to_string()).is_none()); + let mut on_a_step_receipt = plain; + on_a_step_receipt["clock"] = v["clock"].clone(); + assert!(ResourceReceipt::from_json(&on_a_step_receipt.to_string()).is_none()); + let mut gone = v; + gone.as_object_mut().unwrap().remove("clock"); + assert!(ResourceReceipt::from_json(&gone.to_string()).is_none()); + } + #[test] fn the_json_round_trips_to_the_same_bytes() { // With a factory, and a Clifford-only program without one. diff --git a/wai/wai-quantum/tests/data/published_figures.tsv b/wai/wai-quantum/tests/data/published_figures.tsv index d63026c0f..67e83bb43 100644 --- a/wai/wai-quantum/tests/data/published_figures.tsv +++ b/wai/wai-quantum/tests/data/published_figures.tsv @@ -142,6 +142,7 @@ r21.job.qubits.fitted arXiv:1905.09749 Table 3; its counts in the estimate job 2 r21.job.qubits.source_rule arXiv:1905.09749 Table 3; its counts in the estimate job 20 (megaqubits) 20000000.0 guard 2.757e7 r21.job.hours.assumed arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 6.071e1 r21.job.hours.source_rule arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 6.971e1 +r21.job.distance.reaction arXiv:1905.09749 §2.13 (d = 27); its counts in the estimate job 27 27 guard 2.500e1 r21.job.hours.reaction arXiv:1905.09749 Table 3; its counts in the estimate job 5.1 5.1 guard 7.495e0 r21.job.qubits.reaction arXiv:1905.09749 Table 3; its counts in the estimate job 20 (megaqubits) 20000000.0 guard 2.005e7 r21.job.factories arXiv:1905.09749 §2.10; its counts in the estimate job 14 CCZ per piece, 28 in all text diff --git a/wai/wai-web/src/quantum_engine.rs b/wai/wai-web/src/quantum_engine.rs index 1bfc96fe5..45560dc28 100644 --- a/wai/wai-web/src/quantum_engine.rs +++ b/wai/wai-web/src/quantum_engine.rs @@ -92,7 +92,7 @@ impl QuantumEngine { /// What a fault-tolerant machine would need to run a program /// (`{"qasm" | "counts" | "routine", "budget", "qubit", "calibration", - /// "max_slowdown", "export"}`): its logical counts, each estimate on the + /// "max_slowdown", "clock", "export"}`): its logical counts, each estimate on the /// frontier sealed with the page's key in the resource-estimate receipt /// family and the whole answer in a job's form, so `engine_verify` /// checks its seal and `engine_estimate_verify` re-runs it; or -- 2.54.0 (Apple Git-157) From a896898430c79a8415e0c3860fcbf545268a4b66 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 7 Nov 2026 11:43:37 -0500 Subject: [PATCH 3/4] wai-quantum: a CUDA sampling kernel that looks past its table stops, and the device stays The jetson-hub leg's reds were not the card. The kernel log there holds Xid 31 (an MMU fault: a kernel read an unmapped address) many times today, the same address each time. They come from the process running the device tests, and none is a hardware Xid. The same tests on Thor's GPU, another card and driver, gave the same fault. The fault test puts "fields held in 32 bits" into the CUDA kernels. The sampling kernel shares field_of, so a wrong field sent its table lookup far past the table. On CUDA nothing checks that index, so the read faulted. A fault is sticky, and the process's primary context was lost: - the next fault's kernels "opened on 0 devices"; - every later CUDA test found "CUDA_ERROR_ILLEGAL_ADDRESS". WebGPU's kernels survive the same port because their indices are bounds-checked. The Xid named the decline test's thread because that test first made the context and its channel. chains_in_order now takes the table's entry count and a fault word. A lookup at or past the end sets the word and stops, and the host returns an error. The host sizes the table to every field a cell can reach, so the kernels as written never do this. A port that gets a field wrong loses its grade (certify takes an error as not holding) and not the device. a_kernel_that_looks_past_its_table_loses_its_grade_and_not_the_device puts a lookup 2^40 entries past the table into the CUDA kernel (and 256 MB past it into WebGPU's). It requires the grade lost, and the clean kernels to open again on as many devices and certify bit-exact. - With the check switched off it fails on Thor: "the clean kernels opened on 0 devices, before it on 1", with a new Xid 31. - With the check on, the device tests pass twice with no Xid, and compute-sanitizer's memcheck finds 0 errors in the fault tests. The workflow expects 8 device tests where it expected 7. Co-Authored-By: Claude Opus 5.5 --- .../wai-quantum-landscape-devices.yml | 2 +- wai/wai-quantum/src/quantum_landscape/cuda.rs | 19 ++++++++-- wai/wai-quantum/tests/landscape_devices.rs | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape-devices.yml b/.github/workflows/wai-quantum-landscape-devices.yml index ab8544573..3231c0c78 100644 --- a/.github/workflows/wai-quantum-landscape-devices.yml +++ b/.github/workflows/wai-quantum-landscape-devices.yml @@ -140,7 +140,7 @@ jobs: rm -f "$report" WAI_LANDSCAPE_REPORT="$report" nice -n 10 \ cargo test --locked --release --features "${{ matrix.features }}" --test landscape_devices -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/devices.log" - grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (7 expected)"; exit 1; } + grep -qE '^test result: ok\. 8 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (8 expected)"; exit 1; } cat "$report" IFS=',' read -ra want <<< "$WAI_LANDSCAPE_EXPECT" for p in "${want[@]}"; do diff --git a/wai/wai-quantum/src/quantum_landscape/cuda.rs b/wai/wai-quantum/src/quantum_landscape/cuda.rs index 78ad755e9..894eab9b9 100644 --- a/wai/wai-quantum/src/quantum_landscape/cuda.rs +++ b/wai/wai-quantum/src/quantum_landscape/cuda.rs @@ -67,7 +67,7 @@ __device__ __forceinline__ u64 splitmix(u64 s, u64 j) { return x ^ (x >> 31); } -extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* cols, const int* vals, const i64* fields, i64 cost, unsigned n, unsigned chains, const i64* offsets, unsigned k0, unsigned k1, const u64* seeds, const unsigned* sweeps, const u64* table, int* cells) { +extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* cols, const int* vals, const i64* fields, i64 cost, unsigned n, unsigned chains, const i64* offsets, unsigned k0, unsigned k1, const u64* seeds, const unsigned* sweeps, const u64* table, u64 entries, int* cells, unsigned* fault) { unsigned c = blockIdx.x * blockDim.x + threadIdx.x; if (c >= chains) return; int* s = cells + (u64)c * n; @@ -83,6 +83,13 @@ extern "C" __global__ void chains_in_order(const unsigned* rows, const unsigned* i64 h = field_of(rows, cols, vals, fields, s, i); u64 u = splitmix(seed, (u64)k * n + i + 1) >> 11; u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i]; + // The host sizes the table to every field a cell can reach, so these kernels never + // pass it; a port that gets a field wrong is stopped here, before it reads past the + // table and the device's context is lost to every job after it. + if (e >= entries) { + atomicOr(fault, 1u); + return; + } s[i] = u < table[2 * e] ? -1 : (u < table[2 * e + 1] ? 0 : 1); } } @@ -235,21 +242,27 @@ impl CudaLandscape { let offsets = s.clone_htod(&reach.offsets).map_err(driver)?; let (cost, n32, count32) = (w.cost(), n as u32, count as u32); let window = device::window(w, &reach, sampling); + let mut fault = s.alloc_zeros::(1).map_err(driver)?; let mut k0 = 0usize; while k0 < temperatures.len() { let k1 = (k0 + window).min(temperatures.len()); let (sweeps, table) = device::window_sweeps(w, &reach, &temperatures[k0..k1]); + let entries = (table.len() / 2) as u64; let (sweeps, table) = (s.clone_htod(&sweeps).map_err(driver)?, s.clone_htod(&nonempty(&table)).map_err(driver)?); let (a0, a1) = (k0 as u32, k1 as u32); let mut a = s.launch_builder(&self.kernels.chains); - a.arg(&rows).arg(&cols).arg(&vals).arg(&fields).arg(&cost).arg(&n32).arg(&count32).arg(&offsets).arg(&a0).arg(&a1).arg(&seeds).arg(&sweeps).arg(&table).arg(&mut cells); + a.arg(&rows).arg(&cols).arg(&vals).arg(&fields).arg(&cost).arg(&n32).arg(&count32).arg(&offsets).arg(&a0).arg(&a1).arg(&seeds).arg(&sweeps).arg(&table).arg(&entries).arg(&mut cells).arg(&mut fault); // SAFETY: 2·count·n cells, an offset a cell, a seed a chain, a word a sweep of the - // window, and the window's table of (a, b) pairs for every field a cell can reach. + // window, the window's table of `entries` (a, b) pairs, which the kernel reads only + // below `entries`, and one fault word. unsafe { a.launch(launch(count as u64)) }.map_err(driver)?; // One window at a time: only its table is held, and a failure leaves no more queued. s.synchronize().map_err(driver)?; k0 = k1; } + if s.clone_dtoh(&fault).map_err(driver)?[0] != 0 { + return Err("the sampling kernel met a field its table does not reach, and stopped rather than read past it".into()); + } let got = s.clone_dtoh(&cells).map_err(driver)?; Ok(device::outcomes(w, &got, count, temperatures)) } diff --git a/wai/wai-quantum/tests/landscape_devices.rs b/wai/wai-quantum/tests/landscape_devices.rs index 67d617821..6b160f93a 100644 --- a/wai/wai-quantum/tests/landscape_devices.rs +++ b/wai/wai-quantum/tests/landscape_devices.rs @@ -330,6 +330,42 @@ fn every_fault_in_a_device_s_kernels_loses_its_grade() { } } +/// A port whose sampling kernel looks a field up past its table (by 2⁴⁰ +/// entries on CUDA, so the address is never mapped) loses its grade and not +/// the device: the clean kernels then open on as many devices as before and +/// are certified bit-exact on each. A CUDA kernel that read there would lose +/// its context to every job after it in the process; WebGPU's kernels are +/// bounds-checked. +#[test] +fn a_kernel_that_looks_past_its_table_loses_its_grade_and_not_the_device() { + let present: Vec = members().iter().map(|m| m.id()).collect(); + let have = built(); + let past: Vec<(&str, &str, &str)> = [ + ("cuda", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i];", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i] + (1ull << 40);"), + ("webgpu", "return kind - 1u + bitcast(h) + graph[shape.offsets + i];", "return kind - 1u + bitcast(h) + graph[shape.offsets + i] + 0x1000000u;"), + ] + .into_iter() + .filter(|(b, _, _)| have.contains(b)) + .collect(); + for (backend, found) in backends(&past) { + if !past.iter().any(|(b, _, _)| *b == backend) { + continue; + } + for m in found.into_iter().filter_map(Result::ok) { + let c = member::certify(m.as_ref()).unwrap(); + report(&format!("fault a lookup past the table in {}: settle {}, sample {}", m.id(), c.parity(KERNEL_SETTLE).label(), c.parity(KERNEL_SAMPLE).label())); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact, "{}: a lookup past the table kept the sampler bit-exact", m.id()); + } + let again: Vec> = backends(&[]).into_iter().filter(|(b, _)| *b == backend).flat_map(|(_, f)| f).filter_map(Result::ok).collect(); + let before = present.iter().filter(|id| backend_of(id) == backend).count(); + assert_eq!(again.len(), before, "{backend}: after the fault the clean kernels opened on {} devices, before it on {before}", again.len()); + for m in again { + let c = member::certify(m.as_ref()).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact), "{}: the device lost its grade with the faulted kernels", m.id()); + } + } +} + /// A WebGPU member waits once a window, so a run longer than one wait but made /// of windows shorter than it finishes; and a job a member cannot finish in its /// wait goes to the next member, the answer naming the one that failed. -- 2.54.0 (Apple Git-157) From 36b7d36fd045d131e03140748adc21e356bb0b22 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 8 Nov 2026 11:39:44 -0500 Subject: [PATCH 4/4] wai-quantum: the landscape's WebGPU members come from one instance, so a card is found every time With the CUDA fault fixed, jetson-hub's leg still lost a member: the RTX 4050 through Vulkan, from the fourth fault on. The disk was not the cause. The leg compiled and wrote its files, and the same tests failed twice more with 248 GB free. Nor was the card's memory: at most 630 of 6,141 MiB was in use. Each discovery made its own wgpu instance. That host's NVIDIA Vulkan driver (595.99) lists its card to a process's first ten instances and not after, even with every one dropped. At that moment it logs NV_ERR_NO_MEMORY for a host-memory allocation. A process that looks again, as the device tests do for every test and every faulted kernel, found the card gone; the order of the tests only decided where. On jetson-hub, one process discovering 60 times: - an instance a discovery: the card is found 10 times, then not again; - one instance for the process: the card is found 60 times. Discovery now takes the process's one instance. a_member_is_found_however_often_the_host_looks discovers twelve times in one process and wants the same members each time. On the Framework (AMD, through Vulkan) and Thor (NVIDIA), the device tests, it among them, pass twice each. The workflow expects 9 device tests where it expected 8. Co-Authored-By: Claude Opus 5.5 --- .../wai-quantum-landscape-devices.yml | 2 +- .../src/quantum_landscape/webgpu.rs | 22 ++++++++++++++----- wai/wai-quantum/tests/landscape_devices.rs | 14 ++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wai-quantum-landscape-devices.yml b/.github/workflows/wai-quantum-landscape-devices.yml index 3231c0c78..9eb378574 100644 --- a/.github/workflows/wai-quantum-landscape-devices.yml +++ b/.github/workflows/wai-quantum-landscape-devices.yml @@ -140,7 +140,7 @@ jobs: rm -f "$report" WAI_LANDSCAPE_REPORT="$report" nice -n 10 \ cargo test --locked --release --features "${{ matrix.features }}" --test landscape_devices -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/devices.log" - grep -qE '^test result: ok\. 8 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (8 expected)"; exit 1; } + grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (9 expected)"; exit 1; } cat "$report" IFS=',' read -ra want <<< "$WAI_LANDSCAPE_EXPECT" for p in "${want[@]}"; do diff --git a/wai/wai-quantum/src/quantum_landscape/webgpu.rs b/wai/wai-quantum/src/quantum_landscape/webgpu.rs index ec401dd52..6bce8eeea 100644 --- a/wai/wai-quantum/src/quantum_landscape/webgpu.rs +++ b/wai/wai-quantum/src/quantum_landscape/webgpu.rs @@ -132,14 +132,26 @@ pub fn discover_with(source: &str) -> Vec> { if wgpu::Instance::enabled_backend_features().is_empty() { return Vec::new(); } - let mut d = wgpu::InstanceDescriptor::new_without_display_handle(); - d.backends = wgpu::Backends::PRIMARY; - d.flags = wgpu::InstanceFlags::empty(); - let instance = wgpu::Instance::new(d); - let Ok(adapters) = resolve(instance.enumerate_adapters(wgpu::Backends::PRIMARY)) else { return Vec::new() }; + let Ok(adapters) = resolve(instance().enumerate_adapters(wgpu::Backends::PRIMARY)) else { return Vec::new() }; adapters.into_iter().map(|a| open(a, source)).collect() } +/// The process's one instance of the platform's GPU interfaces, which every +/// discovery enumerates. A discovery that made its own lost a GPU after ten: +/// an NVIDIA Vulkan driver (595.99, an RTX 4050) lists its card to a process's +/// first ten instances and not after, even with every one dropped, so a host +/// that discovers its members again (each test, each faulted kernel) found +/// that card gone. +fn instance() -> &'static wgpu::Instance { + static INSTANCE: std::sync::OnceLock = std::sync::OnceLock::new(); + INSTANCE.get_or_init(|| { + let mut d = wgpu::InstanceDescriptor::new_without_display_handle(); + d.backends = wgpu::Backends::PRIMARY; + d.flags = wgpu::InstanceFlags::empty(); + wgpu::Instance::new(d) + }) +} + fn open(adapter: wgpu::Adapter, source: &str) -> Result { let info = adapter.get_info(); let backend = backend_label(info.backend).ok_or_else(|| format!("{}: backend {:?} is not one this member runs on", info.name, info.backend))?; diff --git a/wai/wai-quantum/tests/landscape_devices.rs b/wai/wai-quantum/tests/landscape_devices.rs index 6b160f93a..7c5b1de2c 100644 --- a/wai/wai-quantum/tests/landscape_devices.rs +++ b/wai/wai-quantum/tests/landscape_devices.rs @@ -330,6 +330,20 @@ fn every_fault_in_a_device_s_kernels_loses_its_grade() { } } +/// A host finds the same members however often it looks. Each discovery once +/// made its own instance of the GPU interfaces, and an NVIDIA Vulkan driver +/// lists its card to a process's first ten instances and not after: by a test +/// late in this file, that card was gone. Twelve discoveries, one process. +#[test] +fn a_member_is_found_however_often_the_host_looks() { + let ids = |found: Found| -> Vec { found.into_iter().filter_map(Result::ok).map(|m| m.id()).collect() }; + let first: Vec> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect(); + for look in 2..=12 { + let again: Vec> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect(); + assert_eq!(again, first, "discovery {look} found other members than the first"); + } +} + /// A port whose sampling kernel looks a field up past its table (by 2⁴⁰ /// entries on CUDA, so the address is never mapped) loses its grade and not /// the device: the clean kernels then open on as many devices as before and -- 2.54.0 (Apple Git-157)