QuEST-Kit / QuEST

A multithreaded, distributed, GPU-accelerated simulator of quantum computers
https://quest.qtechtheory.org/
MIT License
398 stars 137 forks source link

accelerating matrix validation #441

Closed TysonRayJones closed 4 months ago

TysonRayJones commented 4 months ago

problem

I was intending to bind field .isUnitary to large (heap-memory) matrices, assessed at runtime inside functions like syncCompMatr(). This would enable unitarity to be assessed once before repeated application of a matrix to a Qureg, avoiding duplicate computation; especially worthwhile for full-state operators like FullStateDiagMatr. This would integrate nicely with the new validation toggle - when validation is off, sync*() would set .isUnitarity = -1 to indicate it was unknown. If validation was later restored and that same matrix passed to a simulation function like multiQubitUnitary(), unitarity could be lazily evaluated at that moment, overwriting the matrix's field to preclude later repeated evaluation.

Alas, this isn't directly possible with a value (like int) field - the QuEST API passes structs by value/copy! Functions like sync*() cannot modify the passed matrix struct's .isUnitary field. Even making just the sync*() functions accept pointers (already grossly inconsistent), the lazily deferred evaluation of isUnitary after a validation toggle would remain impossible. That would mean unitarity is wastefully re-evaluated at every invocation of e.g. multiQubitUnitary() until the next call to sync*(). That's user-astonishing; why should a user need to re-synch their unmodified matrix after toggling validation back on?

We could at least avoid silent wasteful re-evaluation by throwing a validation error when the user passes a struct with unknown unitarity (.isUnitary=-1), and validation is on. The error can instruct users to recall sync*() on any matrix which was previously sync'd while validation was off. It's a shame fresh evaluation isn't automatic, but not unreasonable.

Beware however that users may be passing the API structs through several layers of their own functions before passing a pointer to sync*(). That is, the pass-by-copy nature of the QuEST API may encourage them to think all structs are immutable, so the struct modified sync*(ptr) might already be a stack copy, keeping the 'outer most' instance unchanged. On some occassions, that will make validation will fail; structs reaching validation remain un-synced. But on other occassions, if a user syncs correctly but later uncorrectly, the heap amps will be modified but the stack .isUnitary will not, corrupting the field.

Note that disabling validation before calling sync*() would only be necessary when a user wishes to avoid even a single calculation of unitarity. If they can permit one unitarity check, then they can simply call sync*() before disabling validation and unitarity will be thereafter forever known (until the next sync*()).

hacky solution

We could make isUnitary a pointer to a single int in heap memory, heh! Then all copies of the "same" struct retain access to the flag, which is modifiable. We can even defensively make the pointer const to avoid a leak! This idea isn't too hideous; we only need this isUnitary field in structs that already have heap memory.