Skip to content

[Bugfix][KV Offloading] Handle queued request aborts without allocated KV blocks - #49146

Merged
chaunceyjiang merged 2 commits into
vllm-project:mainfrom
chaunceyjiang:kv_offload_abort_req
Jul 21, 2026
Merged

[Bugfix][KV Offloading] Handle queued request aborts without allocated KV blocks#49146
chaunceyjiang merged 2 commits into
vllm-project:mainfrom
chaunceyjiang:kv_offload_abort_req

Conversation

@chaunceyjiang

@chaunceyjiang chaunceyjiang commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Fix #49118

Purpose

Handle queued request aborts without allocated KV blocks

Test Plan

CUDA_VISIBLE_DEVICES=4,5,6,7 \
  vllm serve /mnt/data3/models/Qwen/Qwen3.6-35B-A3B \
    --served-model-name Qwen3.6-35B-A3B \
    --host 0.0.0.0 \
    --port 8840 \
    --tensor-parallel-size 4 \
    --language-model-only \
    --dtype bfloat16 \
    --max-model-len 180000 \
    --max-num-seqs 4 \
      --max-num-batched-tokens 4224 \
    --gpu-memory-utilization 0.90 \
    --kv-cache-dtype fp8_e4m3 \
    --enable-prefix-caching \
    --enable-chunked-prefill \
    --reasoning-parser qwen3 \
    --kv-transfer-config '{
      "kv_connector": "OffloadingConnector",
      "kv_role": "kv_both",
      "kv_connector_extra_config": {
        "cpu_bytes_to_use": 8589934592
      }
    }'


import json
import socket
import sys
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor

BASE_URL = "http://127.0.0.1:8840"
MODEL = "Qwen3.6-35B-A3B"

PROMPT_CHARS = 220_000
REQUEST_TIMEOUT = 900


