Serialize GPU-stream drivers to fix concurrent-encoder crashes
MLX is not thread-safe: mlx_eval/mlx_async_eval run gpu::eval inline on the
calling thread, encoding into and committing the shared Metal command buffer,
and the C++ stream mutex only guards the stream-map lookup (not the encoder).
Since evalLock was dropped from the eval hot path, a request's prefill
async_eval could run concurrently with a teardown/unload Stream.synchronize()
on the same command buffer.
Live-reproduced SIGABRT (disconnect-during-cold-load + retry): 'Completed
handler provided after commit' in addCompletedHandler, and AGX 'command
encoder is already encoding'. Two threads: unload->Stream.gpu.synchronize()
committing the buffer while startSoloFastPath->TokenIterator.prepare->async_eval
added a completion handler to it.
Fix: hold evalLock across the brief CPU-side encode+commit in eval/asyncEval/
MLXArray.eval (Stream.synchronize/clearCache already take it). The GPU executes
asynchronously after the lock releases, so CPU->GPU pipeline parallelism is
preserved; the lock is uncontended in steady-state single-producer decode and
only serializes during teardown/cancel/cross-model.
Also guard compiled activations (safeGeluApproximate/compiledSwiGLU/compiledGeGLU)
with CompiledDecodeTrace.isActive to avoid an illegal nested compile inside the
compiled-decode trace, and drain the stream on generateLoopTask cancel/error
early-exits.
Validated live: disconnect repro 35/35 clean (was crash at iter 1-2), concurrent
GPU 15/15 clean, decode 76.6 tok/s (no regression), multiturn coherent.