Skip to content

【代码贡献】QSVD: skip the unused state-vector simulation in the loss inner loop - #44

Open
mnn31 wants to merge 1 commit into
OriginQ:developfrom
mnn31:fix/qsvd-example-hang
Open

【代码贡献】QSVD: skip the unused state-vector simulation in the loss inner loop#44
mnn31 wants to merge 1 commit into
OriginQ:developfrom
mnn31:fix/qsvd-example-hang

Conversation

@mnn31

@mnn31 mnn31 commented Aug 9, 2026

Copy link
Copy Markdown

Problem

example/QAlgBase/testeg_QSVD.py takes minutes to finish. #39 reports the same
thing independently (>300 s on that machine) and deliberately leaves it for
separate handling, which is what this PR does. On my machine the unmodified
example runs 137 s.

#39 also edits QSVD.py, removing an unused machine local in max_eig. The
two hunks are about ten lines apart and merge cleanly; I verified against #39's
head: automatic merge, 21 tests pass, the example runs in 22 s.

Root cause

SVD.loss computes a full state-vector simulation unconditionally, but only
the return_type=False branch reads it:

stv = StateVector(self.q0 + self.q1)
phase = stv.evolve(cir).ndarray().real       # dead on the return_type=True path
phase = phase.reshape(2**self.q1, 2**self.q0)
prob = np.diagonal(re.reshape(2**self.q1, 2**self.q0))
same_p = np.sum(prob)
if return_type:
    return 1-same_p                          # phase never used
else:
    return phase, np.argmax(abs(phase))

QSVD_min drives loss through SLSQP with a numerical jacobian, so the scalar
branch is the inner loop: 4952 evaluations for an 8x8 matrix (48 parameters,
100 iterations). Every one of them paid for a state vector it threw away.

cProfile over 200 scalar loss calls on an 8x8 matrix:

   ncalls  tottime  percall  filename:lineno(function)
      200    1.095    0.005   {built-in method pyqpanda3.quantum_info.quantum_info.evolve}
      200    0.070    0.000   {built-in method pyqpanda3.core.core.run}
      200    0.038    0.000   QSVD.py:122(loss)
      200    0.019    0.000   {built-in method pyqpanda3.core.core.amplitude_encode}

86% of the runtime was the discarded evolve, which is roughly 15x more
expensive than the CPUQVM.run that actually produces the loss.

It is not an infinite loop and not exponential blow-up. Removing the dead work
is enough.

Fix

Move the three StateVector lines inside the else branch. Nothing else
changes: the probability path, the shot count and the normalization check in
parse_quantum_result_list are all untouched, so error behaviour such as the
nan ValueError on a zero matrix is preserved.

Before / after

The optimizer trajectory is identical, same nfev, same nit, same final loss,
so this is runtime only:

matrix before after nfev final loss
4x4 ~1 s ~0.3 s 1126 0.000000
4x8 ~9 s ~2 s 4145 0.000300
6x8 ~32 s ~4 s 4953 0.006501
8x8 ~35 s ~4 s 4952 0.003647

example/QAlgBase/testeg_QSVD.py on unmodified develop vs with the fix:
137.3 s → 24.7 s, 6 passed both times, and every printed singular value,
cosine similarity and error metric is byte-identical between the two runs.
CPU time drops further than wall time, 626 s → 24 s, because evolve was
running multithreaded.

Correctness against numpy.linalg.svd, seed 42, after the fix:

matrix max relative error on singular values cos with u[:, 0] cos with v[0]
4x4 1.69e-10 1.000000 1.000000
4x8 8.21e-03 1.000000 0.999999
8x8 7.93e-02 0.999986 0.999990

The residual error on the larger matrices is the variational ansatz hitting
SLSQP's default 100-iteration limit, which is pre-existing and unchanged by
this PR.

Tests

Four tests added to test/QAlgBase/Test_QSVD_SVD.py:

  • test_singular_values_against_numpy: 4x4, relative error against
    numpy.linalg.svd below 1e-2
  • test_singular_vectors_against_numpy: 4x8, left and right dominant singular
    vectors overlap the numpy ones above 0.99, using the index loss reports
  • test_loss_skips_state_vector_on_scalar_path: regression guard, counts
    StateVector construction and asserts 0 on the scalar path, 1 on the matrix
    path
  • test_qsvd_min_runtime: 8x8 QSVD_min under 20 s

Both regression tests fail on unmodified develop (the runtime one at 30.8 s)
and pass with the fix.

$ cd test && python -m pytest -o addopts="" -q
21 passed in 11.86s          # 17 before this PR

Checked and deliberately left out

max_eig derives both register indices from max_index % 2**q0. The right
register index is semantically max_index // 2**q0, but the only indices the
module ever produces are the diagonal ones d * 2**q0 + d with d < 2**q0, for
which the two expressions are equal. Verified on 4x4, 4x8, 8x4 and 2x4 with
non-zero argmax values (argmax 10, 15, 27): lo == hi every time, cosine
overlap 1.0 either way. No observable defect, so no change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant