Repository navigation
fix(server): --no-batch no longer wraps the cache-pool capacity to zero - #14
Conversation
385cac1 to
2d05743
Compare
The legacy `--no-batch` worker builds its BatchScheduler with `max_queue_depth = usize::MAX`. `BatchScheduler::with_config` sized the CachePool as `max_batch_size + max_queue_depth`, which wraps to 0 in a release build (no overflow checks), so the pool refused every dense-KV sequence with `CachePool: max capacity (0) reached` and every request answered HTTP 500 "Server busy". Saturate the sum. Adds a regression test on the pool capacity of a scheduler built with an unbounded queue depth, using the synthetic NemotronH scheduler of request_memory_tests, which now takes the queue depth as a parameter.
2d05743 to
50be7a8
Compare
eejd
left a comment
There was a problem hiding this comment.
Independent review (read-only; scratch builds under /developer/Workspaces/scratch/rv14, engine runs under the luigi lock, nothing pushed/merged/tagged). Reviewed head 50be7a89 onto local fb47616d.
Verdict: the fix and its test are correct; no blocking findings.
Fix correctness
max_batch_size.saturating_add(max_queue_depth)(config.rs:92) is the only sum of this shape onmain99967257(grepofmax_queue_depth *[-+*]/usize::MAX *[-+]oversrc/; the other sentinel users eithersaturating_sub/checked_*(DRY windowsampling.rs:2382,app.rsbudget,cache_args.rs:229) or compare (state.rs:198depth < maxthen+1)).- Downstream of the saturated value:
CachePool::max_sequencesis read only byallocate_with_layout(active.len() >= max_sequences, cache.rs:6670, never true forusize::MAX) and by the new test;CachePool::newandPrefillQueue::with_capacityallocate nothing proportional to the argument.usize::MAXtherefore means "no limit", which is what the legacy worker (model_worker.rs:1338, "unbounded") asks for. A user-supplied huge--max-queue-depthon the batched path goes through the same line and is covered too. - Release vs debug:
[profile.release]has nooverflow-checks, andtest-fastinherits it, so the unfixed+wraps to 0 in both (the 500max capacity (0)). A dev/debug build would instead panic at scheduler construction ([INFERENCE]: standard Rust semantics, not exercised here). The fix is right for both. - Not blocking, claim in the PR body ("
mainhas no other sites"):src/distributed/pipeline/serving.rs:903(start + self.chunk_size).min(..)has the same shape (chunk_size == usize::MAXwhen chunking is disabled; wraps forstart > 0). Only tests constructChunkedPrefillPipeline(no production caller insrc/), so it is unreachable today. Worth an upstream note, not a change here.
Regression test
Built main+fix and re-ran only the one line reverted, same flags as the port (--profile test-fast --features metal,accelerate, -p mlxcel --lib pool_capacity_tests -- --test-threads=1):
- with fix: 2 passed;
- fix reverted (
max_batch_size + max_queue_depth):an_unbounded_queue_depth_does_not_zero_the_cache_poolFAILEDleft: 0, right: 18446744073709551615; the bounded1 + 8case passes.
So the test fails without the fix for the reason stated.
Behaviour on the new build (test-fast build of 50be7a89, not the port binary)
- bare
main99967257,--no-batch,/v1/completionsN=200: HTTP 500CachePool: max capacity (0) reached(defect reproduces on main). 50be7a89:--no-batchN=33/200/267 HTTP 200, text identical to mlx_lm; Llama-3.2-1B default-launch length sweep 18/18 identical to mlx_lm; stock snapshotEOS tokens from config: [128009, 128001, 128008],finish=tool_calls, 30 tokens, 3/3.
History / branches / UPSTREAM.md
- All 8
archive/*tags are annotated tags oneejd/rs-mlxcel; each dereferences (viagh api git/tags) to exactly the SHA in its name and inUPSTREAM.md(728d339d,c7832e5e,ef5c2c0a,0c516210,5ea5aeac,c3d5c649,b9f2ed8e,03c24b30). Oldlocalc7832e5e=v0.7.0(ec9db8bf) +UPSTREAM.mdonly (diff: 1 file, +22). The four upstream commits named in the table (e6c9cda9,c480ed67,7c3bece5,5a5e73b4) are ancestors of99967257and their subjects match the stated PRs.git describegivesv0.7.0-364-g99967257; the MLX pin there is243bdf09. - Fork
main=99967257at review start (gh api branches/main);local=main+ exactly one docs-only commit (fb47616d,UPSTREAM.md, 1 file +38); the topic is not merged intolocal(PR is open, baselocal, mergeable, no CI configured). Upstreammainhas since moved toc40e2d59(+4 commits: lablup#2448, lablup#2449, lablup#2453, lablup#2455; none touchesconfig.rs, the CMake pin orCargo.lock), so the forkmainis 4 behind; that is drift, not a defect. - Force-pushes of
topic/no-batch-500: the first (overb9f2ed8e) is covered byarchive/topic-no-batch-500-b9f2ed8(v0.7.0 base; helperscheduler_pool_capacity+ two pure-function tests; superseded: the new test goes through the realBatchScheduler::with_config, which is the better test). The intermediates385cac10and2d057436are still fetchable by SHA (gh api commits/<sha>) but unreferenced; I diffed their patches against50be7a89: the non-test hunks (config.rsfix,request_memory_tests.rs) are identical, they differ only in the firstpool_capacity_testsdraft (anenqueue_request-driven test, which per the PR body passed without the fix, so nothing of value was lost). One housekeeping point: the evidence binaries cited in the PR body and in #1 were built from those two commits; unreferenced commits can be garbage-collected, so tag them (archive/topic-no-batch-500-385cac1,-2d05743) if that provenance should stay checkable.
Finding (outside the diff, in UPSTREAM.md on local, lines 4-7)
UPSTREAM.md states the dated tag v0.7.0.<YYYYMMDD>-local<NN> as the rule. docs/development-model.md (macports-ports-local, lines 284-285, 310) and ADR-0008 §2/§3 define the local tag as v<base>-local<NN> / version <base>-local<NN> with <base> = last upstream release local contains, and reserve <last-release>.<YYYYMMDD> for -devel (no local patches). The owner decision on the tag form is still open (see macports-ports-local#1403); if it goes to v0.7.0-local01, lines 4-7 of UPSTREAM.md need a follow-up commit on local (a plain commit, no force-push). Until then the sentence should read as a proposal.
Closes #9.
Re-cut (2026-10-19, owner option B of #1): this PR now targets the new
local(fb47616d= upstreammain99967257+UPSTREAM.md). The oldlocalc7832e5e(v0.7.0 base) and the previous head of this branchb9f2ed8eare archived as tagsarchive/local-pre-recut-c7832e5andarchive/topic-no-batch-500-b9f2ed8(all five old topic heads and the evidence integration commit are taggedarchive/*;UPSTREAM.mdrecords old -> new).localwas moved with--force-with-lease=local:c7832e5e...after the tags were pushed; this branch with--force-with-lease=topic/no-batch-500:b9f2ed8e...(twice more over my own intermediate heads385cac10and2d057436; both are now taggedarchive/topic-no-batch-500-385cac1andarchive/topic-no-batch-500-2d05743: the evidence binaries were built from them).Problem
mlxcel serve --no-batchanswered every /v1/completions request with HTTP 500Server busy: CachePool: max capacity (0) reached.Cause
The legacy worker builds its scheduler with
max_queue_depth = usize::MAX;BatchScheduler::with_configsized the pool asmax_batch_size + max_queue_depth, which wraps to 0 in a release build (nooverflow-checks), andCachePool::allocaterefuses a dense-KV sequence atactive.len() >= max_sequences. That is the only reachable wrapping sum onmain99967257(grepoversrc/: the otherusize::MAXusers saturate, compare or are test-only;PrefillQueue::with_capacityandCachePool::newallocate nothing). One moreusize::MAX + shapeexists atsrc/distributed/pipeline/serving.rs:903(start + chunk_size), unreachable (ChunkedPrefillPipelinehas no production caller); noted in #9, not changed here.Fix
max_batch_size.saturating_add(max_queue_depth). Testpool_capacity_tests(scheduler built with an unbounded queue depth must keeppool().max_sequences() == usize::MAX; the bounded case stays1 + 8).request_memory_tests::scheduler()now delegates toscheduler_with_queue_depth(n).Evidence
cargo test -p mlxcel --lib pool_capacity_tests -- --test-threads=1: before (fix reverted)an_unbounded_queue_depth_does_not_zero_the_cache_poolFAILEDleft: 0, right: 18446744073709551615; after 2/2 pass. (A first version of the test, driven throughenqueue_requeston the synthetic NemotronH model, passed before the fix because that model's sequence state is model-owned and skips the capacity check; replaced.)local(MLX pin243bdf09,mlx.metallib1510naxsymbols;--no-batch, Llama-3.2-1B-4bit, /v1/completions N=33/200/267): 3/3 HTTP 200, text identical to mlx_lm at all three lengths; baseline installed portmlxcel 0.7.0: 3/3 HTTP 500. Full table: engine-fix(mlxcel): build and run upstream main against the sweep/abort/EOS/BOS matrices; decide the delivery route for the four fixes #1.385cac10/2d057436of this branch, whose non-test sources are identical to the head (the later commits differ in thepool_capacity_testsmodule only).Review dispositions (RevMlxcelB, 2026-10-19): overall correct
serving.rs:903same pattern, unreachable: recorded on engine-fix(mlxcel):--no-batchreturns HTTP 500CachePool: max capacity (0) reachedon every /v1/completions request #9 as an upstream-note item; no change here.local, no force-push); the first tag isv0.7.0-local01.mlxcel-coretests that fail on baremain(not this change): diagnosis(mlxcel): six mlxcel-core --lib tests fail on bare upstream main 99967257 with NAX kernels (PrefillTrim / tile-padded widths) #16; the port deselects exactly those.Review dispositions: pending (independent re-review)