Skip to content

fix(server): fail-fast unschedulable KV waits - #227

Closed
bakon11 wants to merge 1 commit into
mudler:mainfrom
bakon11:fix/serve-kv-abort-max-tokens
Closed

fix(server): fail-fast unschedulable KV waits#227
bakon11 wants to merge 1 commit into
mudler:mainfrom
bakon11:fix/serve-kv-abort-max-tokens

Conversation

@bakon11

@bakon11 bakon11 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Hermes-class clients can send huge system prompts. With default KV (256×32=8192 tokens) oversized prompts never admit under full_sequence_must_fit and used to sit in waiting forever (model_executed=0, GPUs idle).

  • Abort waiting requests that cannot ever get KV (fail-fast finish)
  • Does not clamp max_tokens — main already treats non-positive max_tokens as unset (SERVE-MAXTOKENS-UNSET)
  • Scheduler + engine/core_proc paths covered; unit test added
  • Docs: ENVIRONMENT.md (VT_ENGINE_STEP_LOG), USAGE.md scheduler fail-fast section

Land order

  1. feat(rocm): V1 sampling ops for Hermes EngineCore #234 ROCm V1 sampler + parallel RandomSample (Hermes chat finish + temp>0 decode)
  2. This PR (fix(server): fail-fast unschedulable KV waits #227) KV fail-fast
  3. fix(server): SSE keepalives via collector timed wait #316 SSE keepalives
  4. feat(gemma4/rocm): FP8 resident MoE + SharedK-WMMA (split from #228) #317 Gemma4/ROCm FP8 MoE / SharedK-WMMA (split from closed feat(gemma4/rocm): FP8 resident MoE + SharedK-WMMA prefill (RDNA4) #228)

Test plan

  • Local unit: scheduler unschedulable wait aborts
  • Local gates: doc-checkpoint, env-doc, device-leakage, pr-size, agent-record
  • CI green after tip-of-main rebase
  • Manual: prompt that exceeds num_blocks×block_size finishes with clear error (no infinite wait)

TESTERS WANTED (BF16/FP16 MoE)

Need multi-GPU ROCm hosts with host RAM ≫30G on MoE (Gemma-4-26B class). Dense 12B is not a MoE proxy.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

TESTERS WANTED — BF16 / FP16 MoE long-prompt / KV path on ROCm

This PR is quant-agnostic (scheduler fail-fast when KV cannot admit a wait + max_tokens clamp). Lab soak used FP8 Gemma-4-26B MoE + large Hermes-style prompts on 2× R9700.

We cannot reliably run BF16 MoE 26B here (~52 GiB weights, ~30 GiB host RAM). Dense 12B BF16 is not a valid MoE stand-in for expert/KV pressure at SOUL-scale prompts.

Please test if you can load BF16/FP16 MoE on ROCm

  1. Short prompt — baseline OK
  2. Large system prompt (tens of k tokens) with num_blocks / max_model_len deliberately too small — expect fail-fast error, not multi-minute hang with unfinished>0 / no progress
  3. Same large prompt with adequate KV — completes or streams
  4. max_tokens=-1 or huge max_tokens — clamped / accepted sanely (see also feat(rocm): V1 sampling ops for Hermes EngineCore #234 for Hermes -1 mapping)

Report

GPU + ROCm, model/dtype, max_model_len/num_blocks, hang vs clean error, any EngineCore death.

Pairs with #234 (sampler) and #228 (FP8 MoE kernels; separate lane).

@localai-org-maint-bot localai-org-maint-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fail-fast behavior is important, but the scheduler and serving changes need regression coverage before merge. Please add tests proving an impossible full-sequence admission terminates with the intended error (rather than waiting forever), an adequately sized request still admits, and max_tokens=-1/oversized values follow the documented clamp. Please also bring the branch onto current main and clear the repository gates.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Addressing CHANGES_REQUESTED (localai-org-maint-bot)

Regression coverage added

Test Asserts
Scheduler.abort_unschedulable_waiting: aborts prompt that cannot fit KV Tiny num_blocks=4 + 200-tok prompt → aborted with kFinishedAborted, unfinished=0
… leaves admittable waiters alone Large pool → empty abort list; schedule() still admits
… no-op while running is non-empty Running non-empty → huge waiter not aborted yet
CompletionRequest max_tokens=-1 normalizes… → positive default (4096)
ChatCompletionRequest max_tokens=-1… same
CompletionRequest max_tokens=0… → positive before PostInit

Also fixed CompletionRequest path to normalize max_tokens<=0 the same way chat already did (Hermes/-1).

Lab run (HIP build):

test_scheduler --test-case='*abort_unschedulable*'  → 3 passed
test_openai_protocol --test-case='*max_tokens*'     → 3 passed

Docs / gates

USAGE already documents the fail-fast + clamp on this branch. Will rebase onto current main after #234 lands (merge order: #234#227#228).

bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 10, 2026
Address CHANGES_REQUESTED on mudler#227:

- Scheduler.abort_unschedulable_waiting: abort when prompt cannot fit
  KV (tiny num_blocks); leave admittable waiters; no-op while running
- CompletionRequest max_tokens=-1/0 normalize (parity with chat path)
- Protocol unit tests for Hermes-style max_tokens=-1

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 10, 2026
Address CHANGES_REQUESTED on mudler#227:

- Scheduler.abort_unschedulable_waiting: abort when prompt cannot fit
  KV (tiny num_blocks); leave admittable waiters; no-op while running
- CompletionRequest max_tokens=-1/0 normalize (parity with chat path)
- Protocol unit tests for Hermes-style max_tokens=-1

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
@bakon11
bakon11 force-pushed the fix/serve-kv-abort-max-tokens branch from 7857045 to 1d8061b Compare August 10, 2026 03:20
bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 10, 2026
Address CHANGES_REQUESTED on mudler#227:

- Scheduler.abort_unschedulable_waiting: abort when prompt cannot fit
  KV (tiny num_blocks); leave admittable waiters; no-op while running
- CompletionRequest max_tokens=-1/0 normalize (parity with chat path)
- Protocol unit tests for Hermes-style max_tokens=-1

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
@bakon11
bakon11 force-pushed the fix/serve-kv-abort-max-tokens branch from 1d8061b to 8cc62b9 Compare August 10, 2026 04:21
@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main — pr-size failed with base must be an ancestor of head. Local doc/agent gates green after rebase.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Fixed agent-record: documented VT_ENGINE_STEP_LOG in ENVIRONMENT.md (was read in core_proc step heartbeat).

@localai-bot

Copy link
Copy Markdown
Collaborator

Reviewed on CUDA. The deadlock fix is good and I want it — two findings before merge, one of which you also have in #234.

Verified (GB10 sm_121a, rebased on current main)

test_scheduler             433/433
test_openai_serving        534/534
test_qwen27_paged_engine   235/235   (SACRED)
test_qwen36_paged_engine   315/315   (SACRED)

The good part

abort_unschedulable_waiting() is correctly designed, and the guard is the reason:

if (!running.empty() || waiting == nullptr || waiting->empty()) return;

Only firing when nothing is running is exactly right — it's what makes "cannot ever be admitted" a sound conclusion rather than a guess, because no running request can later free blocks. Probing with allocate_slots and then freeing the trial, plus one reset_prefix_cache() attempt before giving up, is the careful order. And the diagnostic naming prompt_tokens / max_model_len / free_blocks / block_size is what someone hitting this at 3am actually needs. An infinite unfinished>0, model_executed=0 spin with the GPU idle is a genuinely bad failure mode and this converts it into a clear error.

Finding 1: the probe inflates prefix-cache stats

auto computed = kv_cache_manager->get_computed_blocks(*request);

get_computed_blocks carries a prefix_cache_stats.record side effect. The codebase already learned this and grew a pure variant specifically to avoid it — KVCacheManager::num_matched_prefix_tokens (kv_cache_manager.cpp:151), whose own comment says:

Pure read of the longest cached-prefix match — the same lookup get_computed_blocks performs, MINUS the prefix_cache_stats.record side effect (so LPM ordering never double-counts) and minus block allocation.

Your probe runs whenever running is empty with something waiting, i.e. potentially every step through a stall, so it double-counts queries/hits repeatedly and corrupts the very metric someone would look at while diagnosing this deadlock. Use num_matched_prefix_tokens() for the probe, and take the real get_computed_blocks only on the admission path in schedule().

Finding 2: max_tokens <= 0 should map to UNSET, not 16

sp.max_tokens = default_max_tokens.value_or(16);
if (!sp.max_tokens.has_value() || *sp.max_tokens <= 0) sp.max_tokens = 16;

Same issue I raised on #234, and more severe there because the floor is 16 rather than 4096. We already implement "unlimited" correctly one layer down, mirroring vLLM 1:1 (input_processor.cpp:175-180, mirroring input_processor.py:317-321):

if (!params.max_tokens.has_value()) {
  const int64_t seq_len = static_cast<int64_t>(prompt_token_ids.size());
  params.max_tokens = static_cast<int>(max_model_len_ - seq_len);
}

So -1 should become std::nullopt, which routes there. As written, a Hermes client asking for "unlimited" can get 16 tokens back with finish_reason=length, indistinguishable from a real limit.

Note #227 and #234 both patch this same spot and will conflict with each other. Worth fixing it in one of them and rebasing the other.

Fix those two and I'll re-run this set and merge — the scheduler work is the valuable half and I don't want it stuck behind the clamp.

localai-bot pushed a commit that referenced this pull request Aug 10, 2026
…stant

Three open PRs each grew their own workaround for the same defect, with three
different answers: #227 clamps to 16, #234 and #228 to 4096. All three would
conflict with each other, and all three truncate. This fixes it once, in the one
place that owns the translation.

Hermes and some OpenAI clients send `max_tokens: -1` for "no client-side limit".
`SamplingParams::PostInit` rejects anything below 1, which is what made a clamp
look necessary — but a constant is the wrong answer, because "unlimited" already
has a correct representation here: UNSET. The engine then generates to
`max_model_len - seq_len` (`v1/engine/input_processor.cpp:175-180`, a 1:1 mirror
of vllm `input_processor.py:317-321`), which is exactly what the client asked
for.

With a constant, a client asking for unlimited against a 32k model gets 4096 (or
16) tokens back with `finish_reason=length`, indistinguishable from a real limit
it set itself. It also hardcodes a policy number into the protocol layer, where
`VT_SERVER_MAX_NEW_TOKENS` is already the tunable.

Both request paths are covered: `/v1/completions` (`max_tokens`) and
`/v1/chat/completions` (`max_completion_tokens` preferred over `max_tokens`,
matching chat_completion/serving.py:299). A positive value is honoured unchanged,
and a serving-resolved default still wins over an unset request value.

RED before, GREEN after, same test and command, on dgx with the new test built
against UNPATCHED main:

  before: test case THREW exception: max_tokens must be at least 1, got -1
          29 cases | 28 passed | 1 failed;  175 assertions | 2 failed
  after:  29 cases | 29 passed | 0 failed;  179 assertions | 0 failed

`test_openai_protocol` gains six subcases pinning the semantics: -1 and 0 unset
on both paths, a positive value unchanged, and -1 yielding to a serving default
when one is supplied.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [ClaudeCode]
localai-bot pushed a commit that referenced this pull request Aug 10, 2026
…ans UNSET

Three open PRs each grew a workaround for the same defect with three different
answers: #227 clamps to 16, #234 and #228 to 4096. They would conflict with each
other and all three truncate. Fixed once, in the layer that owns the translation,
so those PRs can drop it and rebase.

Hermes and some OpenAI clients send max_tokens=-1 for "no client-side limit";
PostInit rejects <1, which is what made a clamp look necessary. But "unlimited"
already has a correct representation: UNSET, which routes to
max_model_len - seq_len (input_processor.cpp:175-180, mirroring vllm
input_processor.py:317-321). A constant silently truncates exactly the
long-context request that asked to be left unlimited.

RED before / GREEN after on dgx, the new test built against UNPATCHED main:
  before: THREW 'max_tokens must be at least 1, got -1'; 28/29 cases, 2 failed
  after:  29/29 cases, 179/179 assertions

Covers both request paths and documents the semantics in docs/USAGE.md.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [ClaudeCode]
@localai-bot

Copy link
Copy Markdown
Collaborator

Heads-up: the max_tokens clamp is now fixed on main as 2d2bdd04 (row/SERVE-MAXTOKENS-UNSET), so please drop it from this PR and rebase — otherwise the three PRs conflict over one spot.

A non-positive max_tokens (or max_completion_tokens) is now treated as unset on both request paths, which routes to max_model_len - seq_len in input_processor.cpp:175-180, mirroring vllm input_processor.py:317-321. No constant, no truncation of the long-context request that asked to be left unlimited.

RED before / GREEN after on dgx, the new test built against unpatched main:

before: THREW 'max_tokens must be at least 1, got -1';  28/29 cases, 2 assertions failed
after:  29/29 cases, 179/179 assertions

test_openai_protocol now pins the semantics (−1 and 0 unset on both paths, positive unchanged, and −1 yielding to a serving default). Semantics documented in docs/USAGE.md.

Everything else in your PR stands — this is only about the shared hunk.

@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebase + review fixes (addressing CHANGES_REQUESTED)

Rebuilt tip-of-main as $(git rev-parse --short HEAD).

Dropped

Finding 1 fixed

  • abort_unschedulable_waiting probe now uses num_matched_prefix_tokens() only (no get_computed_blocks side-effect on prefix_cache_stats). Real admission path in schedule() still uses get_computed_blocks.

Kept

  • Fail-fast abort when running empty and allocate_slots cannot admit under full-sequence reserve (+ one prefix-cache reset attempt)
  • Scheduler regression tests
  • VT_ENGINE_STEP_LOG documented (core_proc heartbeat)

Local gates: device-leakage, public-doc-tables, doc-checkpoint, env-doc, pr-size — OK.

@bakon11
bakon11 force-pushed the fix/serve-kv-abort-max-tokens branch 2 times, most recently from 0c43f73 to 3a94808 Compare August 10, 2026 14:02
@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main — tip 3a94808c. Title/body updated: no max_tokens clamp (main already unset). Local gates green. Please re-run CI (prior arm64 fail looked run-related; watching fresh checks).

@bakon11 bakon11 changed the title fix(server): fail-fast unschedulable KV waits + clamp max_tokens fix(server): fail-fast unschedulable KV waits Aug 10, 2026
@bakon11

bakon11 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Tip-of-main rebuild (clean fail-fast only)

Rebuilt as single commit on current main (3286f6fd).

Included

  • Scheduler::abort_unschedulable_waiting() + 3 regression tests
  • EngineCoreProc: call abort after post_step (spec drafts preserved — previous tip accidentally deferred/removed it)
  • Optional VT_ENGINE_STEP_LOG heartbeat + ENVIRONMENT/USAGE notes

Explicitly NOT included (would clobber main)

Local gates green. Please re-run CI.

@bakon11

bakon11 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

CI hygiene 2026-08-11: rebased onto origin/main (60e71a0). Local gates green (doc-checkpoint, env-doc, agent-record, pr-size, device-leakage). Land order remains #234#227#316#317.

@bakon11
bakon11 force-pushed the fix/serve-kv-abort-max-tokens branch 2 times, most recently from 02254bc to fbc0167 Compare August 11, 2026 00:31
@bakon11

bakon11 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto origin/main c70f42b9 (main moved +2 during CI). Local gates green. Land order unchanged: #234#227#316#317. PR bodies refreshed (RandomSample note on #234; #228 closed in land path).

Abort waiting requests that cannot fit in the configured KV pool while
the engine is idle, instead of spinning forever at model_executed=0.
Rebased onto current main; docs only add the surface (no release-archive
version reverts).

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
@bakon11
bakon11 force-pushed the fix/serve-kv-abort-max-tokens branch from fbc0167 to 32b771d Compare August 11, 2026 01:25
bakon11 pushed a commit to bakon11/vllm.cpp that referenced this pull request Aug 11, 2026
- serving_chat: sse_ping reason=queue_wait|engine_wait|decode_stall|empty_output
  (drop misleading prefill_wait=1)
- input_processor: reject prompt_tokens > serve max_model_len; clamp max_tokens
  to remaining window; wire set_max_model_len from model_loader
- cherry-pick mudler#227 abort_unschedulable_waiting so idle WAITING cannot spin forever

Lab park on :8010; product path for Hermes wedge class.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Hermes:grok-4.5 [Hermes]
@bakon11

bakon11 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@localai-org-maint-bot Status check against CHANGES_REQUESTED / localai-bot notes:

  1. Prefix probeabort_unschedulable_waiting uses num_matched_prefix_tokens() (no get_computed_blocks side effect on the stall probe). Real admission still uses get_computed_blocks in schedule().
  2. max_tokens floor — dropped from this PR; main already has non-positive → unset (2d2bdd04 / SERVE-MAXTOKENS-UNSET).
  3. Regression tests in tests/vllm/v1/test_scheduler.cpp:
    • aborts prompt that cannot fit KV
    • leaves admittable waiters alone
    • no-op while running is non-empty

Sanitize ASan/TSan reds match the ambient main baseline on every open PR — not unique to this tip. Happy to rebase again if main moves.

localai-bot pushed a commit that referenced this pull request Aug 11, 2026
…and at admission (#227)

External PR #227 reported a real defect: a prompt larger than the KV pool
(`--num-blocks x --block-size`, 256x32=8192 by default) is admitted, never
allocates, and the engine spins forever at `model_executed=0` with an idle GPU.

That PR's remedy was to abort the head waiter from the scheduler. vLLM does not
do that -- `scheduler.py:919-940` peeks and `break`s, and never aborts an
unschedulable waiter. Probing the live allocator to decide also POISONS the
prefix cache: the probe reaches `block_pool.cache_full_blocks(...)`, publishes
block hashes for KV no forward pass ever wrote, and `free_blocks` deliberately
keeps those hashes matchable -- measured, one probe turned the next 64-token
prefill into a 16-token one that "matched" 48 tokens of uninitialized KV.

So this lands the two guards vLLM actually has, neither of which existed here.

STARTUP (`kv_cache_utils.py:751-788` `_check_enough_kv_cache_memory`, with
`:791-798` `max_memory_usage_bytes`, `:800-851` `estimate_max_model_len` and
`:1967-2027` `_auto_fit_max_model_len`, all @ 555967922; applied at
`LoadedEngine::ResolveMaxModelLen`). A pinned `--max-model-len` the pool cannot
hold is REFUSED with upstream's message, its numbers, and the flags that close
the gap. An UNPINNED one is auto-fitted down to the pool and logged, which is
what keeps the default path alive -- our `ResolveNumBlocks` still has no memory
profiler (TODO ROAD-V1-MEM M3), so the fallback 256 blocks is a placeholder, not
a measurement, and refusing against it would refuse every large-context model.
Deviations recorded in the header: upstream's two `Callable`s are passed as
values (both are pure arithmetic here), and `estimate_max_model_len`'s binary
search is written closed-form because `KVBytesPerBlock` does not vary with the
block count. `if kv_cache_spec:` (`:872-878`) is mirrored as `bytes_per_block >
0`, so an attention-free or pure-GDN model is never refused.

ADMISSION (`input_processor.py:387-432` `_validate_prompt_len`, decoder arm). An
empty prompt, and one at or past the RESOLVED `max_model_len`, raise the new
`InputValidationError`. `api_server.cpp` catches it ahead of its generic
`std::exception` arm and answers **HTTP 400 `BadRequestError`**, mirroring
`serve/utils/error_response.py:62-65` -- a client mistake, not a 500 and not a
finish reason. `InputProcessor` now takes the resolved serving length rather
than the raw `max_position_embeddings`, which is what upstream's
`model_config.max_model_len` is; without that the two guards would disagree.

Together they make the post-condition the scheduler relies on true: anything
admitted fits in KV, so the wedge PR #227 hit cannot form.

Also kept from PR #227, repaired: the two-phase prefill-progress logging (`begin`
at schedule, `running`/`done` after `execute_model`, so `elapsed_s` is real wall
time instead of the cost of scheduling), and the `VT_ENGINE_STEP_LOG` engine
heartbeat that tells an idle engine from a stalled one. The progress state map
leaked one entry per request for the process lifetime; it is bounded again, now
also evicting requests the scheduler has forgotten, which the old `logged_done`
rule alone never did for an aborted prefill.

NOT taken from PR #227: `abort_unschedulable_waiting` and its three tests (the
prefix-cache poisoning above is inherent to probing the live allocator, and the
tests passed with the guard deleted); the `reset_prefix_cache()` call from the
scheduler (upstream only does that from the explicit RPC); and the prose
paragraph inserted INTO the middle of the `docs/USAGE.md` flag table, which split
it so every row below lost its header. That PR also never compiled anywhere --
it rewrote the `EngineCoreProc` ctor to 5 parameters against a 6-parameter
declaration -- and reverted `post_step`, the `check_for_draft_tokens` threading
and the `VT_SPEC_TRACE` instrumentation, the omission that made MTP/DFlash/
ngram/DSpark inert on the CLI and server. This branch is based on `origin/main`,
so none of that is carried.

`scripts/dgx-online-serving.sh` pinned `--max-model-len 262144` against a
4736x32 = 151552-token pool, four times what it could serve; the flag is dropped
so the length auto-fits to the pool, which is what that server actually served.

Issue: #83 (M4, refuse before allocating). Closes the report in #227.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5-1m [ClaudeCode]
localai-bot pushed a commit that referenced this pull request Aug 11, 2026
Re-merge before re-gating, per the landing rules. Conflict-free.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5-1m [ClaudeCode]
localai-bot pushed a commit that referenced this pull request Aug 11, 2026
#227)

Closes the gap @bakon11 found in #227, but NOT with the mechanism that PR
proposed. The symptom was real -- a request whose KV need can never be
satisfied hangs the server -- and the diagnosis was correct. The fix was at
the wrong layer, and three findings made it unlandable as filed:

  1. It had never compiled anywhere: core_proc.h:185 declares a 6-param ctor,
     the PR rewrote core_proc.cpp:20 to 5 and never touched the header.
     Reproduced on the PR branch in isolation, so merging could not fix it.
  2. It silently REVERTED landed work -- a paste from a pre-SPEC-DSPARK
     checkout that deleted post_step(model_executed), the
     check_for_draft_tokens threading, and the VT_SPEC_TRACE instrumentation.
     That exact omission previously made MTP/DFlash/ngram/DSpark ALL inert on
     the CLI and server path, with green tests.
  3. Its abort_unschedulable_waiting probe POISONED THE PREFIX CACHE, measured
     rather than argued: the probe calls the real allocate_slots, which
     publishes block hashes into cached_block_hash_to_block, and free_blocks
     deliberately KEEPS those hashes matchable -- but no forward pass ever
     ran. Control prefills 64 prompt tokens; after ONE probe the next
     schedule() prefills 16, matching 48 tokens of uninitialized KV. Restoring
     the free count is therefore not a sufficient repair; the probe must not
     touch the live allocator at all.

Its own three tests were vacuous: deleting the !running.empty() guard they
exist to protect left all three green, because the 500-token waiter fits in
the 1000-block fixture and the probe succeeds regardless.

vLLM does not abort unschedulable waiters. scheduler.py:919-940 peeks and
breaks. It prevents the state at two OTHER layers, and NEITHER existed here:

  - startup, kv_cache_utils.py:751-788 _check_enough_kv_cache_memory (plus
    :791-798, :800-851, :1967-2027 auto-fit, :2160-2174 call site), now four
    free functions in v1/core/kv_cache_utils.{h,cpp} applied at
    LoadedEngine::ResolveMaxModelLen;
  - admission, input_processor.py:387-432 _validate_prompt_len, called from
    all three process_inputs* entry points and surfaced as HTTP 400 via the
    BadRequestError mapping at serve/utils/error_response.py:62-65 -- not as
    a finish reason on a 200 with an empty body, which is what #227 emitted.

RED-then-GREEN by mutation on the final tree (guards reverted, tests
untouched, files restored byte-identical after): RED showed
prompt_tokens=40 admitted against max_model_len=32 -- the defect itself --
plus the 400 assertion reading 200. GREEN: test_kv_cache_utils 35/35,
test_input_processor 16/16, test_loaded_engine_dense 15/15,
test_openai_api_server 51/51. Full CPU ctest 369/369 at load 28.

One judgement call recorded rather than buried: a bare mirror of the startup
throw would refuse to start most large-context models, because
ResolveNumBlocks still returns a hardcoded 256 (the gpu_memory_utilization
profile is unported). Upstream's own auto-fit was therefore ported too, so
the throw fires only when the user PINNED a --max-model-len the pool cannot
hold. That required dropping --max-model-len 262144 from
scripts/dgx-online-serving.sh, which asked for 4x its own 151552-token pool;
that edit is bash -n clean but UNEXECUTED -- no GPU here.

Also not carried from #227: the scheduler-side reset_prefix_cache() on stall
(upstream only does this from the explicit RPC), the unbounded PrefillStates
map that dropped its size>64 eviction, and the prose paragraph inserted into
the middle of the docs/USAGE.md flag table, which split every row below it.

Owed and stated plainly: no committed spec (this is a PR repair with no
roadmap row; both guards are recorded in porting-inventory and linked to #83
and #227), no fresh-reviewer pass, and no GPU verification -- the startup
guard changes resolved max_model_len on the default path for large-context
models, which is a real behavior change on gates that need dgx.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [ClaudeCode]
@localai-bot

Copy link
Copy Markdown
Collaborator

The defect you reported is fixed on main as afc754e6a191 — but with a different remedy than this PR's, so it is worth saying exactly what happened rather than just closing.

You were right about the bug: a prompt larger than the KV pool is admitted, never allocates, and the engine spins at model_executed=0 with an idle GPU and a client hanging on nothing. That is real and it is now closed.

What was not taken, and why. abort_unschedulable_waiting decided admissibility by calling allocate_slots and freeing the trial allocation. That is not side-effect-free. With caching enabled and delay_cache_blocks=false, allocate_slots commits the trial blocks to the prefix-cache index via cache_blocks(...), and block_pool.cpp's free_blocks deliberately keeps hashed blocks in cached_block_hash_to_block so they stay matchable. The trial therefore publishes block hashes for KV that no forward pass ever wrote, and the next request with that prefix hits them.

That is measured, not theoretical — two of us found it independently. One probe of a 32-token waiter made an identical later request match 16 tokens of never-computed KV; the other measurement turned a 64-token prefill into a 16-token one "matching" 48 tokens of uninitialized KV. Same mechanism.

Two smaller things in the same function: kWaitingForRemoteKvs requests already own blocks an in-flight transfer is writing into, and free(*request) frees all of a request's blocks, not the trial's; and the num_new_tokens <= 0 branch aborted such a request "defensively", which is the opposite of correct for one whose tokens are all already computed.

What landed instead are the two guards vLLM actually has, neither of which existed here — _check_enough_kv_cache_memory at startup (a pinned --max-model-len the pool cannot hold is refused with upstream's message and numbers; an unpinned one is auto-fitted down and logged) and _validate_prompt_len at admission (HTTP 400, a client mistake rather than a 500 or a finish reason). Together they make the post-condition true that the scheduler already assumed: anything admitted fits, so the wedge cannot form. Note that upstream's scheduler deliberately peeks and breaks and never aborts an unschedulable waiter.

Kept from this PR, repaired: the two-phase prefill-progress logging (begin at schedule, running/done after execute_model, so elapsed_s is wall time rather than the cost of scheduling) and the VT_ENGINE_STEP_LOG heartbeat that distinguishes an idle engine from a stalled one. Its progress map leaked one entry per request for the process lifetime and is bounded now.

Closing as fixed-otherwise. The report was good and the diagnosis was right; only the remedy changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants