diff --git a/.github/workflows/wai-quantum-estimate.yml b/.github/workflows/wai-quantum-estimate.yml index 9daff054c..209780c2e 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,18 @@ 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 \ + 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 @@ -104,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/.github/workflows/wai-quantum-landscape-devices.yml b/.github/workflows/wai-quantum-landscape-devices.yml index ab8544573..9eb378574 100644 --- a/.github/workflows/wai-quantum-landscape-devices.yml +++ b/.github/workflows/wai-quantum-landscape-devices.yml @@ -140,7 +140,7 @@ jobs: rm -f "$report" WAI_LANDSCAPE_REPORT="$report" nice -n 10 \ cargo test --locked --release --features "${{ matrix.features }}" --test landscape_devices -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/devices.log" - grep -qE '^test result: ok\. 7 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (7 expected)"; exit 1; } + grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/devices.log" || { echo "::error::not every device test ran and passed (9 expected)"; exit 1; } cat "$report" IFS=',' read -ra want <<< "$WAI_LANDSCAPE_EXPECT" for p in "${want[@]}"; do diff --git a/wai/changes/2026-11-06-estimate-reaction-clock.md b/wai/changes/2026-11-06-estimate-reaction-clock.md new file mode 100644 index 000000000..20080a349 --- /dev/null +++ b/wai/changes/2026-11-06-estimate-reaction-clock.md @@ -0,0 +1,30 @@ +### 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. 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 + `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. 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 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 03d6b8fb2..706b65711 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,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 (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. @@ -763,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. @@ -772,6 +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 (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. @@ -2543,6 +2545,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, 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 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 b4c691083..08198bceb 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -19366,7 +19366,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": { @@ -19453,6 +19453,31 @@ "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, + "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 (never shorter than a measurement on the qubit), with enough factories to keep up." + }, "export": { "type": "string", "enum": [ @@ -19515,6 +19540,28 @@ "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, + "exclusiveMaximum": 1000000000 + } + }, + "additionalProperties": false + } + ] + }, "export": { "enum": [ "counts", @@ -19749,6 +19796,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" }, @@ -19789,6 +19857,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" + } + } } } }, @@ -19885,6 +19973,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" }, @@ -19925,6 +20034,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 98e4a5cc6..eb163cebb 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -40920,7 +40920,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": { @@ -40996,6 +40996,31 @@ "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, + "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 (never shorter than a measurement on the qubit), with enough factories to keep up." + }, "export": { "type": "string", "enum": [ @@ -41065,6 +41090,28 @@ "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, + "exclusiveMaximum": 1000000000 + } + }, + "additionalProperties": false + } + ] + }, "export": { "enum": [ "counts", @@ -41299,6 +41346,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" }, @@ -41339,6 +41407,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" + } + } } } }, @@ -41435,6 +41523,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" }, @@ -41475,6 +41584,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..3817499b2 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, 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 #[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); @@ -115,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"]; @@ -157,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 { @@ -173,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())), @@ -184,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() { @@ -192,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])); @@ -244,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 db8520b9e..ba8d30723 100644 --- a/wai/wai-quantum-py/python/wai_quantum/_api.py +++ b/wai/wai-quantum-py/python/wai_quantum/_api.py @@ -338,6 +338,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 @@ -351,7 +352,10 @@ 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 (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 @@ -369,6 +373,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 3a09e2131..8d0fee9ef 100644 --- a/wai/wai-quantum-py/tests/test_engine.py +++ b/wai/wai-quantum-py/tests/test_engine.py @@ -390,6 +390,20 @@ 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=100) + r = e["result"] + 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(): 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-service/README.md b/wai/wai-quantum-service/README.md index 27fde1a4c..561ee89a2 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/Cargo.toml b/wai/wai-quantum/Cargo.toml index e4d98a843..4860ff419 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/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 bd5f4658d..507d30440 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), `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 | @@ -197,12 +198,38 @@ 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; +- 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. + | 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 +326,14 @@ 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. 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`, @@ -321,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 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 @@ -333,7 +366,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..c87897236 100644 --- a/wai/wai-quantum/docs/published-architectures.md +++ b/wai/wai-quantum/docs/published-architectures.md @@ -105,14 +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 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, 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 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 -again runs at the beat. +runs at the beat on its step clock; on its reaction-limited clock it keeps +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 @@ -122,10 +137,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 +323,11 @@ 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). | +| 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. | @@ -342,10 +359,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 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 9218de03a..af040d7cd 100644 --- a/wai/wai-quantum/schemas/estimate.v1.json +++ b/wai/wai-quantum/schemas/estimate.v1.json @@ -43,6 +43,28 @@ "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, + "exclusiveMaximum": 1000000000 + } + }, + "additionalProperties": false + } + ] + }, "export": { "enum": [ "counts", @@ -304,6 +326,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" + } + } } } }, @@ -320,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 7f669d35f..dc7d3ad47 100644 --- a/wai/wai-quantum/src/engine/estimate/mod.rs +++ b/wai/wai-quantum/src/engine/estimate/mod.rs @@ -28,9 +28,19 @@ //! 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), `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. pub mod calibration; pub mod count; @@ -39,9 +49,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 +85,42 @@ 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 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), + 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()), + } +} + +/// 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. @@ -141,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]")?; @@ -463,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", }; @@ -508,6 +554,18 @@ pub fn figures(e: &Estimate, slowdown: u64) -> Value { }) } +/// [`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, reaction_us: Option, toffolis: u64) -> Value { + let mut v = figures(e, slowdown); + if let Some(t) = reaction_us { + v["clock"] = json!({ "reaction_us": t, "tick_us": t * slowdown as f64, "toffolis": toffolis }); + } + v +} + /// One estimate of the frontier: its code, slowdown and figures. #[derive(Clone, Debug, PartialEq)] pub struct Point { @@ -521,14 +579,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 +641,24 @@ 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)?; + 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 (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 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.", + t + )); + } let cal = options.calibration.describe(&options.qubit); if cal["applies"] == true && cal["extrapolated"] == true { notes.push(format!( @@ -602,10 +677,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, reaction_us, toffolis), + "frontier": points.iter().map(|p| figures_clocked(&p.estimate, p.slowdown, reaction_us, toffolis)).collect::>(), "notes": notes, }); + if let Some(t) = reaction_us { + result["options"]["clock"] = json!({ "reaction_us": t }); + } if derived.rotations.is_array() { result["rotations"] = derived.rotations.clone(); } @@ -646,6 +724,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 @@ -656,13 +739,14 @@ impl Evaluation { pub fn sealed(request: &Value, key: &ed25519_dalek::SigningKey) -> Result { super::job::exact_request(request)?; 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() { @@ -670,7 +754,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(); @@ -699,7 +784,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()) { @@ -708,7 +793,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, }; } @@ -756,13 +841,17 @@ 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, "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. @@ -820,6 +909,131 @@ 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": 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. + 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(); + 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(); + 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(); + 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()); + } + #[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 f8e650ee0..62fe00e38 100644 --- a/wai/wai-quantum/src/published_architectures.rs +++ b/wai/wai-quantum/src/published_architectures.rs @@ -559,16 +559,26 @@ 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 { - 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 { @@ -576,9 +586,44 @@ fn job(counts: Value, budget: f64, p: f64, calibration: &str) -> Result 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 { @@ -589,8 +634,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 +673,41 @@ 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))?; + 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, + 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.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 +745,36 @@ 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 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 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)), + ); Ok(section("FeMoCo on the surface code (arXiv:2011.03494, PRX Quantum 2, 030305, 2021)", r)) } @@ -839,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_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_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/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/src/quantum_resource.rs b/wai/wai-quantum/src/quantum_resource.rs index 3b4a00777..fd5af676b 100644 --- a/wai/wai-quantum/src/quantum_resource.rs +++ b/wai/wai-quantum/src/quantum_resource.rs @@ -94,11 +94,34 @@ //! 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. //! - **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 +//! +//! 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; +//! - 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. use crate::repro::ln; @@ -344,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. @@ -586,6 +609,41 @@ 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. 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 }, +} + +/// 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)] @@ -613,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. @@ -647,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, @@ -665,9 +727,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()) || reaction_ns < qubit.meas_ns || 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 +763,47 @@ 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.checked_mul(f.qubits).ok_or(EstimateError::Invalid)?, 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 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(), isa: *isa, @@ -724,11 +819,26 @@ pub fn estimate_isa(isa: &IsaCounts, qubit: &PhysicalQubit, qec: &QecModel, cfg: factories, algorithm_qubits, factory_qubits, - physical_qubits: algorithm_qubits + factory_qubits, - }); + physical_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 +859,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; @@ -920,6 +1048,84 @@ mod tests { } } + /// 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 }; + 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; + } + } + } + // 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(); + 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); + } + + /// 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] diff --git a/wai/wai-quantum/src/quantum_resource_receipt.rs b/wai/wai-quantum/src/quantum_resource_receipt.rs index 27b9bf4a3..a0547bbd8 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,10 +438,66 @@ 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 } + /// 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 { @@ -410,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| { @@ -469,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 { @@ -543,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")?)?, @@ -553,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) } } @@ -606,6 +757,75 @@ 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()); + } + + /// 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 2bbd77606..67e83bb43 100644 --- a/wai/wai-quantum/tests/data/published_figures.tsv +++ b/wai/wai-quantum/tests/data/published_figures.tsv @@ -142,6 +142,9 @@ 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 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 +175,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 diff --git a/wai/wai-quantum/tests/landscape_devices.rs b/wai/wai-quantum/tests/landscape_devices.rs index 67d617821..7c5b1de2c 100644 --- a/wai/wai-quantum/tests/landscape_devices.rs +++ b/wai/wai-quantum/tests/landscape_devices.rs @@ -330,6 +330,56 @@ fn every_fault_in_a_device_s_kernels_loses_its_grade() { } } +/// A host finds the same members however often it looks. Each discovery once +/// made its own instance of the GPU interfaces, and an NVIDIA Vulkan driver +/// lists its card to a process's first ten instances and not after: by a test +/// late in this file, that card was gone. Twelve discoveries, one process. +#[test] +fn a_member_is_found_however_often_the_host_looks() { + let ids = |found: Found| -> Vec { found.into_iter().filter_map(Result::ok).map(|m| m.id()).collect() }; + let first: Vec> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect(); + for look in 2..=12 { + let again: Vec> = backends(&[]).into_iter().map(|(_, f)| ids(f)).collect(); + assert_eq!(again, first, "discovery {look} found other members than the first"); + } +} + +/// A port whose sampling kernel looks a field up past its table (by 2⁴⁰ +/// entries on CUDA, so the address is never mapped) loses its grade and not +/// the device: the clean kernels then open on as many devices as before and +/// are certified bit-exact on each. A CUDA kernel that read there would lose +/// its context to every job after it in the process; WebGPU's kernels are +/// bounds-checked. +#[test] +fn a_kernel_that_looks_past_its_table_loses_its_grade_and_not_the_device() { + let present: Vec = members().iter().map(|m| m.id()).collect(); + let have = built(); + let past: Vec<(&str, &str, &str)> = [ + ("cuda", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i];", "u64 e = (u64)(kind - 1u) + (u64)h + (u64)offsets[i] + (1ull << 40);"), + ("webgpu", "return kind - 1u + bitcast(h) + graph[shape.offsets + i];", "return kind - 1u + bitcast(h) + graph[shape.offsets + i] + 0x1000000u;"), + ] + .into_iter() + .filter(|(b, _, _)| have.contains(b)) + .collect(); + for (backend, found) in backends(&past) { + if !past.iter().any(|(b, _, _)| *b == backend) { + continue; + } + for m in found.into_iter().filter_map(Result::ok) { + let c = member::certify(m.as_ref()).unwrap(); + report(&format!("fault a lookup past the table in {}: settle {}, sample {}", m.id(), c.parity(KERNEL_SETTLE).label(), c.parity(KERNEL_SAMPLE).label())); + assert_ne!(c.parity(KERNEL_SAMPLE), Parity::BitExact, "{}: a lookup past the table kept the sampler bit-exact", m.id()); + } + let again: Vec> = backends(&[]).into_iter().filter(|(b, _)| *b == backend).flat_map(|(_, f)| f).filter_map(Result::ok).collect(); + let before = present.iter().filter(|id| backend_of(id) == backend).count(); + assert_eq!(again.len(), before, "{backend}: after the fault the clean kernels opened on {} devices, before it on {before}", again.len()); + for m in again { + let c = member::certify(m.as_ref()).unwrap(); + assert_eq!((c.parity(KERNEL_SETTLE), c.parity(KERNEL_SAMPLE)), (Parity::BitExact, Parity::BitExact), "{}: the device lost its grade with the faulted kernels", m.id()); + } + } +} + /// A WebGPU member waits once a window, so a run longer than one wait but made /// of windows shorter than it finishes; and a job a member cannot finish in its /// wait goes to the next member, the answer naming the one that failed. diff --git a/wai/wai-web/src/quantum_engine.rs b/wai/wai-web/src/quantum_engine.rs index d58de530d..8cd10ab45 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