Allow calloc-like optimisations in SparsePauliOp allocations - #16830
Conversation
|
One or more of the following people are relevant to this code:
|
As of Rust 1.98, `vec![0.0; 2 * len]` lowers to OS/kernel-level
"allocate zero-filled" functions, but using a non-primitive type in the
`Vec` (like `Complex64`) doesn't permit this optimisation. We want to
use `calloc` instead of manually writing zeros into the vector because
it often gets kernel-level support to have zero impact on pages that are
never written to, whereas manually issuing write calls across each row
guarantees that we have to page in everything.
This has a larger performance impact for large dense matrices with high
sparsity factors.
Using a benchmark script:
```python
import timeit
import numpy as np
from qiskit.quantum_info import SparsePauliOp
def make_sparse_list(num_qubits, num_terms, locality, rng):
terms = []
for _ in range(num_terms):
indices = rng.choice(num_qubits, size=locality, replace=False)
label = "".join(rng.choice(tuple("XYZ")) for _ in indices)
coeff = complex(rng.normal(), rng.normal())
terms.append((label, indices, coeff))
return terms
rng = np.random.default_rng(42)
print(f"{'n':>3} {'terms':>6} {'locality':>8} {'time (s)':>10}")
configs = [
(6, 10, 2),
(10, 10, 2),
(10, 50, 2),
(10, 100, 4),
(14, 10, 2),
(14, 50, 2),
(14, 100, 4),
]
for num_qubits, num_terms, locality in configs:
terms = make_sparse_list(num_qubits, num_terms, locality, rng)
op = SparsePauliOp.from_sparse_list(terms, num_qubits=num_qubits)
reps, tot = timeit.Timer(op.to_matrix).autorange()
print(f"{num_qubits:>3} {num_terms:>6} {locality:>8} {tot / reps:>10.4f}")
```
On main (as of aae0d80), on a Macbook Pro M4 Max using 16 threads,
the timings were:
```text
n terms locality time (s)
6 10 2 0.0000
10 10 2 0.0003
10 50 2 0.0003
10 100 4 0.0003
14 10 2 0.2860
14 50 2 0.2767
14 100 4 0.2757
```
With both this commit and Qiskitgh-16828 in place, the numbers instead become:
```text
n terms locality time (s)
6 10 2 0.0000
10 10 2 0.0001
10 50 2 0.0001
10 100 4 0.0002
14 10 2 0.0229
14 50 2 0.0313
14 100 4 0.0459
```
ef30e6c to
a5f2042
Compare
Actually it transpires (of course...) that `bytemuck` has this safe interface for zeroable types, which makes the requested optimisation rather more explicit. I left the old commit message in place too because I liked that discovery.
e150d20 to
ecfb27f
Compare
|
Ha - totally forgot that |
|
Just curious, how did you discover this? |
|
It comes very indirectly from #16809 - David and Julien had been looking at the implementation, doing some benchmarking, and Julien had discovered a weirdness where After that, it just seemed most likely that the weirdo ad-hoc initialisation was at fault here; the raw |
As of Rust 1.98,
vec![0.0; 2 * len]lowers to OS/kernel-level "allocate zero-filled" functions, but using a non-primitive type in theVec(likeComplex64) doesn't permit this optimisation. We want to usecallocinstead of manually writing zeros into the vector because it often gets kernel-level support to have zero impact on pages that are never written to, whereas manually issuing write calls across each row guarantees that we have to page in everything.This has a larger performance impact for large dense matrices with high sparsity factors.
Using a benchmark script:
On main (as of aae0d80), on a Macbook Pro M4 Max using 16 threads, the timings were:
With both this commit and gh-16828 in place, the numbers instead become:
AI/LLM disclosure
The benchmark script in the commit message is a significantly modified version of a script Julien generated with Claude. Nothing else is LLM.