Repository navigation
Serve the engine's identity and start time on GET /engine - #5
Conversation
A client that keeps state tied to one engine process, such as a
prefill/decode router, has to notice when that process is replaced. The
kubelet restarts the engine container in place, so the pod, its IP and its
endpoints do not change and there is nothing to watch from outside.
The sidecar already scrapes the engine every poll, so it now reports which
instance it sees: {"engine_id", "started_at", "source"}, or 503 before the
engine has been identified. engine_id changes exactly when the engine
process is replaced. It comes from vLLM's process_start_time_seconds (an
exact start time) or from sglang's startup_* gauges, which are durations set
once per process and work as a fingerprint that survives sidecar restarts.
With neither, the sidecar mints an id, which does not.
A restart seen this way also resets the verdict state machine, re-arming
the warm-up grace period. Counter regression alone misses an engine that was
idle, or whose new process outran the old count within one poll.
|
Tested
While a restarted engine is still loading, Script: real engines on the test cluster, run by hand (not part of CI). |
…seen on /engine restarts counts engine restarts seen since watcher_started_at. It resets with the sidecar, so engine_id stays the field to compare; the two make the endpoint readable on its own when debugging.
… it is weak - A startup series missing from a scrape no longer counts as an sglang restart. Phases are reported late during startup, and a /metrics body cut off at the read limit loses its tail; treating either as a new process would also reset the hang verdict state and re-arm the warm-up period. Only a series seen before with a different value is a new process, and late phases join the remembered set so a later change in them is caught. - Log the identity source when it changes, with a warning for observed. It is what vLLM falls back to with --api-server-count > 1, whose multiprocess metrics registry has no process collector; README says so. - Attach noteEngine's doc comment to the function.
|
Re-tested with
The latest commit (802aab3, review fixes: missing startup series no longer read as a restart, a warning when the identity falls back to |
…e engine restart 1f0d236 taught the pd-ds router's drift probe to fire on engine /health so the router restarts the moment a newly-scheduled engine is actually usable, and no earlier. That probe keys on *IP set growth*. It misses the case where an engine's IP is unchanged but the process behind it was replaced. That case is real on vllm + mooncake: the router pulls each prefill's engine_id off its /query endpoint once at boot (vllm_pd_router.rs) and pins it into every kv_transfer_params. When the kubelet restarts a crashed engine in place, the pod keeps its IP, DNS shows no growth, but the engine's new process has a fresh engine_id -- the router keeps addressing a dead incarnation and transfers hang or fail. NIXL dodges this because its handshake is lazy per-transfer. The fix piggybacks on the hang-watcher sidecar that already runs on every engine pod. With modelsphere/hang-watcher#5 the sidecar serves GET /engine on its existing healthz port, returning {"restarts", "watcher_started_at", ...} where the pair is a stable identity of the engine process the sidecar is watching. No new RBAC, no router-version dependency, no engine-side changes required. Boot script (pd-ds.yaml): after DNS resolves the upstream IPs, also query each engine's /engine endpoint and record "<ip>|<restarts>|<watcher_started_at>" per line in the existing /run/pd-router/<role>-ips file. 404 on /engine = old sidecar without the endpoint, record "<ip>||" sentinel. 503 / conn refused / timeout = transient: retry up to 90min, then fall through to the same sentinel and proceed (fail-open, mirrors the DNS budget). The sentinel makes the runtime probe's answer "no identity info", identical to the hang-watcher-disabled render. Liveness probe (_pod.tpl): per tick, re-query /engine on each argv'd IP. Fires iff: - both sides returned a real tuple and they differ (in-place swap) - boot recorded sentinel but live returns real (late-arriving identity; restart so the router re-snapshots with identity) and the usual /health gate passes (mirrors 1f0d236: don't restart into a still-loading engine). hangWatcher.enabled=false renders HW_PORT=0 into the probe and boot script, short-circuiting every query to the sentinel: one-line switch, single code path. Old-sidecar compat: /engine 404s, boot records sentinel, liveness never has a live real tuple to compare against -> drift probe degrades to pre-patch behavior (IP growth only), still useful. Chart versions: sglang 0.9.17 -> 0.9.18, vllm 0.8.16 -> 0.8.17.
What
A new endpoint on the sidecar's existing port:
restartscounts the engine restarts this sidecar has seen sincewatcher_started_at(its own start); it resets with the sidecar, soengine_idremains the field a client should compare.503until the engine has been identified, so "not known yet" is never read as an id.Why
A client that keeps state tied to one engine process -- a prefill/decode router holding transfer sessions with its workers -- needs to know when that process was replaced. The kubelet restarts the engine container in place: the pod, its IP and its endpoints stay the same, so there is nothing to watch from outside. The sidecar already scrapes the engine every poll, so it can say which instance it is looking at.
Where the identity comes from
sourcestarted_atprocess_start_timeprocess_start_time_secondsstartup_fingerprintstartup_time_seconds/startup_cuda_graph_time_seconds(>= 0.5.19)observedsglang publishes no start timestamp, only per-phase startup durations. They are set once per process and measured to the nanosecond, so they work as a fingerprint. They appear over several scrapes during startup, so new series joining is the same process; only a changed value or a vanished series is a new one. Consumers should compare
engine_id, notstarted_at.Reading the engine process directly (the way
psdoes) is not possible: containers in a pod have separate pid namespaces.shareProcessNamespacewould allow it but makes the pause container pid 1, which breaks charts whose preStop signals pid 1 and kills everything else it can see.Behaviour change
A restart seen this way resets the verdict state machine (fresh baseline, warm-up grace period re-armed). Counter regression, the only restart signal so far, misses an engine that was idle (counters 0 before and after) or whose new process outran the old count within one poll.
Testing
smoke_engine_test.sh(added to CI): the real binary against a fake engine, for both sources -- 503 before the first scrape, id changes on engine restart, same id after a sidecar restart.