Skip to content

Commit 83762b7

Browse files
aoshen02claudemergify[bot]
authored
[Frontend] Add /abort_requests to the RLHF dev API router (#47173)
Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent a02984e commit 83762b7

8 files changed

Lines changed: 69 additions & 12 deletions

File tree

docs/serving/online_serving/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ For further details on Weight Transfer, please refer to [this page](../../traini
170170
- `/pause` - Pause generation (causes denial of service)
171171
- `/resume` - Resume generation
172172
- `/is_paused` - Check if generation is paused
173+
- `/abort_requests` - Abort in-flight requests (all in-flight, or the given `request_ids`) without pausing the scheduler
173174
- `/init_weight_transfer_engine` - Initialize weight transfer engine for RLHF
174175
- `/start_weight_update` - Prepares the inference engine for a weight update.
175176
- `/update_weights` - Update model weights (can alter model behavior)

docs/training/async_rl.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ When using the vLLM HTTP server, the same functionality is available via:
4242

4343
- `POST /pause?mode=keep` - Pause generation
4444
- `POST /resume` - Resume generation
45+
- `POST /abort_requests` - Abort in-flight requests without pausing the scheduler (send `{}` to abort all, or `{"request_ids": [...]}`)
4546

4647
!!! note "Data Parallelism"
4748
When using data parallelism with vLLM's **internal load balancer** (i.e. `data_parallel_backend="ray"`), pause and resume are handled automatically across all DP ranks -- a single call is sufficient. When using an **external load balancer** (i.e. multiple independent vLLM instances behind a proxy), you must send pause and resume requests to **every** engine instance individually before and after the weight update.

docs/usage/security.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ The following endpoints **do not require authentication** even when `--api-key`
191191
- `/pause` - Pause generation (causes denial of service)
192192
- `/resume` - Resume generation
193193
- `/is_paused` - Check if generation is paused
194+
- `/abort_requests` - Abort in-flight requests (causes loss of in-flight work)
194195
- `/scale_elastic_ep` - Trigger scaling operations
195196
- `/is_scaling_elastic_ep` - Check if scaling is in progress
196197
- `/init_weight_transfer_engine` - Initialize weight transfer engine for RLHF

rust/src/llm/src/inflight.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ impl InflightRequests {
6464
.collect()
6565
}
6666

67+
/// Collect the internal engine ids of every in-flight request. Used to
68+
/// abort all outstanding requests when no external ids are given.
69+
pub(crate) fn all_internal_ids(&self) -> Vec<String> {
70+
let map = self.map.lock();
71+
map.values().flat_map(|internal_ids| internal_ids.keys()).cloned().collect()
72+
}
73+
6774
#[cfg(test)]
6875
fn is_empty(&self) -> bool {
6976
self.map.lock().is_empty()

rust/src/llm/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ impl Llm {
122122
/// tracking entries themselves are removed when the corresponding output
123123
/// streams are dropped, not here.
124124
pub async fn abort(&self, external_ids: &[String]) -> Result<()> {
125-
let internal_ids = self.inflight.resolve(external_ids);
125+
// Empty `external_ids` means abort every in-flight request.
126+
let internal_ids = if external_ids.is_empty() {
127+
self.inflight.all_internal_ids()
128+
} else {
129+
self.inflight.resolve(external_ids)
130+
};
126131
if internal_ids.is_empty() {
127132
return Ok(());
128133
}

rust/src/server/src/routes/abort_requests.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@ pub async fn abort_requests(
2020
body: Result<Json<AbortRequestsRequest>, JsonRejection>,
2121
) -> Result<StatusCode, ApiError> {
2222
let Json(body) = body.map_err(|error| ApiError::json_parse_error(error.body_text()))?;
23-
let request_ids = body.request_ids.ok_or_else(|| {
24-
ApiError::invalid_request(
25-
"Missing 'request_ids' in request body".to_string(),
26-
Some("request_ids"),
27-
)
28-
})?;
23+
// Empty/missing `request_ids` aborts all in-flight requests.
24+
let request_ids = body.request_ids.unwrap_or_default();
2925

3026
state
3127
.chat

rust/src/server/src/routes/tests.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5276,10 +5276,12 @@ async fn abort_requests_route_returns_ok_for_well_formed_body() {
52765276

52775277
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
52785278
#[serial]
5279-
async fn abort_requests_route_rejects_missing_request_ids() {
5279+
async fn abort_requests_route_aborts_all_when_request_ids_missing() {
52805280
let (app, engine_task) =
52815281
test_admin_app_with_engine_script(|_dealer, _push| boxed_test_future(async move {})).await;
52825282

5283+
// Missing `request_ids` means "abort all in-flight requests"; with no
5284+
// in-flight requests this is a no-op that still succeeds.
52835285
let response = app
52845286
.clone()
52855287
.call(
@@ -5293,11 +5295,10 @@ async fn abort_requests_route_rejects_missing_request_ids() {
52935295
.await
52945296
.expect("call app");
52955297

5296-
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
5298+
let status = response.status();
52975299
let body = to_bytes(response.into_body(), usize::MAX).await.expect("read body");
5298-
let json: serde_json::Value = serde_json::from_slice(&body).expect("decode json");
5299-
assert_eq!(json["error"]["type"], "invalid_request_error");
5300-
assert_eq!(json["error"]["param"], "request_ids");
5300+
assert_eq!(status, StatusCode::OK, "{}", String::from_utf8_lossy(&body));
5301+
assert!(body.is_empty());
53015302
engine_task.abort_and_join().await;
53025303
}
53035304

vllm/entrypoints/serve/dev/rlhf/api_router.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,51 @@ async def resume_generation(raw_request: Request) -> JSONResponse:
9191
)
9292

9393

94+
@router.post("/abort_requests")
95+
async def abort_requests(raw_request: Request) -> JSONResponse:
96+
"""Abort in-flight requests without pausing the scheduler.
97+
98+
Empty/missing ``request_ids`` aborts all in-flight requests.
99+
"""
100+
101+
engine = engine_client(raw_request)
102+
103+
try:
104+
body = await raw_request.json()
105+
except json.JSONDecodeError as e:
106+
raise HTTPException(status_code=400, detail="Invalid JSON format") from e # noqa: B904
107+
108+
request_ids = body.get("request_ids")
109+
110+
try:
111+
if request_ids:
112+
# Body ids are external (user-supplied) request ids.
113+
await engine.abort(request_ids)
114+
else:
115+
# The dev RL server runs AsyncLLM; abort everything it is tracking.
116+
# request_states is keyed by internal ids; parent_requests holds
117+
# parallel-sampling parents. Abort both as internal ids.
118+
from vllm.v1.engine.async_llm import AsyncLLM
119+
120+
assert isinstance(engine, AsyncLLM)
121+
op = engine.output_processor
122+
request_ids = [
123+
*op.request_states.keys(),
124+
*op.parent_requests.keys(),
125+
]
126+
await engine.abort(request_ids, internal=True)
127+
return JSONResponse(
128+
content={"status": "aborted", "aborted": len(request_ids)},
129+
status_code=HTTPStatus.OK.value,
130+
)
131+
except Exception as err: # pragma: no cover - defensive
132+
logger.exception("Failed to abort requests")
133+
return JSONResponse(
134+
content={"error": f"Failed to abort requests: {err}"},
135+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
136+
)
137+
138+
94139
@router.get("/is_paused")
95140
async def is_paused(raw_request: Request) -> JSONResponse:
96141
"""Return the current pause status."""

0 commit comments

Comments
 (0)