Skip to content

feat: add endpoint to clear all kv blocks in vllm v1 - #1384

Merged
jain-ria merged 10 commits into
ai-dynamo:mainfrom
jain-ria:clear-blocks-example
Jun 12, 2025
Merged

jain-ria merged 10 commits into
ai-dynamo:mainfrom
jain-ria:clear-blocks-example

Conversation

@jain-ria

@jain-ria jain-ria commented Jun 4, 2025

Copy link
Copy Markdown
Contributor

Overview:

Adds an endpoint to allow users to directly access vLLM v1's clear all kv blocks feature.

Details:

Added a new axum router, handler function, and dynamo endpoint for clearing all kv blocks.
Example: curl -X POST http://localhost:8000/clear_kv_blocks

Where should the reviewer start?

Router and handler function:

  • /lib/llm/src/http/service/clear_kv_blocks.rs

Dynamo endpoint:

  • /launch/dynamo-run/src/subprocess/vllm_v1_inc.py

Corresponding updates in:

  • /lib/llm/src/http/service/service_v2.rs
  • /lib/llm/src/http/service/service.rs

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Introduced a new endpoint to clear key-value (KV) cache blocks across distributed worker groups.
    • Added a REST API route for clearing KV blocks, providing detailed status and results for each worker group.
  • Improvements
    • Enhanced error handling and logging for the new endpoint to aid in debugging and monitoring.
    • Updated service configuration to support integration with distributed runtime environments.
    • Enabled concurrent handling of multiple endpoints for improved server responsiveness.

@github-actions

github-actions Bot commented Jun 4, 2025

Copy link
Copy Markdown
Contributor

👋 Hi jain-ria! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added the external-contribution Pull request is from an external contributor label Jun 4, 2025
@jain-ria jain-ria changed the title Add clear_all_blocks endpoint feat: add clear_all_blocks endpoint Jun 4, 2025
@github-actions github-actions Bot added the feat label Jun 4, 2025
@jain-ria jain-ria changed the title feat: add clear_all_blocks endpoint feat: add clear_kv_blocks endpoint Jun 4, 2025

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

♻️ Duplicate comments (1)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)

126-132: ⚠️ Potential issue

reset_prefix_cache() must be off-loaded to avoid blocking the event loop

self.engine_client.reset_prefix_cache() is a synchronous call that may involve GPU / NCCL work.
Invoking it directly in an async def coroutine risks freezing the entire event-loop under heavy load.

-        try:
-            self.engine_client.reset_prefix_cache()
+        try:
+            loop = asyncio.get_running_loop()
+            await loop.run_in_executor(None, self.engine_client.reset_prefix_cache)

The identical concern was raised in a previous review but has not yet been addressed.

🧹 Nitpick comments (1)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)

268-275: Graceful handling of endpoint-serving failures

asyncio.gather() cancels the remaining tasks as soon as the first one raises.
A transient error in either serve_endpoint would therefore bring down the whole worker.

Consider running the coroutines shielded and restarting the faulty one, or use return_exceptions=True and decide per-endpoint how to proceed.

This strengthens availability without changing happy-path behaviour.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44fafcc and 148fa5a.

📒 Files selected for processing (4)
  • launch/dynamo-run/src/input/http.rs (1 hunks)
  • launch/dynamo-run/src/subprocess/vllm_v1_inc.py (3 hunks)
  • lib/llm/src/http/service/clear_kv_blocks.rs (1 hunks)
  • lib/llm/src/http/service/service_v2.rs (5 hunks)
✅ Files skipped from review due to trivial changes (1)
  • lib/llm/src/http/service/clear_kv_blocks.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • launch/dynamo-run/src/input/http.rs
  • lib/llm/src/http/service/service_v2.rs
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Mirror Repository to GitLab
  • GitHub Check: pre-merge-rust (lib/runtime/examples)
  • GitHub Check: pre-merge-rust (.)
  • GitHub Check: pre-merge-rust (lib/bindings/python)
  • GitHub Check: Build and Test - vllm
🔇 Additional comments (1)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)

188-190: Consider explicit registration for the clear_kv_blocks endpoint

Only generate_endpoint is passed to register_llm.
If any LLM-registry, routing or discovery logic expects all callable endpoints to be registered, the reset route will be invisible.

Please double-check whether register_llm must also receive clear_endpoint; if not, add a clarifying comment explaining why.

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

♻️ Duplicate comments (3)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)

126-132: 🛠️ Refactor suggestion

Avoid blocking the event-loop when clearing KV blocks