def make_prompt(seed: str, chars: int = PROMPT_CHARS) -> str:
    fragment = f"[{seed}] filler text for a long queued request. "
    return (fragment * (chars // len(fragment) + 1))[:chars]


def chat(prompt: str, timeout: float = REQUEST_TIMEOUT) -> dict:
    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": 16,
        "temperature": 0,
    }
    request = urllib.request.Request(
        f"{BASE_URL}/v1/chat/completions",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=timeout) as response:
        return json.load(response)


def abort_queued_request(prompt: str) -> str:
    try:
        chat(prompt, timeout=1.0)
    except (
        urllib.error.URLError,
        socket.timeout,
        TimeoutError,
        ConnectionError,
    ) as exc:
        return f"aborted ({type(exc).__name__})"
    return "unexpectedly completed"


def check_health() -> None:
    with urllib.request.urlopen(f"{BASE_URL}/health", timeout=5) as response:
        if response.status != 200:
            raise RuntimeError(f"health check returned {response.status}")


def main() -> int:
    check_health()
    print("Server is healthy; starting queued-abort reproduction.")
    busy_errors = []
    # max-num-seqs=4:

    with ThreadPoolExecutor(max_workers=8) as executor:
        busy = [
            executor.submit(chat, make_prompt(f"busy-{index}")) for index in range(5)
        ]
        time.sleep(0.5)
        aborted = [
            executor.submit(
                abort_queued_request,
                make_prompt(f"abort-{index}"),
            )
            for index in range(3)
        ]
        for index, future in enumerate(aborted):
            try:
                print(f"abort-{index}: {future.result()}")
            except Exception as exc:
                print(f"abort-{index}: client error: {exc!r}")

        for index, future in enumerate(busy):
            try:
                future.result()
                print(f"busy-{index}: completed")
            except Exception as exc:
                busy_errors.append(exc)
                print(f"busy-{index}: failed: {exc!r}")
    time.sleep(1)
    try:
        check_health()
    except Exception as exc:
        print()
        print("REPRODUCED: EngineCore is no longer healthy.")
        print(f"Health error: {exc!r}")
        return 1
    if busy_errors:
        print()
        print("Server is healthy, but one or more in-flight requests failed.")
        return 2
    final = chat("Reply with only the digit 4: what is 2+2?", timeout=60)
    content = final["choices"][0]["message"]["content"]
    print()
    print(f"PASS: EngineCore survived. Final response: {content!r}")
    return 0


if __name__ == "__main__":
    sys.exit(main())


Test Result

before:

(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332] EngineCore encountered a fatal error.
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332] Traceback (most recent call last):
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/v1/engine/core.py", line 1323, in run_engine_core
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     engine_core.run_busy_loop()
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/v1/engine/core.py", line 1364, in run_busy_loop
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     self._process_engine_step()
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/v1/engine/core.py", line 1403, in _process_engine_step
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     outputs, model_executed = self.step_fn()
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]                               ^^^^^^^^^^^^^^
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/v1/engine/core.py", line 645, in step_with_batch_queue
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     scheduler_output = self.scheduler.schedule(self._should_throttle_prefills())
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/v1/core/sched/scheduler.py", line 1170, in schedule
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     meta = self._build_kv_connector_meta(self.connector, scheduler_output)
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/v1/core/sched/scheduler.py", line 1192, in _build_kv_connector_meta
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     return connector.build_connector_meta(scheduler_output)
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py", line 157, in build_connector_meta
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     return self.connector_scheduler.build_connector_meta(scheduler_output)
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py", line 1157, in build_connector_meta
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     store_jobs=self._build_store_jobs(scheduler_output),
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]   File "/mnt/data4/jxy/vllm/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py", line 975, in _build_store_jobs
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]     assert len(offload_keys) == len(offload_block_ids)
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332]            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(EngineCore pid=909178) ERROR 07-20 11:15:01 [core.py:1332] AssertionError
(Worker_TP0 pid=911359) INFO 07-20 11:15:01 [multiproc_executor.py:793] Parent process exited, terminating worker queues
(EngineCore pid=909178) INFO 07-20 11:15:01 [multiproc_executor.py:429] [shutdown] Executor: waiting for worker exit count=4
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704] AsyncLLM output_handler failed.
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704] Traceback (most recent call last):
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704]   File "/mnt/data4/jxy/vllm/vllm/v1/engine/async_llm.py", line 660, in output_handler
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704]     outputs = await engine_core.get_output_async()
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704]               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704]   File "/mnt/data4/jxy/vllm/vllm/v1/engine/core_client.py", line 1061, in get_output_async
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704]     raise self._format_exception(outputs) from None
(APIServer pid=905233) ERROR 07-20 11:15:01 [async_llm.py:704] vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.
(APIServer pid=905233) INFO:     127.0.0.1:60860 - "POST /v1/chat/completions HTTP/1.1" 500 Internal Server Error
(APIServer pid=905233) INFO:     127.0.0.1:60870 - "POST /v1/chat/completions HTTP/1.1" 500 Internal Server Error
(APIServer pid=905233) INFO:     127.0.0.1:60884 - "POST /v1/chat/completions HTTP/1.1" 500 Internal Server Error
(APIServer pid=905233) INFO:     127.0.0.1:60890 - "POST /v1/chat/completions HTTP/1.1" 500 Internal Server Error
(APIServer pid=905233) INFO:     127.0.0.1:60892 - "POST /v1/chat/completions HTTP/1.1" 500 Internal Server Error
(APIServer pid=905233) INFO:     Shutting down
(APIServer pid=905233) INFO:     Waiting for application shutdown.
(APIServer pid=905233) INFO:     Application shutdown complete.

after:

