diff --git a/README.md b/README.md
index 72291aa4509..15d52f5943e 100644
--- a/README.md
+++ b/README.md
@@ -378,6 +378,30 @@ Add release notes manually on GitHub. See the
parsing bug and an upper bound before Click 9.
+
+ | [Serve] Keep reused replicas routable on count-only updates |
+ 7f89638 (plus 6444f96, 82fc2fb) |
+ sky/serve/replica_managers.py |
+
+ A sky serve update that changes only the
+ service: section (e.g. replicas: 1 → 2)
+ reuses running replicas by relabelling them to the new version, but
+ active_versions was only refreshed by the next prober
+ pass. Until then the load balancer filtered out every reused replica
+ and returned 503 No ready replicas for up to one sync
+ interval (20 s). Regression from upstream #3249, which replaced the
+ per-sync version choice used when reuse (#3214) was written.
+ Fix: recompute active_versions right after the relabel,
+ using the prober's own function.
+ Follow-up 6444f96: the prober wrote back its stale
+ ReplicaInfo snapshot after probing, reverting a relabel
+ that landed mid-probe (update_version does not hold the
+ replica lock), so the autoscaler replaced the healthy replica. The
+ prober now re-reads the stored version before writing back.
+ 82fc2fb removes the unit test added in
+ 6444f96.
+ |
+
@@ -461,6 +485,8 @@ git cherry-pick 5420c46 bb8cbc9 0e986b2 f579438 # Intel xe GPU discovery and fo
git cherry-pick b43391c 64919e5 # Publish wheels on push with unique versions
git cherry-pick 44d09ea # Allow Click 8.2.1+ while excluding 8.2.0 and 9.x
git cherry-pick 12bd32c # Mark wheel releases as latest (not prereleases)
+# Keep reused replicas routable on count-only serve updates (3 commits, in order):
+git cherry-pick 7f89638 6444f96 82fc2fb
# Resolve any conflicts if upstream changed the same files
# 4. Push new branch
diff --git a/sky/serve/replica_managers.py b/sky/serve/replica_managers.py
index 7fd96069315..8a9a9ac36a2 100644
--- a/sky/serve/replica_managers.py
+++ b/sky/serve/replica_managers.py
@@ -1422,6 +1422,13 @@ def _probe_all_replicas(self) -> None:
f'seconds ({current_delay_seconds}s '
f'/ {initial_delay_seconds}s). '
'Skipping.')
+ # `update_version` relabels replicas without holding the lock,
+ # possibly while this probe was in flight. Keep its version
+ # instead of writing back the stale one from our snapshot.
+ latest_info = serve_state.get_replica_info_from_id(
+ self._service_name, info.replica_id)
+ if latest_info is not None:
+ info.version = latest_info.version
serve_state.add_or_update_replica(self._service_name,
info.replica_id, info)
if should_teardown:
@@ -1539,6 +1546,13 @@ def update_version(self, version: int, spec: 'service_spec.SkyServiceSpec',
logger.info('Replica config changed (rest), skipping. '
f'old: {old_config}, '
f'new: {new_config}')
+ # A reused replica now carries the new version, but `active_versions`
+ # still lists only the old ones until the next prober cycle, so the
+ # load balancer would drop every reused replica in between. Refresh it
+ # now so the reuse never takes a ready replica out of rotation.
+ serve_utils.set_service_status_and_active_versions_from_replica(
+ self._service_name,
+ serve_state.get_replica_infos(self._service_name), update_mode)
def _get_version_spec(self, version: int) -> 'service_spec.SkyServiceSpec':
spec = serve_state.get_spec(self._service_name, version)