Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 36 additions & 28 deletions crates/ppvm-runtime/src/sum/noise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,33 @@ use crate::{config::Config, sum::PauliSum};
use num::traits::Float;
use std::hash::BuildHasher;

#[inline(always)]
fn pauli_code<W: PauliWordTrait>(word: &W, addr: usize) -> usize {
(word.get_xbit(addr) as usize) | ((word.get_zbit(addr) as usize) << 1)
}

impl<T: Config> PauliError<T> for PauliSum<T>
where
f64: std::ops::Mul<T::Coeff, Output = T::Coeff>
+ std::ops::Add<T::Coeff, Output = T::Coeff>
+ std::ops::Sub<T::Coeff, Output = T::Coeff>,
{
fn pauli_error(&mut self, addr0: usize, p: [<T as Config>::Coeff; 3]) {
self.scale(|k, v| {
match k.get(addr0) {
Pauli::I => {}
Pauli::X => {
*v *= 1.0f64 - 2.0f64 * p[1].clone() - 2.0f64 * p[2].clone();
}
Pauli::Y => {
*v *= 1.0f64 - 2.0f64 * p[0].clone() - 2.0f64 * p[2].clone();
}
Pauli::Z => {
*v *= 1.0f64 - 2.0f64 * p[0].clone() - 2.0f64 * p[1].clone();
}
Pauli::L => {}
};
let x_factor = 1.0f64 - 2.0f64 * p[1].clone() - 2.0f64 * p[2].clone();
let z_factor = 1.0f64 - 2.0f64 * p[0].clone() - 2.0f64 * p[1].clone();
let y_factor = 1.0f64 - 2.0f64 * p[0].clone() - 2.0f64 * p[2].clone();
Comment on lines 19 to +22

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pauli_error behavior was refactored to rely on bit-based Pauli classification (pauli_code + get_lbit), but there is no unit test that directly exercises PauliError::pauli_error (unlike depolarize / depolarize2). Adding a regression test that checks the scaling for I/X/Y/Z (and that L is unchanged) would help catch any future mismatches between the bit encoding and the Pauli enum mapping.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a regression test test_pauli_error in crates/ppvm-runtime/tests/noise.rs (commit 050cb02) that verifies the coefficient scaling for each Pauli at the target qubit:

  • I: coefficient unchanged
  • X: scales by 1 - 2*py - 2*pz
  • Y: scales by 1 - 2*px - 2*pz
  • Z: scales by 1 - 2*px - 2*py
  • L: coefficient unchanged (lost qubit is skipped via the get_lbit guard)


self.scale(move |k, v| {
if k.get_lbit(addr0) {
return;
}
match pauli_code(k, addr0) {
0 => {}
1 => *v *= x_factor.clone(),
2 => *v *= z_factor.clone(),
3 => *v *= y_factor.clone(),
_ => unreachable!(),
}
});
}
}
Expand Down Expand Up @@ -235,29 +241,31 @@ where
+ std::ops::Sub<T::Coeff, Output = T::Coeff>,
{
fn depolarize(&mut self, addr0: usize, p: T::Coeff) {
self.scale(|k, v| match k.get(addr0) {
Pauli::I => {}
Pauli::X => {
*v *= 1.0f64 - 4.0f64 / 3.0f64 * p.clone();
}
Pauli::Y => {
*v *= 1.0f64 - 4.0f64 / 3.0f64 * p.clone();
let factor = 1.0f64 - 4.0f64 / 3.0f64 * p;
self.scale(move |k, v| {
if !k.get_lbit(addr0) && pauli_code(k, addr0) != 0 {
*v *= factor.clone();
}
Pauli::Z => {
*v *= 1.0f64 - 4.0f64 / 3.0f64 * p.clone();
}
Pauli::L => {}
});
}
}

impl<T: Config> Depolarizing2<T> for PauliSum<T>
where
PauliSum<T>: TwoQubitPauliError<T>,
f64: std::ops::Mul<T::Coeff, Output = T::Coeff>
+ std::ops::Add<T::Coeff, Output = T::Coeff>
+ std::ops::Sub<T::Coeff, Output = T::Coeff>,
{
fn depolarize2(&mut self, addr0: usize, addr1: usize, p: T::Coeff) {
let ps: [T::Coeff; 15] = core::array::from_fn(|_| p.clone() * (1.0 / 15.0));
self.two_qubit_pauli_error(addr0, addr1, ps);
let factor = 1.0f64 - (16.0f64 / 15.0f64) * p;
self.scale(move |k, v| {
if k.get_lbit(addr0) || k.get_lbit(addr1) {
return;
}
if pauli_code(k, addr0) != 0 || pauli_code(k, addr1) != 0 {
*v *= factor.clone();
}
});
}
}

Expand Down
45 changes: 45 additions & 0 deletions docs/autotune/runtime-micro-overall/log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# runtime-micro-overall

## Target

Improve the overall `ppvm-runtime` `micro` benchmark suite, using the first fresh full-suite run in this session as the baseline.

## Architecture Notes

- The slowest representative microbenchmarks in the baseline run clustered around the shared `PauliSum` transform paths: `clifford/single/h` at `1.053 µs`, `clifford/two-qubit/cnot` at `1.050 µs`, and `scaling/cnot/1000` at `1.418 µs`.
- A quick ad-hoc profile showed cloning was not the dominant cost for representative operations on the benchmark state shape (`clone_only_ns≈72-83ns` versus total operation timings in the `170-295ns` range in the profiler), so the first optimization hypothesis focused on transform mechanics rather than clone elimination.
- Single-word profiling did not show `rehash()` dominating gate cost, so rehash-specific work was deprioritized.

## Iterations

### owned-bijective-clifford-transform

- Status: discard
- Hypothesis: Clifford conjugation is bijective, so consuming owned map entries should avoid clone-heavy `map_add` work and speed up `h`/`cnot`/related scaling benchmarks.
- Result: strong regression.
- Evidence:
- `clifford/single/h`: `1.053 µs -> 1.362 µs`
- `clifford/two-qubit/cnot`: `1.050 µs -> 1.441 µs`
- `scaling/cnot/1000`: `1.418 µs -> 2.533 µs`
- Takeaway: the current `map_add` path plus existing map behavior is materially better than the owned-entry transform on this workload.

### noise-factor-specialization

- Status: keep
- Hypothesis: the noise group spends avoidable time in repeated branchy factor selection and, for `depolarize2`, unnecessary construction of a 15-entry probability array followed by the generic two-qubit channel.
- Result: keep.
- Implemented:
- Simplified `pauli_error` and `depolarize` to use bit-based Pauli classification and precomputed single-qubit factors.
- Replaced `depolarize2` with the closed-form two-qubit depolarizing factor `1 - 16p/15` for any non-identity two-qubit Pauli term.
- Reverted the attempted generic `two_qubit_pauli_error` factor table after it added too much fixed overhead.
- Evidence from the final full-suite run:
- `noise/pauli_error`: `279.81 ns -> 213.57 ns`
- `noise/depolarize`: `272.73 ns -> 217.39 ns`
- `noise/depolarize2`: `509.21 ns -> 269.71 ns`
- `noise/amplitude_damping`: `524.70 ns -> 458.56 ns`
- `noise/two_qubit_pauli_error`: `523.07 ns -> 457.41 ns`

## Current Best

- Keep the noise specialization in `crates/ppvm-runtime/src/sum/noise.rs`.
- Discard the owned-entry Clifford transform idea unless new profiling reveals a different map implementation bottleneck.
43 changes: 43 additions & 0 deletions docs/autotune/runtime-micro-overall/metric.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
task = "runtime-micro-overall"
branch = "codex/autotune-runtime"
benchmark = "cargo bench -p ppvm-runtime --bench micro -- --noplot"

[[result]]
name = "baseline"
status = "baseline"
notes = "Initial full micro-suite run used to choose the target. Absolute medians copied from the first fresh run in this session."

[result.metrics]
noise_pauli_error_ns = 279.81
noise_two_qubit_pauli_error_ns = 523.07
noise_depolarize_ns = 272.73
noise_depolarize2_ns = 509.21
noise_amplitude_damping_ns = 524.70
clifford_h_ns = 1053.30
clifford_cnot_ns = 1050.30
scaling_cnot_1000_ns = 1418.00

[[result]]
name = "owned-bijective-clifford-transform"
status = "discard"
notes = "Replaced map_add-based Clifford transforms with an owned-entry bijective transform. This regressed representative benchmarks badly and was reverted."

[result.metrics]
clifford_h_ns = 1362.00
clifford_cnot_ns = 1441.20
scaling_cnot_1000_ns = 2533.00

[[result]]
name = "noise-factor-specialization"
status = "keep"
notes = "Kept the single-qubit noise fast path and specialized depolarize2 to the closed-form uniform two-qubit depolarizing factor."

[result.metrics]
noise_pauli_error_ns = 213.57
noise_two_qubit_pauli_error_ns = 457.41
noise_depolarize_ns = 217.39
noise_depolarize2_ns = 269.71
noise_amplitude_damping_ns = 458.56
clifford_h_ns = 1022.50
clifford_cnot_ns = 1080.40
scaling_cnot_1000_ns = 1197.10
Loading