Skip to content

Commit 7591d55

Browse files
committed
fix: surface PENDING peers under their served_model_name during boot
Before: when a launching peer is still in PENDING (no service advertised yet), get_all_models surfaced it with id="" and worker_group_id set. The frontend (ModelList.svelte) builds wgToModel from peers that already carry an id, then drops any remaining id="" peer whose worker_group_id doesn't appear in that map. During the brief PENDING window every peer in the worker group is service-less, so wgToModel is empty for that group and the replica is silently filtered out. By the time we COULD render it, registrar.go flips status from PENDING to READY and advertises the service in the same step — so PENDING is never actually visible on the dashboard. After: fall back to labels.served_model_name (already emitted by model-launch's _ocf_labels on every peer) when synthesising the no-service entry. The peer now has a real model id during boot, the frontend's grouping succeeds, and the status pill renders "pending" until the health check passes. Tests updated: the multi-node-replica grouping test previously asserted the follower kept id="". With served_model_name on every peer, both peers in the group now resolve to the same id; we still verify the shared worker_group_id keeps them in one replica. Added a defensive test for the older-binary case (no served_model_name label) where the id stays empty as before.
1 parent bd4427a commit 7591d55

2 files changed

Lines changed: 41 additions & 12 deletions

File tree

backend/services/model_service.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,13 @@ def get_all_models(endpoint: str, with_details: bool = False):
6363
# worker_group_id and show it as part of a launching/follower set.
6464
if not meta["worker_group_id"]:
6565
continue
66+
# Fall back to the served_model_name label so the frontend can
67+
# group PENDING peers under their eventual model card during boot.
68+
# Without this, the brief PENDING window is invisible because the
69+
# peer has no advertised service yet and nothing else maps its
70+
# worker_group_id back to a model id.
6671
entry = {
67-
"id": "", # no model yet
72+
"id": meta["labels"].get("served_model_name", ""),
6873
"object": "model",
6974
"created": "0x",
7075
"owner": "0x",

backend/tests/test_model_service.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def json(self):
5353
"slurm_job_id": "12345",
5454
"worker_group_id": "12345",
5555
"framework": "sglang",
56+
"served_model_name": "swiss-ai/Apertus-8B",
5657
"started_at": "2026-05-15T18:00:00Z",
5758
},
5859
"hardware": {"gpus": [{"name": "GH200"}] * 4},
@@ -100,9 +101,11 @@ def test_new_binary_head_carries_labels():
100101

101102

102103
def test_metrics_only_follower_groups_with_head_via_worker_group_id():
103-
"""A multi-node replica's follower has no `service` but does carry
104-
worker_group_id. It should appear in the output with id='' so the
105-
frontend can attribute it to the same replica as the head."""
104+
"""A peer with no advertised `service` (multi-node follower, or a head
105+
still in PENDING during boot) should fall back to its served_model_name
106+
label so the frontend can render the model card during the brief window
107+
before the service is published. Without the fallback, the peer's id
108+
stays empty and the frontend silently drops it."""
106109
with patch("backend.services.model_service.requests.get") as mock_get:
107110
mock_get.return_value = _dnt_response(
108111
{
@@ -114,15 +117,34 @@ def test_metrics_only_follower_groups_with_head_via_worker_group_id():
114117
assert len(out) == 2
115118
by_id = {e["peer_id"]: e for e in out}
116119
assert by_id["QmHead"]["id"] == "swiss-ai/Apertus-8B"
117-
assert by_id["QmFollower"]["id"] == ""
118-
# Shared worker_group_id lets the frontend group them.
120+
# Follower inherits id from the served_model_name label — same model card.
121+
assert by_id["QmFollower"]["id"] == "swiss-ai/Apertus-8B"
122+
assert by_id["QmFollower"]["status"] == "pending"
123+
# Shared worker_group_id lets the frontend group them within the model.
119124
assert (
120125
by_id["QmHead"]["worker_group_id"]
121126
== by_id["QmFollower"]["worker_group_id"]
122127
== "12345"
123128
)
124129

125130

131+
def test_pending_peer_without_served_model_name_label_falls_back_to_empty_id():
132+
"""Defensive: if a peer is mid-boot from an older binary that doesn't
133+
emit served_model_name, we still surface it via worker_group_id with
134+
id=''. The frontend then needs another peer in the same group with an
135+
id to attribute it; otherwise it's dropped."""
136+
peer = {
137+
**PEER_NEW_BINARY_FOLLOWER,
138+
"labels": {k: v for k, v in PEER_NEW_BINARY_FOLLOWER["labels"].items() if k != "served_model_name"},
139+
}
140+
with patch("backend.services.model_service.requests.get") as mock_get:
141+
mock_get.return_value = _dnt_response({"/QmPending": peer})
142+
out = get_all_models("http://x/v1/dnt/table", with_details=True)
143+
assert len(out) == 1
144+
assert out[0]["id"] == ""
145+
assert out[0]["worker_group_id"] == "12345"
146+
147+
126148
def test_follower_without_worker_group_id_skipped():
127149
"""Older binary follower with no labels and no service is uninformative —
128150
drop it so the model list stays clean."""
@@ -196,9 +218,10 @@ def test_real_prod_payload_returns_models():
196218

197219
def test_upgraded_payload_groups_multinode_replica():
198220
"""Simulated v0.0.6 deployment: the gemma 'multi-node demo' pair share a
199-
worker_group_id. One has a service, the other is metrics-only with id=''.
200-
Backend returns both entries with the shared worker_group_id so the
201-
frontend can aggregate them into one logical replica."""
221+
worker_group_id. Both peers carry the served_model_name label, so both
222+
resolve to the same model id even though only one advertises a service.
223+
Backend returns both entries with the shared worker_group_id + model id
224+
so the frontend can aggregate them into one logical replica."""
202225
with patch("backend.services.model_service.requests.get") as mock_get:
203226
mock_get.return_value = type(
204227
"R",
@@ -212,7 +235,8 @@ def test_upgraded_payload_groups_multinode_replica():
212235
by_wg.setdefault(e["worker_group_id"], []).append(e)
213236
multi = [v for v in by_wg.values() if len(v) > 1]
214237
assert multi, "fixture should contain at least one multi-peer worker group"
215-
# At least one peer in the multi-peer group should be metrics-only (id='').
216238
pair = multi[0]
217-
assert any(e["id"] == "" for e in pair), pair
218-
assert any(e["id"] != "" for e in pair), pair
239+
# Both peers in the group share the same non-empty model id.
240+
ids = {e["id"] for e in pair}
241+
assert ids != {""}, pair
242+
assert len(ids) == 1, f"peers in one worker group should share one model id: {ids}"

0 commit comments

Comments
 (0)