mlx: run with MLX's graph-cache thrashing check off; keep the first panic - #212
Conversation
…anic Two changes from the investigation in docs/maxusai/mlx-thrash-check-masks-as-cudagraph.md (PR #211). 1. The runner subprocess starts with MLX_ENABLE_CACHE_THRASHING_CHECK=0 unless the operator exported the variable on the server. MLX's CUDA backend keys a graph cache by shape, and its "thrashing check" is a LIFETIME miss counter that throws once misses pass 2 x MLX_CUDA_GRAPH_CACHE_SIZE (default 400). The runner cannot catch and continue a throw out of graph commit: the request dies, the deferred prefix-cache close fails on the encoder the throw left behind, and the log blames cudaGraphAddDependencies. A think-on decode crosses the threshold in ~700 distinct prefill lengths. With the check off the LRU still evicts and nothing measurable changes: 120/120 and 400/400 clean under the conditions that failed 120/120 with it on. 2. TextGenerationPipeline's deferred cleanups go through guardClose, which keeps the FIRST panic when a cleanup panics while the request is already unwinding: the cleanup's panic is logged, the original is re-raised wrapped with the stack captured while its frames were still live, and recoverRequest / mlxthread report the real cause. Go's recover only ever returns the most recent panic, which is why the fuse spent a day being diagnosed as cudaGraphAddDependencies. Tests: the default and the operator override for the env; guardClose keeps the first panic and still runs every cleanup, is a plain call on the normal path, and lets a cleanup's own first panic through; runRequest reports the first cause. go test ./... is clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewing as consolidator. I added So the guarantee I thought I had shipped was "a panic reaches you as a reason". The guarantee that actually existed was "a panic reaches you". This closes that, and it is the reason the thrash throw never appeared in any log. Two properties I checked because they are what make it safe rather than merely better:
Wrapping with the stack captured while its frames were still live is the detail that makes the re-raise useful rather than a bare error string. On the env defaultApplying it in 120/120 ok against yesterday's 120/120 fail under identical conditions is about as clean as a before/after gets. Two things worth stating in the code comment
The control arm ( |
runRequest's mlxThread == nil branch never runs in production (server.go always builds the Runner with a worker), so the existing test skipped the path that matters: the worker recovers the *firstPanic, wraps it in its own panicError and re-raises it on the calling goroutine, and recoverRequest prints that with %v. Cover it with a real mlxthread worker and assert the message still leads with the original cause, with the worker stack after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nc (sync15nt) The think-on half of the sync-0.32.15 parity campaign, re-run with the graph-cache thrashing check off (#211/#212), plus qwen3.6 nvfp4 in both modes. Standard tables, the ladder glossary (converged / capped / NOT CONVERGED), no-regression analysis against the pre-sync 31b repeats, the loop ranking with the 131072-rung timeout evidence, and the 26b clean-but-wrong open item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-off Picks up the upstream mlxrunner prefix-cache rework alongside this branch's pipeline.go changes; dry-run and actual merge both clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the two fixes proposed in #211 (docs(mlx): the graph-cache thrashing check kills the runner and masks itself as cudaGraphAddDependencies).
What changes
MLX_ENABLE_CACHE_THRASHING_CHECK=0for the MLX runner subprocess by default (x/mlxrunner/client.go,mlxRunnerEnvDefaults, applied next to theCUDA_PATH/CUDA_HOMEsetEnvcalls). A non-empty value exported on the server is left alone, so an operator who wants MLX's advisory back setsMLX_ENABLE_CACHE_THRASHING_CHECK=1.x/mlxrunner/unwind.go):TextGenerationPipeline's five deferred cleanups go throughguardClose. If a cleanup panics while the request is already unwinding a panic, the cleanup's panic is logged and the original is re-raised (wrapped with the stack captured while its frames were still live), sorecoverRequest/mlxthreadreport the real cause instead of whatever the lastclose()hit. On the normal path it is a plain call; a cleanup that is the first thing to fail still propagates.Why
MLX's CUDA backend keys a graph cache by shape; the "thrashing check" (ml-explore/mlx ollama#2600) is a lifetime miss counter that throws once misses pass
2 × MLX_CUDA_GRAPH_CACHE_SIZE(default 400). The runner cannot catch-and-continue a throw out of graph commit: the request dies, the deferred prefix-cacheclose()fails on the poisoned encoder, and the log blamescudaGraphAddDependencies. A think-on decode crosses the threshold in ~700 distinct prefill lengths — on every image we ship, and not introduced by the v0.32.15 sync. With the check off the LRU still evicts and nothing measurable changes. Full mechanism and measurements:docs/maxusai/mlx-thrash-check-masks-as-cudagraph.md(#211).Verification
go test ./...clean (golang:1.26 container); new tests: env default + operator override;guardClosekeeps the first panic and still runs every cleanup, plain on the normal path, lets a cleanup's own first panic through;runRequestreports the first cause.maxusai/ollama:sync-0.32.15(native payload unchanged vs76918a7), fresh containers, gemma4:12b-nvfp4,MLX_CUDA_GRAPH_CACHE_SIZE=8to provoke the fuse fast:/proc/<pid>/environ):MLX_ENABLE_CACHE_THRASHING_CHECK=0present with no thrash env on the container — the default reaches MLX.MLX_ENABLE_CACHE_THRASHING_CHECK=1, same cache 8, n=12): running — result appended below when it lands.Notes
vsuite) are unaffected; images built from this commit no longer need it.