Skip to content

Commit 5411724

Browse files
authored
FEAT: support replica removing (#4784)
1 parent e8a5698 commit 5411724

12 files changed

Lines changed: 357 additions & 48 deletions

File tree

xinference/api/restful_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,32 @@ async def terminate_model(self, model_uid: str) -> JSONResponse:
787787
raise HTTPException(status_code=500, detail=str(e))
788788
return JSONResponse(content=None)
789789

790+
async def terminate_model_replica(
791+
self, model_uid: str, replica_id: int
792+
) -> JSONResponse:
793+
try:
794+
assert self._app is not None
795+
remaining_replicas = await (
796+
await self._get_supervisor_ref()
797+
).terminate_model_replica(model_uid, replica_id)
798+
if remaining_replicas == 0:
799+
self._app.router.routes = [
800+
route
801+
for route in self._app.router.routes
802+
if not (
803+
hasattr(route, "path")
804+
and isinstance(route.path, str)
805+
and route.path == "/" + model_uid
806+
)
807+
]
808+
return JSONResponse(content={"remaining_replicas": remaining_replicas})
809+
except ValueError as ve:
810+
logger.error(str(ve), exc_info=True)
811+
raise HTTPException(status_code=400, detail=str(ve))
812+
except Exception as e:
813+
logger.error(e, exc_info=True)
814+
raise HTTPException(status_code=500, detail=str(e))
815+
790816
async def _get_model_last_error(self, replica_model_uid: bytes, e: Exception):
791817
if not isinstance(e, xo.ServerClosed):
792818
return e

xinference/api/routers/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ def register_routes(api: "RESTfulAPI") -> None:
113113
methods=["GET"],
114114
dependencies=([Security(auth, scopes=["models:list"])] if is_auth else None),
115115
)
116+
router.add_api_route(
117+
"/v1/models/{model_uid}/replicas/{replica_id}",
118+
api.terminate_model_replica,
119+
methods=["DELETE"],
120+
dependencies=([Security(auth, scopes=["models:stop"])] if is_auth else None),
121+
)
116122
router.add_api_route(
117123
"/v1/models/{model_uid}/requests/{request_id}/abort",
118124
api.abort_request,

xinference/client/restful/async_restful_client.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1432,6 +1432,21 @@ async def terminate_model(self, model_uid: str):
14321432
)
14331433
await _release_response(response)
14341434

1435+
async def terminate_model_replica(self, model_uid: str, replica_id: int) -> int:
1436+
"""Terminate a specific replica of a running model."""
1437+
1438+
url = f"{self.base_url}/v1/models/{model_uid}/replicas/{replica_id}"
1439+
1440+
response = await self.session.delete(url, headers=self._headers)
1441+
if response.status != 200:
1442+
raise RuntimeError(
1443+
f"Failed to terminate model replica, detail: {await _get_error_string(response)}"
1444+
)
1445+
1446+
response_data = await response.json()
1447+
await _release_response(response)
1448+
return response_data["remaining_replicas"]
1449+
14351450
async def get_launch_model_progress(self, model_uid: str) -> dict:
14361451
"""
14371452
Get progress of the specific model.
@@ -1575,7 +1590,7 @@ async def get_model(self, model_uid: str) -> AsyncRESTfulModelHandle:
15751590
model_uid, self.base_url, auth_headers=self._headers
15761591
)
15771592
else:
1578-
raise ValueError(f"Unknown model type:{desc['model_type']}")
1593+
raise ValueError(f"Unknown model type: {desc['model_type']}")
15791594

15801595
async def describe_model(self, model_uid: str):
15811596
"""

xinference/client/restful/restful_client.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,20 @@ def terminate_model(self, model_uid: str):
13291329
f"Failed to terminate model, detail: {_get_error_string(response)}"
13301330
)
13311331

1332+
def terminate_model_replica(self, model_uid: str, replica_id: int) -> int:
1333+
"""Terminate a specific replica of a running model."""
1334+
1335+
url = f"{self.base_url}/v1/models/{model_uid}/replicas/{replica_id}"
1336+
1337+
response = self.session.delete(url, headers=self._headers)
1338+
if response.status_code != 200:
1339+
raise RuntimeError(
1340+
f"Failed to terminate model replica, detail: {_get_error_string(response)}"
1341+
)
1342+
1343+
response_data = response.json()
1344+
return response_data["remaining_replicas"]
1345+
13321346
def get_launch_model_progress(self, model_uid: str) -> dict:
13331347
"""
13341348
Get progress of the specific model.
@@ -1467,7 +1481,7 @@ def get_model(self, model_uid: str) -> RESTfulModelHandle:
14671481
model_uid, self.base_url, auth_headers=self._headers
14681482
)
14691483
else:
1470-
raise ValueError(f"Unknown model type:{desc['model_type']}")
1484+
raise ValueError(f"Unknown model type: {desc['model_type']}")
14711485

14721486
def describe_model(self, model_uid: str):
14731487
"""

xinference/core/status_guard.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,17 @@ def get_replica_statuses(self, model_uid: str) -> List[ReplicaStatus]:
143143
if model_uid in self._model_uid_to_info:
144144
return self._model_uid_to_info[model_uid].replica_statuses or []
145145
return []
146+
147+
def remove_replica_status(self, model_uid: str, replica_id: int) -> int:
148+
"""Remove status for a specific replica and return remaining replica count."""
149+
if model_uid not in self._model_uid_to_info:
150+
logger.warning(f"Model {model_uid} not found in status guard")
151+
return 0
152+
153+
instance_info = self._model_uid_to_info[model_uid]
154+
replica_statuses = instance_info.replica_statuses or []
155+
instance_info.replica_statuses = [
156+
status for status in replica_statuses if status.replica_id != replica_id
157+
]
158+
instance_info.replica = len(instance_info.replica_statuses)
159+
return instance_info.replica

0 commit comments

Comments
 (0)