(APIServer pid=1344642) INFO:     127.0.0.1:48516 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO 07-20 11:42:12 [loggers.py:310] Engine 000: Avg prompt throughput: 11919.6 tokens/s, Avg generation throughput: 1.8 tokens/s, Running: 2 reqs, Waiting: 2 reqs, GPU KV cache usage: 0.6%, Prefix cache hit rate: 0.0%, External prefix cache hit rate:0.0%
(APIServer pid=1344642) INFO 07-20 11:42:12 [metrics.py:103] KV Transfer metrics: vllm:kv_offload_lookup_sync_delay_seconds_count=22, vllm:kv_offload_lookup_sync_delay_seconds_sum=0.00020857341587543488, vllm:kv_offload_cpu_cache_usage_perc=0.030226700251889168, vllm:kv_offload_cpu_allocation_size_count=35, vllm:kv_offload_cpu_allocation_size_sum=226, vllm:kv_offload_cpu_cache_write_usage_perc=0.030226700251889168, vllm:kv_offload_cpu_cache_read_usage_perc=0.0, vllm:kv_offload_store_bytes=4611932160, vllm:kv_offload_store_time=0.09642745602130891, vllm:kv_offload_store_size_count=132, vllm:kv_offload_store_size_sum=4611932160
(APIServer pid=1344642) INFO:     127.0.0.1:48530 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:48538 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:48554 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:48566 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:59012 - "GET /health HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:59016 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO 07-20 11:42:22 [loggers.py:310] Engine 000: Avg prompt throughput: 17879.9 tokens/s, Avg generation throughput: 7.8 tokens/s, Running: 0 reqs, Waiting: 0 reqs, GPU KV cache usage: 0.0%, Prefix cache hit rate: 0.0%, External prefix cache hit rate:0.0%
(APIServer pid=1344642) INFO 07-20 11:42:22 [metrics.py:103] KV Transfer metrics: vllm:kv_offload_lookup_sync_delay_seconds_count=32, vllm:kv_offload_lookup_sync_delay_seconds_sum=0.00022306758910417557, vllm:kv_offload_store_bytes=6981058560, vllm:kv_offload_store_time=0.14641644805669782, vllm:kv_offload_store_size_count=212, vllm:kv_offload_store_size_sum=6981058560, vllm:kv_offload_cpu_cache_usage_perc=0.0, vllm:kv_offload_cpu_allocation_size_count=51, vllm:kv_offload_cpu_allocation_size_sum=312, vllm:kv_offload_cpu_cache_write_usage_perc=0.0, vllm:kv_offload_cpu_cache_read_usage_perc=0.0
(APIServer pid=1344642) INFO 07-20 11:42:32 [loggers.py:310] Engine 000: Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 0 reqs, Waiting: 0 reqs, GPU KV cache usage: 0.0%, Prefix cache hit rate: 0.0%, External prefix cache hit rate: 0.0%
(APIServer pid=1344642) INFO:     127.0.0.1:47952 - "GET /health HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:47962 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:47972 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:47978 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:47984 - "POST /v1/chat/completions HTTP/1.1" 200 OK
(APIServer pid=1344642) INFO:     127.0.0.1:48000 - "POST /v1/chat/completions HTTP/1.1" 200 OK

Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added v1 bug Something isn't working kv-connector labels Jul 20, 2026
@chaunceyjiang

Copy link
Copy Markdown
Collaborator Author

/cc @orozery PTAL.

@mergify

mergify Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @chaunceyjiang.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 20, 2026

@orozery orozery left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @chaunceyjiang !
Could you rebase and address my comment?
I can also push, just let me know.

@@ -327,6 +327,21 @@ def storable_chunks(
num_chunks = max(0, num_chunks - 1)
return num_chunks

def storable_allocated_chunks(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we please merge the logic here into the existing storable_chunks function?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done.

…d KV blocks

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
…d KV blocks

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
@chaunceyjiang
chaunceyjiang force-pushed the kv_offload_abort_req branch from 1d24e2e to 95aabf6 Compare July 20, 2026 14:58
@orozery orozery added the ready ONLY add when PR is ready to merge/full CI is needed label Jul 20, 2026
@mergify mergify Bot removed the needs-rebase label Jul 20, 2026

@orozery orozery left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @chaunceyjiang !

@chaunceyjiang
chaunceyjiang merged commit 94ed0bf into vllm-project:main Jul 21, 2026
94 checks passed
Alex-ai-future added a commit to Alex-ai-future/vllm that referenced this pull request Jul 21, 2026
…equest_finished

Root-cause fix for issue vllm-project#49118, complementary to PR vllm-project#49146's clamp.

Changes:
1. preempt path: sync clear offload_keys with block_ids
   - Maintains invariant: len(offload_keys) * blocks_per_chunk <= len(block_ids)
   - Prevents offload_keys residue after preempt

2. request_finished: check block_ids before calling update_offload_keys
   - Only populate offload_keys if request was actually scheduled
   - Prevents offload_keys without corresponding block_ids for queued aborts

3. test: update test_abort_queued_request_does_not_build_store_job
   - Assert offload_keys is empty for never-scheduled request
   - Reflects the source fix in request_finished

Why this matters:
- preempt sync clear: defensive fix for invariant consistency
- request_finished check: source fix for queued abort (no longer need clamp)
- mid-prefill abort: still benefits from PR vllm-project#49146's clamp (prefix cache reuse)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alex <jihuihuang@example.com>
Signed-off-by: Alex <jihui.huang@daocloud.io>
Signed-off-by: Alex <alex.tech.lab@outlook.com>
Signed-off-by: Alex <jihui.huang@daocloud.io>
ArjunPakhan pushed a commit to ArjunPakhan/vllm that referenced this pull request Jul 21, 2026
…d KV blocks (vllm-project#49146)

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working kv-connector ready ONLY add when PR is ready to merge/full CI is needed v1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: OffloadingConnector — aborting a queued (never-scheduled) request kills the engine with AssertionError in _build_store_jobs

2 participants