self.engine_client.reset_prefix_cache() is still invoked synchronously.
If that call performs GPU/NCCL synchronisation it will stall the entire asyncio loop and every request handler.

-            self.engine_client.reset_prefix_cache()
+            # Off-load to a worker thread so we don’t block the event-loop
+            loop = asyncio.get_running_loop()
+            await loop.run_in_executor(None, self.engine_client.reset_prefix_cache)
lib/llm/src/http/service/clear_kv_blocks.rs (2)

190-195: ⚠️ Potential issue

Endpoint comparison will never match – use ends_with

instance.endpoint contains the fully-qualified endpoint (e.g. dyn://ns.comp.clear_kv_blocks).
Comparing with bare "clear_kv_blocks" always fails, so no instance will ever be cleared.

-            .filter(|instance| instance.endpoint == "clear_kv_blocks")
+            .filter(|instance| instance.endpoint.ends_with("clear_kv_blocks"))

217-239: 🛠️ Refactor suggestion

Empty payload & lack of result validation

  1. router.round_robin(().into()) sends null – schema can’t evolve.
    Send at least {}.

  2. You ignore the JSON body and mark success unconditionally when a frame arrives.
    Parse the first item and ensure status == "success".

-            match router.round_robin(().into()).await {
+            match router.round_robin(json!({}).into()).await {
...
-                    Some(response) => {
+                    Some(response) if response.get("status") == Some(&json!("success")) => {
...
+                    Some(err_resp) => {
+                        add_worker_result(
+                            false,
+                            instance_name,
+                            "Instance reported error",
+                            namespace,
+                            component,
+                            Some(err_resp.to_string()),
+                        );
+                    }
🧹 Nitpick comments (2)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)

268-275: Unhandled cancellation / error propagation in asyncio.gather

If one of the served endpoints raises, asyncio.gather cancels the remaining task and re-raises.
Consider:

await asyncio.gather(
    generate_endpoint.serve_endpoint(handler.generate),
    clear_endpoint.serve_endpoint(handler.clear_kv_blocks),
    return_exceptions=False,   # explicit & self-documenting
)

and add graceful shutdown / cancellation handling so the worker exits cleanly instead of leaving orphaned tasks.

lib/llm/src/http/service/clear_kv_blocks.rs (1)

44-48: Return appropriate HTTP status code for missing workers

The handler currently returns HTTP 200 with a JSON error message.
Consider using StatusCode::NOT_FOUND (404) to signal that no worker groups are active.

return (StatusCode::NOT_FOUND, Json(json!({ "message": ... })))
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 148fa5a and e14366f.

📒 Files selected for processing (4)
  • launch/dynamo-run/src/input/http.rs (1 hunks)
  • launch/dynamo-run/src/subprocess/vllm_v1_inc.py (3 hunks)
  • lib/llm/src/http/service/clear_kv_blocks.rs (1 hunks)
  • lib/llm/src/http/service/service_v2.rs (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • launch/dynamo-run/src/input/http.rs
  • lib/llm/src/http/service/service_v2.rs
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Mirror Repository to GitLab
  • GitHub Check: pre-merge-rust (.)
  • GitHub Check: pre-merge-rust (lib/runtime/examples)
  • GitHub Check: pre-merge-rust (lib/bindings/python)
  • GitHub Check: Build and Test - vllm

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

♻️ Duplicate comments (2)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)

126-131: ⚠️ Potential issue

Still synchronous ‑ this blocks the event loop
reset_prefix_cache() is executed directly inside the async handler.
If the underlying vLLM call performs GPU / NCCL sync the whole event-loop stalls.

The exact same issue was raised previously – please off-load the call via
asyncio.to_thread() or loop.run_in_executor().

-            self.engine_client.reset_prefix_cache()
+            loop = asyncio.get_running_loop()
+            await loop.run_in_executor(None, self.engine_client.reset_prefix_cache)
lib/llm/src/http/service/clear_kv_blocks.rs (1)

190-194: ⚠️ Potential issue

Endpoint comparison will never match – use ends_with
instance.endpoint holds the fully-qualified name
(e.g. dyn://ns.comp.clear_kv_blocks).
Comparing it to the bare string "clear_kv_blocks" always fails, so every
worker will be reported as unsupported.

-            .filter(|instance| instance.endpoint == "clear_kv_blocks")
+            .filter(|instance| instance
+                .endpoint
+                .ends_with("clear_kv_blocks"))
🧹 Nitpick comments (1)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)

268-275: asyncio.gather error-propagation can swallow exceptions
When either serve_endpoint coroutine exits unexpectedly the other keeps
running and the first exception wins, potentially masking later failures.
Consider asyncio.TaskGroup (3.11+) or gather(..., return_exceptions=True)
and explicit cancellation handling to surface all failures.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e14366f and 15135dc.

📒 Files selected for processing (4)
  • launch/dynamo-run/src/input/http.rs (1 hunks)
  • launch/dynamo-run/src/subprocess/vllm_v1_inc.py (3 hunks)
  • lib/llm/src/http/service/clear_kv_blocks.rs (1 hunks)
  • lib/llm/src/http/service/service_v2.rs (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • launch/dynamo-run/src/input/http.rs
  • lib/llm/src/http/service/service_v2.rs
🧰 Additional context used
🧬 Code Graph Analysis (1)
launch/dynamo-run/src/subprocess/vllm_v1_inc.py (1)
launch/dynamo-run/src/subprocess/vllm_inc.py (2)
  • RequestHandler (53-130)
  • generate (90-130)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Mirror Repository to GitLab
  • GitHub Check: Build and Test - vllm
  • GitHub Check: pre-merge-rust (.)
  • GitHub Check: pre-merge-rust (lib/runtime/examples)
  • GitHub Check: pre-merge-rust (lib/bindings/python)

Comment thread lib/llm/src/http/service/clear_kv_blocks.rs
Comment thread lib/llm/src/http/service/clear_kv_blocks.rs
@jain-ria

Copy link
Copy Markdown
Contributor Author

The AIQ team asked for a way to directly clear all blocks for all workers for testing, which is why I added this through http for now

@alec-flowers

Copy link
Copy Markdown
Contributor

qq, what happens if requests are in flight and this is triggered? Is everything ok?

Is the guidance when using this to stop all requests, call this function, and then can send requests again?

@jain-ria

jain-ria commented Jun 11, 2025

Copy link
Copy Markdown
Contributor Author

Based on some quick tests of a bunch of interleaved generate & clear requests, the generate requests seem to finish normally after the kv blocks are cleared


for instance in &instances_filtered {
let instance_name = format!("{}-instance-{}", entry.name, instance.id());
match router.round_robin(().into()).await {

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.

Why round_robin if we already identified and stored all the instance IDs? Shouldn't we use direct on our explicit list?

message: Option<String>| {
let mut result = json!({
"name": name,
"endpoint": format!("{}/{}/clear_kv_blocks", ns, comp),

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.

"clear_kv_blocks" should be a constant somewhere rather than hard-coded strings in a few places to avoid errors and improve compiler's ability to detect errors later, similar to load_metrics:

pub const KV_METRICS_ENDPOINT: &str = "load_metrics";

.enable_cmpl_endpoints(true)
.enable_embeddings_endpoints(true)
.with_request_template(template)
.runtime(Some(Arc::new(distributed_runtime)))

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.

This seems like a pretty intrusive change if it adds runtime object that wasn't previously necessary for other functionalities.

I think we'll probably want some kind of broadcast generalizations to support doing some kind of call or operation on all instances, if that is what we're generally going for with this feature.

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.

CC @ryanolson @paulhendricks @grahamking (Graham on PTO, but for future viz)

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.

Yea Graham had me remove the DRT from this. I think there should be a way to access things via the ModelState.

Also see #1037 (comment)

@ryanolson ryanolson 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.

I failed to get my review done in a timely manner.

I don't think we should hold an http end point to trigger cache resets.

The proper way to do this would be to take a backend out of service, let it drain, reset the cache then, bring it back in service.

I think we need to think harder about how we issue this directive to a fleet of workers.

Overall this both complicates the public http api and likely introduces more complications that it goodness it provides.

I feel we should refer this change at the soonest opportunity.


mod openai;

pub mod clear_kv_blocks;

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.

Do we want to put this on the public API? I'm not so sure.

@grahamking

Copy link
Copy Markdown
Contributor

@jain-ria Could you revert this please? We now realize this isn't what we want to do, and need to put some extra thought into it. Sorry about that, I was on vacation.

@jain-ria

jain-ria commented Jun 24, 2025

Copy link
Copy Markdown
Contributor Author

Yes! Sorry for the delay, changes are here in this PR

@alec-flowers

alec-flowers commented Jun 25, 2025

Copy link
Copy Markdown
Contributor

While the HTTP server may not be the right place for it, we do need an API for certain external changes that is separate from what a User would see.

I guess I would break it up into User API, Admin API, and Planner API. With there being some overlap between Planner and Admin. For example an Admin could say I want to drain and reset the KV Cache of these workers or I want to scale up to 6 workers, and this would override any directives from Planner.

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

Labels

external-contribution Pull request is from an external contributor feat size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants