canonicalization operations to KA.jl
Note: This was intentionally submitted as an issue in order to potentially re-visit it later should the (Julia)GPU landscape hopefully continue to evolve.
For the sake of my own sanity and hopefully getting something at least usable to be merged at some point before the heat death of the universe, I am writing this in hopes of getting some opinions/consensus on which is the lesser of all evils that we must regrettably consent to.
Certain procedures, such as canonicalisation in this case, are cooperative[1] and iterative[2] in such a manner that **ABSOLUTELY CANNOT** be cleanly implemented with minimal idiomatic code (if at all) should any sane portability, performance, (a)synchronicity, or safety guarantees be provided.
[1] Used here to indicate that correct operation requires some level of communication and/or synchronisation between all threads in the executing context.
[2] Used here to indicate that a sequential series of similar procedures (which are permitted to be parallel) must be performed until a dynamically determined exit condition is satisfied, though this predicate can be deduced to be eventually satisfied within some known number of steps.
<br />
Below is a list of possibilities that I have explored, alongside the issues or limitations that they each present. It has been lovingly titled as `Here be dragons in canonicalisation: Whether 'tis nobler to block or to deadlock` and is open for further suggestions that I may have potentially missed.
1. Grid/NDrange native barriers
This is the most natural and idiomatic solution to this problem. The resulting implementation would be completely asynchronous, involve only a single kernel launch, remain compact and succinct for those who care about that aspect, as well as employ nothing more than the hardware's own native synchronisation primitives.
However, whilst AMD and NVIDIA both support the block-grid programming model and the relevant synchronisation calls (at least on any relatively recent hardware) Apple and Intel stick to the block-ndrange model and the necessary barriers are completely absent. `KernelAbstractions` does not support this for obvious reasons.
2. Grid/NDrange mutex barriers
This solution is similar to the above with the exception that synchronisation is now handled via a mutex-based implementation rather than native platform primitives. It would definitely increase the implementation footprint and its performance would absolutely suffer to an extent, possibly quite severely, but it would still retain complete asynchronicity and utilise only a single kernel launch.
However, this implementation is not safe in the slightest without some runtime knowledge of the hardware's concurrency limits. Launching more threads than the hardware can handle simultaneously will result in a deadlock situation, since the running threads will be sitting in a busy loop awaiting for signals from other threads that have not been (and **CANNOT** be) scheduled for execution. In any case, `KernelAbstractions` does not support querying the hardware limits required to implement this and any "conservative underestimate" will either severely under-utilise the hardware resources (bad) or will fail to actually be an underestimate and deadlock the device (absolutely unacceptable). Providing a few light-weight vendor-specific extensions that query this information and then pass it along should be possible in principle, assuming that the backends themselves permit those queries, but I am not certain whether it is something that `QuantumClifford` wishes to support.
3. Dynamic/Nested Parallelism
This is the second-most natural and idiomatic solution this problem. Like the first solution, it would be completely asynchronous, compact and succinct, and employing nothing more than the hardware's own native features. However, there would now be a parent kernel that manages the procedure by monitoring the exit condition and launching a child kernel at each iteration. I would not envision that the performance penalty for the successive kernel launches will be a major limiting factor since they should only constitute a minuscule fraction of the overall execution time.
However, whilst this is part of the OpenCL specification and thus should also be present in every conformant vendor's own native platform (unleses you're AMD??), it does not appear to be supported by `KernelAbstractions` regardless of whether the pertinent backend actually allows it.
4. Host-side asynchronous kernel launch
This solution would have the responsible host thread/process/task would fork/spawn a child whose sole purpose is to sit in a loop and launch successive iterations of the canonicalisation kernel until the exit condition is satisfied, thereby maintaining (at least some level of) asynchronicity since the potentially blocking kernel launches are happening elsewhere. The safety implication of this solution differ from the second proposal in that it does not hang but can potentially cause a segmentation fault if the object is de-allocated or changes its address in memory. For safety and correctness, this solution would require forbidding enqueuing any further work on the same object before a synchronisation point is established, since it could possibly be scheduled at any point during the canonicalisation loop, as well as requiring both a host-side and a device-side synchronisation barrier.
5. Host-side synchronous kernel launch.
This "solution" should be taken to be synonymous with "I give up". It does not even bother with any asynchronicity concerns, it directly goes into the kernel launch loop and will block until it is finished.
9 条评论