|
| 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