Skip to content

Commit 5090fae

Browse files
committed
Start latency outline
Signed-off-by: Mike Spreitzer <mspreitz@us.ibm.com>
1 parent 4df17ba commit 5090fae

1 file changed

Lines changed: 284 additions & 0 deletions

File tree

docs/latency-outline.md

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# Latency Outline
2+
3+
Well, really it is another outline of the whole thing, but laser
4+
focused on latency (since the whole point is good latency).
5+
6+
## Wake_up case
7+
8+
We have a roadmap of stages of development that is still not fully
9+
agreed. For the sake of concreteness, I will focus here on one
10+
proposed version of one stage of development. It is part of the
11+
following scenario description.
12+
13+
- Time to first token, as measured by inference client (IC).
14+
15+
- Scale up from zero, with some not-entirely-clear-to-me pre-routing
16+
component that can queue requests. I am going to pretend that
17+
inference request queuing, dispatching, and routing are part of
18+
something called inference scheduling (IS). I am not sure whether
19+
everybody else means all these things when they use this term.
20+
21+
- Some auto-scaler (AS) that reacts to signals from the inference
22+
scheduler and other information and responds by updating a
23+
Prometheus metric that it offers and that stipulates the replica
24+
count for each model variant.
25+
26+
- Some replica count relay (RCR) that rapidly scrapes the
27+
aforementioned metric from the AS and syncs those values to the
28+
replica count of each variant's ReplicaSet. For concreteness and
29+
simplicity I suppose here that each variant is deployed as a
30+
ReplicaSet of server-requesting Pods.
31+
32+
- The dual-pod design described in
33+
https://github.com/lionelvillard/llm-d-fast-model-actuation/blob/7dc4f0b820d62018769284fe99c21542ca9f4720/docs/dual-pods.md
34+
(which is the current version of
35+
https://github.com/lionelvillard/llm-d-fast-model-actuation/pull/2,
36+
which itself is a proposed modificaion of
37+
https://github.com/llm-d-incubation/llm-d-fast-model-actuation/pull/15)
38+
as the "Sleep/wake only" stage of development.
39+
40+
- The inference request is for a variant with some sleeping instances
41+
and no awake instances.
42+
43+
- Client already has a TLS connection open to the Inference Gateway (IGW).
44+
45+
Now, on to the latency summary.
46+
47+
- Client sends inference request to the IGW.
48+
49+
- IGW receives request, parses it, and delegates it (i.e., sends a
50+
similar request) to the IS.
51+
52+
- IS receives the request, parses it, and realizes that the
53+
InferencePool for that request currently has zero available
54+
servers. IS starts some process of getting this "pool miss"
55+
interrupt to the AS.
56+
57+
- The pool miss interrupt travels from the IS to the AS.
58+
59+
- The AS does some level analysis and planning, updating its offered
60+
Prometheus metrics.
61+
62+
- The RR gets around to scraping the AS.
63+
64+
- The RR processes the latest metrics and consequently sends a request
65+
to a kube-apiserver to update the replica count of the relevant
66+
ReplicaSet.
67+
68+
- The kube-apiserver does its thing to process that request. This
69+
includes doing an etcd transaction. In an OpenShift cluster there
70+
are extra steps. The kube-apiserver eventually (the word
71+
"eventually" is here because this could take a while; but I should
72+
report that on a lightly loaded plain kube cluster it is not
73+
uncommon for this event to be received before the client gets the
74+
response to the request) sends an update event to the ReplicaSet
75+
controller, which has an
76+
[informer](https://github.com/kubernetes/client-go/blob/v0.34.1/tools/cache/shared_informer.go#L40-L237)
77+
watching the ReplicaSets.
78+
79+
- The ReplicaSet informer receives the update event and puts a
80+
reference to the ReplicaSet into the controller's work
81+
queue. Eventually a worker goroutine dequeues that reference and
82+
works on that ReplicaSet. Last time I looked, this involved doing a
83+
query to the kube-apiserver to get a current list of members of the
84+
set. The worker decides that there are not enough members in the
85+
set, and sends to the kube-apiserver a request to create another
86+
member (in our scenario, this is the server-requesting Pod).
87+
88+
- The kube-apiserver does its thing with this Pod creation
89+
request. This includes doing an etcd transaction. In an OpenShift
90+
cluster there are extra steps. The kube-apiserver eventually sends a
91+
create event to the kube-scheduler.
92+
93+
- The kube-scheduler does its thing --- which is very extensible. I am
94+
not sure of what to suppose here. In the best case this involves no
95+
interactions with other components. Eventually the scheduler decides
96+
which Node should run the new Pod and sends a request to the
97+
kube-apiserver to make that binding.
98+
99+
- The kube-apiserver does its thing with this Pod update request. This
100+
includes doing an etcd transaction. In an OpenShift cluster there
101+
are extra steps. The kube-apiserver eventually sends a Pod update
102+
event to the kubelet that will run the Pod.
103+
104+
- The kubelet receives the Pod update event and begins working on
105+
running the Pod. This includes stuff that depends on details about
106+
volumes and container images. Let us suppose the best case, in which
107+
the stub/requester container image is already on the node and there
108+
is no waiting on anything for volume creation/binding. The kubelet
109+
constructs the Pod, including the stub/requester container, and
110+
starts the main process inside that container. The kubelet sends a
111+
Pod update request to the kube-apiserver to update the status of the
112+
Pod to reflect that it has begun running.
113+
114+
- The kube-apiserver does its thing with this Pod update request. This
115+
includes doing an etcd transaction. In an OpenShift cluster there
116+
are extra steps. The kube-apiserver eventually sends a Pod update
117+
event to the dual-pod controller, which has an informer on
118+
server-requesting Pods.
119+
120+
- The dual-pod controller receives this event and enqueues a reference
121+
to the server-requesting Pod. Eventually a worker goroutine dequeues
122+
that reference and begins working on that Pod. The worker sends a
123+
request to the stub/requester to get the assigned GPUs.
124+
125+
- In the meantime, the stub/requester has started running and
126+
determined what GPUs are assigned to it. We are not guaranteed that
127+
this will be done before the (first) request for that set is
128+
received, but this is a plausible case to focus on first.
129+
130+
- The stub/requester receives the request for its assigned GPU set,
131+
already knows the answer, and sends the response.
132+
133+
- The dual-pod controller worker goroutne that sent the GPU assignment
134+
request receives the reply (here I suppose that this worker uses
135+
simple straight line code for this request/response interaction;
136+
that is not the only possible choice, but will analyze this approach
137+
first).
138+
139+
- That same goroutine determines that the server request can be
140+
answered by waking a sleeping vLLM instance, and sends that
141+
`/wake_up` request to the vLLM instance.
142+
143+
- The vLLM instance receives the `/wake_up` request and handles it,
144+
eventually sending a response.
145+
146+
- The dual-pod controller worker goroutine that sent the `/wake_up`
147+
request was waiting synchronously for the response (again, this is
148+
not the only possible choice, just the first one that I will
149+
analyze) and eventally receives it. This goroutine next sends the
150+
POST request to the stub/requester that conveys the URL to poll for
151+
inference server readiness.
152+
153+
- The stub/requester receives the POST request conveying the URL to
154+
poll for readiness. The stub/requester the sends the response, and
155+
then sends the readiness request to that URL.
156+
157+
- The vLLM instance receives that readiness request. In this case it
158+
is certainly ready, because `/wake_up` does not return until the
159+
instance is ready. The instance sends a positive response for the
160+
readiness query.
161+
162+
- The stub/requester receives the positive POST reply and records that
163+
the inference server is now ready.
164+
165+
- The kubelet running the server-requesting Pod eventually gets around
166+
to polling the stub/requester container for readiness. A positive
167+
response is sent.
168+
169+
- The kubelet receives the positive response and then sends to the
170+
kube-apiserver a request to update the server-requesting Pod's
171+
status.
172+
173+
- The kube-apiserver does its thing with this Pod update request from
174+
the kubelet (reporting readiness). This includes doing an etcd
175+
transactions. In an OpenShift cluster there are extra steps. The
176+
kube-apiserver eventually sends a Pod update event to the IS, which
177+
has been waiting for somewhere to send the inference request.
178+
179+
- Eventually the IS sees that the InferencePool now has a ready
180+
member, and delegates the inference request to that member (the vLLM
181+
instance).
182+
183+
- The vLLM instance receives the inference request and starts working
184+
on it. Eventually the first token is sent back to the IS.
185+
186+
- The IS receives the first token and returns it to the IGW.
187+
188+
- The IGW receives the first token and returns it to the inference
189+
client.
190+
191+
## Server create case
192+
193+
This is like the wake_up case except that there are no sleeping
194+
replicas, but the model has been staged to the chosen Node and the
195+
shared torch.compile cache is hit. We suppose that in order to make
196+
GPU memory room for the new vLLM instance, a sleeping one will have to
197+
be deleted.
198+
199+
The latency summary shares a common prefix and suffix; the following
200+
shows the middle that differs.
201+
202+
- Instead of using `/wake_up`, the dual-pod controller goroutine
203+
working on the server-requesting Pod determines that it needs to
204+
create a new server-running Pod and that before doing that a
205+
sleeping instance has to be deleted. This goroutine sends to the
206+
kube-apiserver a request to delete the chosen server-running Pod.
207+
208+
- The kube-apiserver does its thing with the request to delete that
209+
Pod. This includes an etcd transaction. Note that this only _starts_
210+
the process of deletion (by updating a `.metadata` field of the
211+
Pod). The kube-apiserver returns a response to the deletion request
212+
and eventually sends a Pod update event to the kubelet. The dual-pod
213+
controller worker goroutine receives the reply to the deletion
214+
request and considers its job done.
215+
216+
- The kubelet receives the Pod update event and tears down the actual
217+
running pod. This is a non-trivial process, which I will mostly skip
218+
for brevity. I will note one thing: this includes telling the
219+
container runtime to kill the inference server container, which
220+
includes getting the OS to terminate the inference server; this in
221+
turn may involve signal handling by the inference server (I am not
222+
sure what vLLM actually does here). Once the pod is fully torn down,
223+
the kubelet sends a request to the kube-apiserver to update the Pod,
224+
specifically to remove the kubelet's
225+
[finalizer](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/)
226+
(the presence of that finalizer has prevented the kube-apiserver
227+
from completing the deletion of the Pod).
228+
229+
- The kube-apiserver does its thing with this Pod update request. This
230+
particular update is the request that gets the Pod object fully
231+
deleted. This includes doing an etcd transaction. In an OpenShift
232+
cluster there are extra steps. The kube-apiserver eventually sends a
233+
Pod deletion event to the dual-pod controller.
234+
235+
- When the dual-pod controller gets the server-running Pod deletion
236+
event and enqueues a reference to that Pod. Eventually a worker
237+
gorouine dequeues that reference and works on that Pod. The worker
238+
sends to the kube-apiserver a request to create the new
239+
server-running Pod.
240+
241+
- The kube-apiserver does its thing with this Pod creation
242+
request. This includes doing an etcd transaction. In an OpenShift
243+
cluster there are extra steps. The kube-apiserver eventually sends a
244+
response back to the dual-pod controller and eventually sends Pod
245+
creation event to the relevant Node's kubelet.
246+
247+
- The dual-pod controller worker goroutine receives the Pod creation
248+
response. This does not yet show the IP address of the Pod (the
249+
kubelet will assign that). The worker goroutine notes the
250+
association between serve-requesting Pod and server-running Pod and
251+
is done.
252+
253+
- The kubelet receives the Pod creation event and begins creating the
254+
actual server-running pod. Again, let us suppose no delay for image
255+
pulling or volume creation/binding. The kubelet gets the inference
256+
server container created and the server's main process starts
257+
running. The kubelet sends a request to the kube-apiserver to update
258+
the Pod's status (including setting the Pod's IP address).
259+
260+
- The kube-apiserver does its thing with this Pod update request. This
261+
includes doing an etcd transaction. In an OpenShift cluster there
262+
are extra steps. The kube-apiserver eventually sends a Pod update
263+
event to the dual-pod controller.
264+
265+
- The dual-pod controller receives the server-running Pod update event
266+
and enqueues a reference to the server-requesting Pod. Eventually a
267+
worker goroutine dequeues that reference and works on it. This
268+
goroutine knows that the vLLM instance is not sleeping (how?
269+
probably because this same controller process previously requested
270+
the instance to be created and did not request it to sleep). This
271+
worker sends the POST request to tell the stub/requester what URL to
272+
poll for readiness.
273+
274+
- The stub/requester receives the POST request conveying the URL to
275+
poll for readiness. The stub/requester the sends the response, and
276+
then tries to send the readiness request to that URL.
277+
278+
- The vLLM instance may or may not even have the relevant port open
279+
yet. If the port is open, the instance may or may not be ready
280+
yet. In the unhappy cases, the stub/requester gets a failure or
281+
negative response, and waits a while and then tries again.
282+
283+
- Eventually the vLLM instance receives that readiness reques and
284+
sends a positive response.

0 commit comments

Comments
 (0)