Commit 5d4b3bb
[serve] Gate rank-consistency check on replica membership changes (#64911)
## [serve] Gate rank-consistency check on replica membership changes
### Why
The Ray Serve controller runs on the head node, continuously reconciling
deployments and
making autoscaling decisions.
`check_rank_consistency_and_reassign_minimally` runs at the
tail of every `DeploymentState.update()` — once per deployment per
control-loop tick,
which at measured loop rates is several times a second.
The pass is O(N) in replicas with large constant factors: it
materializes the active-key
set and the rank-key set, takes set differences both ways, copies the
entire `_ranks`
dict, tallies per-rank counts across every active key, and sorts all
rank values — then
the manager above it regroups replicas per node. In steady state every
one of those
passes re-derives an identical answer. At 16K, py-spy showed it
monopolizing the event
loop and starving handle-report ingest for 20 s+.
Ranks can only become inconsistent when replica membership changes, so
gate the pass on
the active replica-id set: run it when the set differs from the last
checked one, skip
when it does not. Transitions are unaffected — membership churn during
rollouts and
autoscaling changes the set, so the pass still runs exactly when it can
matter. The
guards that were already there (deployment not HEALTHY, or any STARTING
replica) run
first, before the replica list is materialized.
`RAY_SERVE_FAIL_ON_RANK_ERROR` defaults off, so in production the pass
can log an error
and return a safe default. When that happens the membership is
deliberately **not**
cached and the check is retried next tick, rather than latching a
membership the pass
never validated. The error flag is read before the reassignment
reconfigure, which calls
back into the rank manager and resets it.
The gate's own cost is the remaining O(N) here: building the id set
measures 1.9 ms at
16,384 replicas, ~4.5% of the optimized loop. #64910, stacked on this
PR, replaces the
set comparison with a `ReplicaStateContainer` mutation-version integer,
which removes it.
### What
- Extract the pass into `_maybe_check_rank_consistency()`.
- Gate it on the set of active replica ids: if the set is unchanged
since the pass last
ran, skip it.
- Only record a membership as checked when the pass did not swallow an
error.
`RAY_SERVE_FAIL_ON_RANK_ERROR` is off by default, so the pass can catch
an exception and
return `[]`; caching that would mean a stable-but-inconsistent
deployment never retries.
`DeploymentRankManager` now records whether the last rank op errored.
- The existing guards are preserved verbatim: only when the deployment
is HEALTHY, and
never while STARTING replicas exist (the node-migration case documented
in the original
comment).
### Why this is safe
Rank consistency can only be violated by a membership change — replicas
added, removed, or
replaced — and every such change alters the active id set, so the pass
still runs on
exactly the ticks where it can find work. Rank release and removal from
`_replicas` happen
in the same `pop(states=[STOPPING])` branch, so the id set and the rank
table cannot drift
apart across ticks. The per-node and node-rank passes group off
`_replica_to_node`, which
is only mutated by rank assign/recover/release, so a fixed replica-id
set implies a fixed
node grouping.
Set comparison is exact — there is no fingerprint collision to reason
about. The residual
cost is the O(N) set construction itself (~1.9 ms/tick at 16K replicas);
the follow-up
#64910 replaces it with an exact `ReplicaStateContainer` mutation
counter, making the gate
O(1).
### Results
- Follow-up profile of the same 16K workload: the rank pass no longer
appears in
controller CPU samples; report ingest resumes (stale-report drops stop).
- Composite (with the rest of the optimization stack): 16K steady-state
control loop
352.6 ms → 42.9 ms.
### Testing
- 6 new unit tests. Four over the gate itself: runs once then skips on
stable
membership; re-runs on membership change; a swallowed rank error is not
cached and
retries next tick; skips entirely (without recording a membership) while
STARTING
replicas exist. Two over a real
`DeploymentRankManager(fail_on_rank_error=False)` —
the production default — covering that a swallowed error is not cached,
and that the
error flag is read before `_reconfigure_replicas_with_new_ranks` can
clear it.
- Full `test_deployment_state.py` suite green (237 tests).
<img width="1248" height="728" alt="perf_A6_loop_haproxy_on"
src="https://github.com/user-attachments/assets/34758e31-6c04-46f9-958c-d6b34d3eae32"
/>
Signed-off-by: john.taylor <john.taylor@anyscale.com>
Co-authored-by: Claude <noreply@anthropic.com>1 parent 286886c commit 5d4b3bb
2 files changed
Lines changed: 139 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2934 | 2934 | | |
2935 | 2935 | | |
2936 | 2936 | | |
| 2937 | + | |
2937 | 2938 | | |
2938 | 2939 | | |
2939 | 2940 | | |
| |||
4972 | 4973 | | |
4973 | 4974 | | |
4974 | 4975 | | |
4975 | | - | |
| 4976 | + | |
| 4977 | + | |
| 4978 | + | |
| 4979 | + | |
| 4980 | + | |
| 4981 | + | |
| 4982 | + | |
| 4983 | + | |
| 4984 | + | |
| 4985 | + | |
| 4986 | + | |
| 4987 | + | |
4976 | 4988 | | |
4977 | | - | |
4978 | | - | |
| 4989 | + | |
4979 | 4990 | | |
4980 | 4991 | | |
4981 | 4992 | | |
4982 | 4993 | | |
4983 | | - | |
| 4994 | + | |
4984 | 4995 | | |
4985 | | - | |
4986 | | - | |
4987 | | - | |
4988 | | - | |
| 4996 | + | |
| 4997 | + | |
| 4998 | + | |
| 4999 | + | |
| 5000 | + | |
| 5001 | + | |
| 5002 | + | |
| 5003 | + | |
| 5004 | + | |
| 5005 | + | |
4989 | 5006 | | |
4990 | | - | |
4991 | | - | |
4992 | | - | |
| 5007 | + | |
| 5008 | + | |
| 5009 | + | |
| 5010 | + | |
4993 | 5011 | | |
4994 | 5012 | | |
4995 | 5013 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10585 | 10585 | | |
10586 | 10586 | | |
10587 | 10587 | | |
| 10588 | + | |
| 10589 | + | |
| 10590 | + | |
| 10591 | + | |
| 10592 | + | |
| 10593 | + | |
| 10594 | + | |
| 10595 | + | |
| 10596 | + | |
| 10597 | + | |
| 10598 | + | |
| 10599 | + | |
| 10600 | + | |
| 10601 | + | |
| 10602 | + | |
| 10603 | + | |
| 10604 | + | |
| 10605 | + | |
| 10606 | + | |
| 10607 | + | |
| 10608 | + | |
| 10609 | + | |
| 10610 | + | |
| 10611 | + | |
| 10612 | + | |
| 10613 | + | |
| 10614 | + | |
| 10615 | + | |
| 10616 | + | |
| 10617 | + | |
| 10618 | + | |
| 10619 | + | |
| 10620 | + | |
| 10621 | + | |
| 10622 | + | |
| 10623 | + | |
| 10624 | + | |
| 10625 | + | |
| 10626 | + | |
| 10627 | + | |
| 10628 | + | |
| 10629 | + | |
| 10630 | + | |
| 10631 | + | |
| 10632 | + | |
| 10633 | + | |
| 10634 | + | |
| 10635 | + | |
| 10636 | + | |
| 10637 | + | |
| 10638 | + | |
| 10639 | + | |
| 10640 | + | |
| 10641 | + | |
| 10642 | + | |
| 10643 | + | |
| 10644 | + | |
| 10645 | + | |
| 10646 | + | |
| 10647 | + | |
| 10648 | + | |
| 10649 | + | |
| 10650 | + | |
| 10651 | + | |
| 10652 | + | |
| 10653 | + | |
| 10654 | + | |
| 10655 | + | |
| 10656 | + | |
| 10657 | + | |
| 10658 | + | |
| 10659 | + | |
| 10660 | + | |
| 10661 | + | |
| 10662 | + | |
| 10663 | + | |
| 10664 | + | |
| 10665 | + | |
| 10666 | + | |
| 10667 | + | |
| 10668 | + | |
| 10669 | + | |
| 10670 | + | |
| 10671 | + | |
| 10672 | + | |
| 10673 | + | |
| 10674 | + | |
| 10675 | + | |
| 10676 | + | |
| 10677 | + | |
| 10678 | + | |
| 10679 | + | |
| 10680 | + | |
| 10681 | + | |
| 10682 | + | |
| 10683 | + | |
| 10684 | + | |
| 10685 | + | |
| 10686 | + | |
| 10687 | + | |
| 10688 | + | |
| 10689 | + | |
| 10690 | + | |
| 10691 | + | |
| 10692 | + | |
| 10693 | + | |
| 10694 | + | |
| 10695 | + | |
| 10696 | + | |
| 10697 | + | |
10588 | 10698 | | |
10589 | 10699 | | |
0 commit comments