-
Notifications
You must be signed in to change notification settings - Fork 742
[Feature] Refactor /v1/abort_requests Endpoint #7615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1292,10 +1292,28 @@ def _insert_zmq_task_to_scheduler(self): | |||||||||||||||
| self.request_worker_map[req_id_for_map] = worker_pid | ||||||||||||||||
| status_value = data.get("status", None) | ||||||||||||||||
| if status_value is not None and status_value == RequestStatus.ABORT.value: | ||||||||||||||||
| req_id = data["request_id"] | ||||||||||||||||
| self.llm_logger.info(f"Receive abort request, req_id: {req_id}") | ||||||||||||||||
| if envs.ENABLE_V1_KVCACHE_SCHEDULER: | ||||||||||||||||
| self.resource_manager.add_abort_req_ids(req_id) | ||||||||||||||||
| if not envs.ENABLE_V1_KVCACHE_SCHEDULER: | ||||||||||||||||
| self.llm_logger.info("abort requests only supported in ENABLE_V1_KVCACHE_SCHEDULER") | ||||||||||||||||
| else: | ||||||||||||||||
| abort_all = data.get("abort_all", False) | ||||||||||||||||
| req_ids = data.get("req_ids", []) | ||||||||||||||||
| if abort_all or req_ids: | ||||||||||||||||
| target_req_ids = self._resolve_abort_targets(abort_all, req_ids) | ||||||||||||||||
| self.llm_logger.info( | ||||||||||||||||
| f"Receive abort_reqs, abort_all={abort_all}, " | ||||||||||||||||
| f"input={len(req_ids)}, resolved={len(target_req_ids)}" | ||||||||||||||||
| ) | ||||||||||||||||
| for req_id in target_req_ids: | ||||||||||||||||
| self.resource_manager.add_abort_req_ids(req_id) | ||||||||||||||||
| time.sleep(0.0001) | ||||||||||||||||
| if self.cfg.scheduler_config.splitwise_role != "prefill": | ||||||||||||||||
| results = self._build_abort_results(target_req_ids) | ||||||||||||||||
|
qwes5s5 marked this conversation as resolved.
|
||||||||||||||||
| if results: | ||||||||||||||||
| self.scheduler.put_results(results) | ||||||||||||||||
| else: | ||||||||||||||||
| req_id = data["request_id"] | ||||||||||||||||
|
||||||||||||||||
| req_id = data["request_id"] | |
| req_id = data.get("request_id") | |
| if not req_id: | |
| self.llm_logger.warning( | |
| "Receive abort request without request_id, skip invalid abort message" | |
| ) | |
| continue |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now_reqs 可能非常大(并且包含请求 ID),这里直接把完整集合打到 debug 日志会导致日志量激增、影响排障信噪比,甚至可能造成敏感信息暴露风险。建议仅记录数量/采样(例如 len(now_reqs) + 前 N 个),或用更低成本的结构化字段。
| self.llm_logger.debug(f"now_reqs: {now_reqs}") | |
| sample_size = 10 | |
| now_reqs_sample = list(now_reqs)[:sample_size] | |
| self.llm_logger.debug( | |
| f"now_reqs count={len(now_reqs)}, sample={now_reqs_sample}, " | |
| f"sample_truncated={len(now_reqs) > sample_size}" | |
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1130,6 +1130,18 @@ async def abort(self, request_id, n=1) -> None: | |||||||||||||||||||||||||
| request_ids=",".join(request_ids), | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async def abort_reqs(self, req_ids=None, abort_all=False): | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| Fire-and-forget: abort multiple requests in one ZMQ message. | ||||||||||||||||||||||||||
| Used by /v1/abort_requests API. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| data = { | ||||||||||||||||||||||||||
| "status": RequestStatus.ABORT.value, | ||||||||||||||||||||||||||
| "abort_all": abort_all, | ||||||||||||||||||||||||||
| "req_ids": req_ids or [], | ||||||||||||||||||||||||||
|
Comment on lines
+1138
to
+1141
|
||||||||||||||||||||||||||
| data = { | |
| "status": RequestStatus.ABORT.value, | |
| "abort_all": abort_all, | |
| "req_ids": req_ids or [], | |
| normalized_req_ids = req_ids or [] | |
| if not abort_all and not normalized_req_ids: | |
| return | |
| data = { | |
| "status": RequestStatus.ABORT.value, | |
| "abort_all": abort_all, | |
| "req_ids": normalized_req_ids, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -496,13 +496,8 @@ async def abort_requests(request: Request): | |
| if not abort_all and not req_ids: | ||
| return JSONResponse(status_code=400, content={"error": "must provide abort_all=true or req_ids"}) | ||
|
|
||
| control_request = ControlRequest( | ||
| request_id=f"control-{uuid.uuid4()}", | ||
| method="abort_requests", | ||
| args={"abort_all": abort_all, "req_ids": req_ids or []}, | ||
| ) | ||
| control_response = await app.state.engine_client.run_control_method(control_request) | ||
| return control_response.to_api_json_response() | ||
| await app.state.engine_client.abort_reqs(req_ids=req_ids or [], abort_all=abort_all) | ||
| return Response(status_code=200) | ||
|
qwes5s5 marked this conversation as resolved.
qwes5s5 marked this conversation as resolved.
qwes5s5 marked this conversation as resolved.
Comment on lines
+499
to
+500
|
||
|
|
||
|
|
||
| def wrap_streaming_generator(original_generator: AsyncGenerator): | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,7 +18,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| import aiohttp | ||||||||||||||||||||||||||||||||||||||||||||||||
| import uvicorn | ||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi import FastAPI, HTTPException, Request | ||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi.responses import JSONResponse, ORJSONResponse, Response, StreamingResponse | ||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi.responses import ORJSONResponse, Response, StreamingResponse | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from fastdeploy.router.utils import ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| InstanceInfo, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,6 +29,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| from fastdeploy.utils import router_logger as logger | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| app = FastAPI() | ||||||||||||||||||||||||||||||||||||||||||||||||
| _background_tasks = set() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @dataclass | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -588,39 +589,15 @@ async def abort_requests(request: Request): | |||||||||||||||||||||||||||||||||||||||||||||||
| decode_servers = app.state.router.decode_servers | ||||||||||||||||||||||||||||||||||||||||||||||||
| all_servers = prefill_servers + decode_servers | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| async with aiohttp.ClientSession() as session: | ||||||||||||||||||||||||||||||||||||||||||||||||
| tasks = [session.post(f"{server.url()}/v1/abort_requests", json=body) for server in all_servers] | ||||||||||||||||||||||||||||||||||||||||||||||||
| responses = await asyncio.gather(*tasks, return_exceptions=True) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Aggregate results from Node D only | ||||||||||||||||||||||||||||||||||||||||||||||||
| all_aborted = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| all_not_found = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| errors = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| decode_start = len(prefill_servers) | ||||||||||||||||||||||||||||||||||||||||||||||||
| for i, (server, resp) in enumerate(zip(all_servers, responses)): | ||||||||||||||||||||||||||||||||||||||||||||||||
| if i < decode_start: | ||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(resp, Exception): | ||||||||||||||||||||||||||||||||||||||||||||||||
| errors.append({"server": server.url(), "error": str(resp)}) | ||||||||||||||||||||||||||||||||||||||||||||||||
| elif resp.status == 200: | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = await resp.json() | ||||||||||||||||||||||||||||||||||||||||||||||||
| result = data.get("result") or {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| all_aborted.extend(result.get("aborted", [])) | ||||||||||||||||||||||||||||||||||||||||||||||||
| all_not_found.extend(result.get("not_found", [])) | ||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||
| errors.append({"server": server.url(), "status": resp.status}) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return JSONResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||
| content={ | ||||||||||||||||||||||||||||||||||||||||||||||||
| "request_id": f"router-{uuid4()}", | ||||||||||||||||||||||||||||||||||||||||||||||||
| "status": "success" if not errors else "error", | ||||||||||||||||||||||||||||||||||||||||||||||||
| "error_message": None if not errors else str(errors), | ||||||||||||||||||||||||||||||||||||||||||||||||
| "result": { | ||||||||||||||||||||||||||||||||||||||||||||||||
| "aborted": all_aborted, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "not_found": list(set(all_not_found)), | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| async def _forward_abort(): | ||||||||||||||||||||||||||||||||||||||||||||||||
| async with aiohttp.ClientSession() as session: | ||||||||||||||||||||||||||||||||||||||||||||||||
| tasks = [session.post(f"{server.url()}/v1/abort_requests", json=body) for server in all_servers] | ||||||||||||||||||||||||||||||||||||||||||||||||
| await asyncio.gather(*tasks, return_exceptions=True) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+593
to
+596
|
||||||||||||||||||||||||||||||||||||||||||||||||
| async with aiohttp.ClientSession() as session: | |
| tasks = [session.post(f"{server.url()}/v1/abort_requests", json=body) for server in all_servers] | |
| await asyncio.gather(*tasks, return_exceptions=True) | |
| timeout = aiohttp.ClientTimeout(total=5) | |
| async def _post_abort(session: aiohttp.ClientSession, server): | |
| server_url = f"{server.url()}/v1/abort_requests" | |
| try: | |
| async with session.post(server_url, json=body) as resp: | |
| if resp.status != 200: | |
| logger.warning(f"Abort request forwarding to {server_url} returned status {resp.status}") | |
| except asyncio.TimeoutError: | |
| logger.warning(f"Abort request forwarding to {server_url} timed out") | |
| except aiohttp.ClientError as exc: | |
| logger.warning(f"Abort request forwarding to {server_url} failed: {exc}") | |
| except Exception: | |
| logger.warning( | |
| f"Unexpected error when forwarding abort request to {server_url}:\n{traceback.format_exc()}" | |
| ) | |
| async with aiohttp.ClientSession(timeout=timeout) as session: | |
| tasks = [_post_abort(session, server) for server in all_servers] | |
| await asyncio.gather(*tasks) |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Router 的 /v1/abort_requests 现在直接返回空 body 的 200(异步转发),与现有文档中“返回 aborted/not_found 与 token 数”的描述不一致,也可能破坏客户端兼容性。建议保持原响应 schema(例如返回 request_id + status,并注明为异步受理),或同步更新 docs/online_serving/router.md 等文档并明确该接口语义变更。
| return Response(status_code=200) | |
| response_payload = {"status": "accepted"} | |
| if isinstance(body, dict): | |
| if "request_id" in body: | |
| response_payload["request_id"] = body["request_id"] | |
| elif "request_ids" in body: | |
| response_payload["request_ids"] = body["request_ids"] | |
| return ORJSONResponse(response_payload, status_code=200) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在接收线程里对每个
req_id都time.sleep(0.0001)会把 abort_all 的耗时线性放大(例如上万请求会变成秒级阻塞),并且阻塞期间无法及时处理其他 ZMQ 消息。建议改为不 sleep,或仅在必要时做批量/指数退避(例如每 N 个 sleep 一次),并评估是否需要把 add_abort_req_ids 放到更合适的异步/批处理路径。