Repository navigation
fix(mlxrunner): admit against the overridden budget, without asking for eviction - #186
Conversation
…or eviction #180 let an operator lower the MLX ceiling but left the admission check reading the un-overridden `available`. The two were the same number before the override existed, so one check covered both; now a cap below the model size is admitted and handed straight to the subprocess. Reproduced by executing Client.Load on the numbers from #180's own benchmark -- gemma4:31b-nvfp4 at 18.29 GiB, 88 GiB free, OLLAMA_MLX_MEMORY_LIMIT=8589934592: Load returns nil even with requireFull=true and the child gets a ceiling 10.3 GiB under the weights. A cap of 1 byte behaves identically. Nothing downstream catches it: runner.go evaluates the weights before configureMemoryLimit runs, so the cap lands on an already-resident model, and that function only guards upward. The operator sees a runner death or a 500 at first inference instead of a refusal naming the variable they set. The naive repair -- changing the existing check to read vramBudget -- is a second bug. sched.go turns ErrLoadRequiredFull under requireFull into "evict a runner and retry"; eviction raises FreeMemory, but the operator's cap is a constant that free memory cannot move, so every retry recomputes the same budget and fails identically. That evicts every other model to satisfy a constraint eviction cannot satisfy. So the two shortfalls are kept distinct, and the ordering is load-bearing: * modelSize > available -> physical, evictable, stays ErrLoadRequiredFull * modelSize > vramBudget -> operator-set, NOT evictable, plain error naming OLLAMA_MLX_MEMORY_LIMIT and both numbers Tests exercise Client.Load rather than budgetWithOverride, because the existing unit tests pass with the call site deleted -- the feature could be entirely unwired and the suite would stay green. Verified against both failure modes: reverting to the merged behaviour fails two of them, and the naive ErrLoadRequiredFull repair fails the same two for different reasons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This is a correct bug report against code I merged in #180, and the analysis is better than the analysis that produced the bug. Endorsing it. I verified the test criticism rather than taking it on faith. Deleted the Green with the feature entirely unwired. My tests exercised a helper nobody was required to call — they pinned the arithmetic and asserted nothing about the behaviour. Driving The eviction point is the part I would have got wrong. My instinct on reading "the admission check uses the wrong variable" was to point it at The ordering being load-bearing is worth the emphasis it gets: physical first (evictable), operator-set second (not). Reversed, a genuinely over-subscribed card would report an operator problem and never trigger the eviction that would fix it. One small thing worth stating in the header, since #180's text does not: the failure this fixes is silent at load and loud later. |
Follow-up to #180, which is already on
main. It let an operator lower the MLX ceiling but left the admission check reading the un-overriddenavailable. The two were the same number before the override existed, so one check covered both — now a cap below the model size is admitted and handed straight to the subprocess.Reproduced, not inferred
By executing
Client.Loadon the numbers from #180's own benchmark — gemma4:31b-nvfp4 at 18.29 GiB, 88 GiB free,OLLAMA_MLX_MEMORY_LIMIT=8589934592:Loadreturns nil even withrequireFull=trueNothing downstream catches it:
runner.goevaluates the weights beforeconfigureMemoryLimitruns, so the cap lands on an already-resident model, and that function only guards upward. The operator gets a runner death or a 500 at first inference instead of a refusal naming the variable they set.The naive repair is a second bug
Changing the existing check to read
vramBudgetroutes an operator shortfall intoErrLoadRequiredFull.sched.goturns that, underrequireFull, into "evict a runner and retry". Eviction raisesFreeMemory— but the operator's cap is a constant that free memory cannot move, so every retry recomputes the same budget and fails identically. That evicts every other model to satisfy a constraint eviction cannot satisfy.So the two shortfalls stay distinct, and the ordering is load-bearing:
modelSize > availableErrLoadRequiredFullmodelSize > vramBudgetOLLAMA_MLX_MEMORY_LIMITand both numbersTests exercise
Client.Load, not the helperThe existing unit tests pass with the call site deleted — the feature could be entirely unwired and the suite would stay green. These drive
Loaditself.Verified against both failure modes:
TestLoadStillEvictsForAPhysicalShortfallis the ordering guard: a real out-of-memory must keep asking the scheduler to free something rather than reporting the operator's knob.Not included
Other findings from the same review, left for their own change since #181 is still open and touches this area:
OLLAMA_MLX_CACHE_LIMITis discarded in total silence, indistinguishable from unset — the failure mode fix(mlxrunner): let an operator lower the MLX memory limit #180 was written to remove, reappearing in feat(mlxrunner): bound MLX's retained buffer cache instead of the whole card #181mlx.SetCacheLimithas no test at all; swapping its C call tomlx_set_memory_limit, the exact confusion its doc comment warns about, survives the whole suiteOLLAMA_MLX_MEMORY_LIMITis still silently discarded here too (minor, same theme)🤖 Generated with Claude Code