PML-384 amps bug - #287
Conversation
QCNNClassifier.amplitude_encode hardcoded torch.complex64 for the encoded state vector and pixel amplitudes. After model.to(torch.float64) the quantum layers correctly switch to complex128, but the encoder kept truncating every input to single precision at the pipeline entry. The layers then silently upcast the state, so the model returned float64 logits computed from float32-quality amplitudes: a double-precision QCNN produced the same numbers as a float32 one (~1e-7 agreement) while every visible signal claimed float64. Derive the encoding dtype from the model instead: use the first quantum layer's complex_dtype (kept authoritative by QuantumLayer._apply during to()/double()/float() conversions), falling back to the parameter dtype via the existing complex_dtype_for utility. The encoding follows the model dtype regardless of the input tensor's dtype, matching the standard torch contract that a module computes in its parameter dtype.
Pin the amplitude-encoding dtype contract fixed in the previous commit: - to(float64) produces a complex128 encoded state and float64 logits. - A default float32 model keeps the historical complex64 encoding. - The encoding follows the model dtype, not the input dtype: a float64 input on a float32 model still encodes as complex64. - Float64 encoding matches an exact complex128 reference to 1e-14 and measurably beats the old complex64 truncation (~1e-8), proving double precision now actually flows through the entry point. - to(torch.float16) raises the existing clear ValueError rather than degrading silently. Also removes two pre-existing whitespace lint findings picked up by ruff --fix in this file.
|
@claude review |
|
Claude finished @LF-Vigneux's task in 1m 52s —— View job Claude Code ReviewI reviewed the diff against
SummaryThe fix correctly addresses the reported bug. What's good
Minor observations (non-blocking)
Testing noteI was unable to execute Overall: correct fix, minimal blast radius, good tests. LGTM. |
LF-Vigneux
left a comment
There was a problem hiding this comment.
Verything sems good! Claudes comments are out of scope
Summary
QCNNClassifier.amplitude_encodehardcodedtorch.complex64. Aftermodel.to(torch.float64)the quantum layers correctly switch tocomplex128, but the encoder kept truncating every input to single precision at the pipeline entry. The layers then silently upcast the state, so the model returned float64 logits computed from float32-quality amplitudes: no error, no warning, and a double-precision QCNN produced the same numbers as a float32 one (measured ~1e-8 truncation on encoded amplitudes, ~7e-8 agreement between a float64 and a float32 model on identical data).The fix derives the encoding dtype from the model, so
model.to(torch.float64)now gives double precision end to end. The contract, pinned by regression tests: a module computes in its parameter dtype; the encoding follows the model dtype regardless of the input tensor's dtype; unsupported dtypes such as float16 raiseValueError.This mainly affects anyone using
.double()for numerical validation —torch.autograd.gradcheckrequires float64 and perturbs at ~1e-6, so the ~1e-8 encoding noise could produce marginal or flaky Jacobian mismatches with no visible cause.Related Issue
Related Jira: PML-384
Type of change
Proposed changes
Fix (
9483f9a8)QCNNClassifier._model_complex_dtype()helper: returns the first quantum layer'scomplex_dtype(kept in sync with.to()/.double()/.float()conversions byQuantumLayer._apply), falling back to the parameter dtype through the existingmerlin.utils.dtypes.complex_dtype_forutility.amplitude_encodeuses it for both the state tensor and the pixel amplitudes instead of hardcodedcomplex64; docstring states the dtype contract.complex64encodings, byte for byte.Regression tests (
ad077e94), five new tests intests/models/test_qcnn.py:.to(float64)produces acomplex128encoded state and float64 logits end to end.complex64encoding.complex64.complex128reference to 1e-14 and measurably differs from the oldcomplex64truncation — this test fails on the pre-fix code..to(torch.float16)raises the existingValueErrorinstead of degrading silently.API changes: none.
_model_complex_dtypeis private;amplitude_encode's signature is unchanged.Out of scope — related gaps found during the investigation
Both are candidates for follow-up tickets; neither causes silent wrong numbers once this fix lands.
QuantumLayerignorestorch.set_default_dtype()at construction.MerlinModule.setup_device_and_dtypefalls back to a hardcodedtorch.float32when nodtype=is passed (merlin/algorithms/module.py:102), soset_default_dtype(float64)before construction still yields float32 quantum layers. Core torch modules honor the global default. Fixing it is one line but changes the construction default of every layer in the library — anyone setting a global double default while relying on quantum layers staying float32 would see simulation cost roughly double, so it needs its own ticket and a release-notes entry. After this PR the behavior is at least coherent: the whole pipeline stays float32 andlayer.dtypereports it honestly.QCNNClassifierhas nodtype/deviceconstructor arguments. RawQuantumLayeracceptsdtype=at construction; the classifier does not, so the only precision route is construct-then-.to(), which builds at float32 and converts afterwards.How to test / How to run
Expected: 17 passed, 1 skipped. Full
tests/models: 101 passed, 8 skipped.Screenshots / Logs (optional)
Pre-fix measurement on identical data and seeds: encoded amplitudes truncated by ~2.4e-8 (complex64 round-trip), float64-model logits within ~6.8e-8 of a float32 model. Post-fix: encoding matches an exact complex128 reference to 1e-14.
Performance considerations (optional)
None for existing float32 usage (unchanged path). Float64 models now genuinely compute in double precision downstream of the encoder, with the usual ~2x memory/compute cost of complex128 simulation — which is what
.to(torch.float64)was asking for.Documentation
amplitude_encodenow states the dtype contract; new helper fully documentedChecklist
PR title includes Jira issue key (e.g., PML-126)
"Related Jira ticket" section includes the Jira issue key (no URL)
Code formatted (ruff format)
Lint passes (ruff)
Static typing passes (mypy) if applicable — not configured for this repo; not run
Unit tests added/updated (pytest) — 5 regression tests
Tests pass locally (pytest) — tests/models: 101 passed, 8 skipped
Tests pass on GPU (pytest) — not run; the change is dtype selection only, no device-specific paths
Test coverage not decreased significantly — increased: the encoding dtype contract had no coverage
Docs build locally if affected (sphinx)
With this command:
the docs are built without any warning or errors.
New public classes/methods/packages are added in the API following the methodology presented in other files — n/a: no new public API
Dependencies updated (if needed) and pinned appropriately — no dependency changes
PR description explains what changed and how to validate it