Repository: gpustack/gpustack

Issue:
Optimize error message for deploying multiple vllm distributed models on the same worker
**Is your enhancement related to a problem? Please describe.**

<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

The error message is not clear when using the same worker for vLLM distributed model deployment

![Image](https://github.com/user-attachments/assets/50078514-ee70-43b3-b33f-6195cb0c521b)

![Image](https://github.com/user-attachments/assets/4676580b-e1cc-479d-89c0-c54b44a7e9dc)

**Describe the solution you'd like**

<!-- A clear and concise description of what you want to happen. -->

Optimize the error message for this use case to clearly state: "Each worker can only be assigned to one distributed vLLM model instance at a time."

Discussion:
@Finenyaco: Verified on main 69c6fac

<img width="848" alt="Image" src="https://github.com/user-attachments/assets/ec4d8432-a041-46ce-bec4-30efae2d89b2" />

Solution patch:
diff --git a/gpustack/policies/candidate_selectors/vllm_resource_fit_selector.py b/gpustack/policies/candidate_selectors/vllm_resource_fit_selector.py
index 6f7fbb2..68b06db 100644
--- a/gpustack/policies/candidate_selectors/vllm_resource_fit_selector.py
+++ b/gpustack/policies/candidate_selectors/vllm_resource_fit_selector.py
@@ -11,6 +11,7 @@ from gpustack.policies.base import (
 )
 from gpustack.policies.utils import (
     get_worker_allocatable_resource,
+    get_worker_model_instances,
 )
 from gpustack.schemas.models import (
     ComputedResourceClaim,
@@ -206,6 +207,9 @@ class VLLMResourceFitSelector(ScheduleCandidatesSelector):
             )
 
     def _set_messages(self):
+        if self._messages:
+            return
+
         messages = [
             f"The model requires {self._gpu_memory_utilization * 100}%(--gpu-memory-utilization={self._gpu_memory_utilization}) VRAM for each GPU that satisfies {byte_to_gib(self._vram_claim)} GiB VRAM in total."
         ]
@@ -604,6 +608,9 @@ class VLLMResourceFitSelector(ScheduleCandidatesSelector):
             if worker.name not in self._selected_gpu_workers:
                 continue
 
+            if not await self._validate_distributed_vllm_limit_per_worker(worker):
+                return []
+
             if worker.name == main_worker_name:
                 continue
 
@@ -634,6 +641,20 @@ class VLLMResourceFitSelector(ScheduleCandidatesSelector):
             )
         ]
 
+    async def _validate_distributed_vllm_limit_per_worker(self, worker: Worker) -> bool:
+        """
+        Validate that there is no more than one distributed vLLM instance per worker.
+        """
+        instances = await get_worker_model_instances(self._engine, worker)
+        for instance in instances:
+            if instance.distributed_servers and instance.distributed_servers.ray_actors:
+                self._messages = [
+                    f"Each worker can run only one distributed vLLM instance. Worker '{worker.name}' already has '{instance.name}'."
+                ]
+                return False
+
+        return True
+
     async def _get_worker_vram_claim(
         self,
         worker: Worker,
diff --git a/gpustack/routes/models.py b/gpustack/routes/models.py
index 2b37848..19ab1b0 100644
--- a/gpustack/routes/models.py
+++ b/gpustack/routes/models.py
@@ -5,6 +5,7 @@ from fastapi.responses import StreamingResponse
 from sqlalchemy import bindparam, cast
 from sqlalchemy.dialects.postgresql import JSONB
 from sqlmodel import col, or_
+from sqlmodel.ext.asyncio.session import AsyncSession
 
 from gpustack.config.config import get_global_config
 from gpustack.api.exceptions import (
@@ -215,6 +216,9 @@ async def validate_gpu_ids(  # noqa: C901
                 message=f'vLLM backend is only supported on Linux, but the selected worker "{worker.name}" is running on {worker_os.capitalize()}.'
             )
 
+        if model_backend == BackendEnum.VLLM and len(worker_name_set) > 1:
+            await validate_distributed_vllm_limit_per_worker(session, model_in, worker)
+
     if model_backend == BackendEnum.VLLM:
         cfg = get_global_config()
         if len(worker_name_set) > 1 and not cfg.enable_ray:
@@ -231,6 +235,24 @@ async def validate_gpu_ids(  # noqa: C901
             )
 
 
+async def validate_distributed_vllm_limit_per_worker(
+    session: AsyncSession, model: Union[ModelCreate, ModelUpdate], worker: Worker
+):
+    """
+    Validate that there is no more than one distributed vLLM instance per worker.
+    """
+    instances = await ModelInstance.all_by_field(session, "worker_id", worker.id)
+    for instance in instances:
+        if (
+            instance.distributed_servers
+            and instance.distributed_servers.ray_actors
+            and instance.model_name != model.name
+        ):
+            raise BadRequestException(
+                message=f"Each worker can run only one distributed vLLM instance. Worker '{worker.name}' already has '{instance.name}'."
+            )
+
+
 @router.post("", response_model=ModelPublic)
 async def create_model(session: SessionDep, model_in: ModelCreate):
     existing = await Model.one_by_field(session, "name", model_in.name)


Test patch:
diff --git a/tests/policies/candidate_selectors/vllm/test_vllm_resource_fit_selector.py b/tests/policies/candidate_selectors/vllm/test_vllm_resource_fit_selector.py
index 29e60e3..2e7ce79 100644
--- a/tests/policies/candidate_selectors/vllm/test_vllm_resource_fit_selector.py
+++ b/tests/policies/candidate_selectors/vllm/test_vllm_resource_fit_selector.py
@@ -76,6 +76,10 @@ async def test_manual_schedule_to_2_worker_2_gpu(config):
             'gpustack.policies.utils.get_worker_model_instances',
             return_value=[],
         ),
+        patch(
+            'gpustack.policies.candidate_selectors.vllm_resource_fit_selector.get_worker_model_instances',
+            return_value=[],
+        ),
         patch(
             'gpustack.policies.scorers.placement_scorer.get_model_instances',
             return_value=[],
@@ -151,6 +155,10 @@ async def test_manual_schedule_to_2_worker_4_gpu_select_main_with_most_gpus(
             'gpustack.policies.utils.get_worker_model_instances',
             return_value=[],
         ),
+        patch(
+            'gpustack.policies.candidate_selectors.vllm_resource_fit_selector.get_worker_model_instances',
+            return_value=[],
+        ),
         patch(
             'gpustack.policies.scorers.placement_scorer.get_model_instances',
             return_value=[],
@@ -230,6 +238,10 @@ async def test_manual_schedule_to_3_workers_4_gpus(
             'gpustack.policies.utils.get_worker_model_instances',
             return_value=[],
         ),
+        patch(
+            'gpustack.policies.candidate_selectors.vllm_resource_fit_selector.get_worker_model_instances',
+            return_value=[],
+        ),
         patch(
             'gpustack.policies.scorers.placement_scorer.get_model_instances',
             return_value=[],