Repository navigation
[Serve] Refresh active_versions when update_version reuses replicas - #8
Merged
Merged
Conversation
Member
Author
A/B test on the GPU cluster (2026-10-06): fix confirmedTwo runs, identical except for the API server image:
Setup (no GPUs needed):
Sample stale state on the old image: Findings beyond the original report:
Raw data on amd-1: Still open from the checklist: unit tests, the residual millisecond window between the two DB writes, upstream PR, and shipping the image to TokenVisor clusters. The cluster was restored to |
The prober snapshots ReplicaInfo, probes for up to the readiness timeout, then writes the whole object back. update_version() relabels replicas without the lock, so a relabel landing mid-probe was reverted, and the autoscaler then replaced the healthy replica. Re-read the stored version before the write-back. Add unit tests for both this and the active_versions refresh.
Hoipang
marked this pull request as ready for review
October 7, 2026 06:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A count-only
sky serve update(only theservice:section changes, e.g.replicas: 1 → 2) can take every ready replica out of the load balancer for up to ~20 s. During that window the load balancer answers503 No ready replicaseven though the replica is READY and serving, and some in-flight requests hang until an upstream proxy times them out.This contradicts the rolling-update guarantee (old replicas keep serving until new ones are ready). That guarantee holds for spec changes; it breaks only in the replica-reuse shortcut for count-only changes.
TokenVisor hits this on every SkyPilot replica-count change: manual edits, and every autoscaler scale event (TokenVisor PR EmbeddedLLM/TokenVisor#807 calls
sky.serve.update(task, name, UpdateMode.ROLLING)with only the replica count changed).Evidence (GPU cluster, 2026-10-06, image
ghcr.io/embeddedllm/skypilot:v0.12.0, commit f579438 = ancestor ofellm-0.12.0)One SkyPilot service, Qwen3-0.6B on an RTX A4000, open-loop load at 7 requests/s, autoscaled 1 → 2 at 14:39:34 UTC:
sky serve update(count 1 → 2). Replica 1 READY and serving.503 No ready replicas78 times (41 + 37 per 10 s). Replica 1 stayed READY throughout.The 503 window is exactly one load-balancer sync interval (20 s).
Root cause (proven from source; identical on
skypilot-org/skypilotmaster)/controller/update_servicehandler callsupdate_versionsynchronously (sky/serve/controller.py:184).SkyPilotReplicaManager.update_version(sky/serve/replica_managers.py:1474) reuses each running replica whose config matches, by rewriting its stored version:info.version = version+serve_state.add_or_update_replica(...).get_active_replica_urls(replica_managers.py:1456), which returns READY replicas whose version is in the service record'sactive_versions.active_versionsis only recomputed byset_service_status_and_active_versions_from_replica(sky/serve/serve_utils.py:492), called from the replica prober loop (replica_managers.py:1443) everyENDPOINT_PROBE_INTERVAL_SECONDS= 10 s, after probing every replica with up toDEFAULT_READINESS_PROBE_TIMEOUT_SECONDS= 15 s each.active_versionsis still[1]→ it is filtered out → the ready list is empty. On a busy replica (slow readiness probe) that gap is ~10–25 s.LB_CONTROLLER_SYNC_INTERVAL_SECONDS= 20 s (sky/serve/load_balancer.py:88). If a pull lands in the gap, it keeps the empty list for a full 20 s. If no pull lands in it, nothing happens, which is why this is intermittent and why it shows up mostly under load.Spec changes are not affected: replicas are not reused, they keep their old version, and
active_versions(rolling = "versions with a ready replica") keeps them in rotation.This PR
Right after the reuse loop in
SkyPilotReplicaManager.update_version, recomputeactive_versionswith the same function the prober uses, so the relabel and the active set change together:get_latest_version_with_min_replicaspicks the latest version meetingmin_replicas, else the oldest version with a ready replica. After a reuse, v2 is the only version with ready replicas, so it is chosen.SHUTTING_DOWNguard inside it.returnfor non-emptyfile_mountsis untouched (no reuse happens on that path, so no gap).Open work for whoever picks this up
serve_state, a READY v1 replica, callupdate_version(2, …)for a count-only change and assertget_active_replica_urls()still returns that replica, for bothUpdateMode.ROLLINGandUpdateMode.BLUE_GREEN. Also assert a spec change (no reuse) keeps v1 active.active_versionsto include the new version before relabelling, then recompute after._sync_with_controller_oncepops and closes that replica'shttpx.AsyncClient(load_balancer.py:128–136); when it returns, a new client is created (:126) and every queued/retried request hits it at once. Default httpx pool is 100 connections,LB_STREAM_TIMEOUT= 120 s applies to the pool wait too, andLB_MAX_RETRY= 3, so the tail can wait > 300 s. The LB logs were gone before we looked. Reproduce once withsky serve logs <service> --load-balancercaptured to confirm. With this PR the client should no longer be dropped on a count change, so the hang may disappear with it; consider separately bounding total LB retry time and not closing a client whose URL disappears for one sync.sky serve updateon a service under steady traffic. It does not need GPUs: a CPU service (e.g. a tiny HTTP server with a readiness path) on Kubernetes, ~5–10 requests/s against the LB endpoint, thensky serve updatewith onlyreplicaschanged, repeated a few times (the bug needs an LB sync to land in the gap). Count503 No ready replicasand requests > 60 s. Expect some before, none after.ghcr.io/embeddedllm/skypilot:v0.12.0(k8s/manifest/skypilot-api-server-deploy.yaml,skypilot-helm-values.yaml). emu also pins a client wheel (tokenvisor-16042fbfrelease inservices/api/pyproject.toml). Confirm which artifact the controller pod actually runs, rebuild it, and re-run the TokenVisor SkyPilot autoscaling e2e (suite D in the GPU trial report) to verify zero failures during 1 → 2.skypilot-org/skypilotmaster; open the same fix there once tested.