Routing LLM Inference in Production: From Engine Signals to Policy — Qianru Lao & Lu Zhang, OpenAI
AAI Engineer
Internet TechnologyComputing/Software
Transcript
00:00:00- Hi, everyone, thanks for joining our talk.
00:00:14I'm Lu, and this is my colleague, Chen Ru.
00:00:17So, today we're gonna talk about the,
00:00:20we both work on the inference team at OpenAI,
00:00:23and today we are going to talk about
00:00:24routing IIM inference in production.
00:00:27Specifically, how our system evolved
00:00:29from routing based on feedback loops
00:00:32driven by engine signals to a more explicit
00:00:35and a predictable policy,
00:00:37which is still informed by engine signals.
00:00:39However, it's more like the way we use it is different.
00:00:43So, for the agenda today,
00:00:44we're gonna begin by introducing
00:00:46the inference load balancer,
00:00:48what it is, what it does, and how it has evolved.
00:00:51And then, Chen Ru will walk us through
00:00:53the newer control plane and the data plane
00:00:56driven architecture, what are the responsibilities of each
00:00:59and followed by a concrete case study
00:01:01of how we reduce the global network overhead.
00:01:05And in the end, I return to discuss
00:01:07the protection mechanisms that help keep the system
00:01:10stable under production level stress.
00:01:13So, to begin with, what is the inference load balancer?
00:01:17And where it sits.
00:01:20So, this is a very high-level diagram of the system we are talking about.
00:01:25On the left-hand side are the front-end clusters.
00:01:29Those are the GPU clusters, sorry, those are the CPU clusters
00:01:33that act as gateways into our system.
00:01:36And they receive user requests, then prepare them into the inference request
00:01:42that can be processed by the inference engines.
00:01:44And on the right-hand side are the engine clusters,
00:01:47which are usually GPU clusters and each hosting multiple inference engines.
00:01:53So, that's why we got the name of engine clusters.
00:01:56And as you may have already heard, nowadays GPUs are pretty popular and expensive.
00:02:02So, sitting in the middle, it is the IRB or inference load balancer.
00:02:07It actually runs on the front-end clusters, but it's also a bridge into our inference stack.
00:02:14It has two main responsibilities: select an engine and approximate the request.
00:02:19For this talk, we are going to focus on the engine selection part.
00:02:23So, in some ways, IRB resembles a very traditional load balancer
00:02:28because a request usually targets a model, and a model is backed by multiple engines.
00:02:35They may live on different clusters in different regions or even across the continents
00:02:40because that gives us a good resiliency towards localized degradation or cluster failures.
00:02:46However, the inference stack or the uniqueness of the inference introduces a lot of nuances,
00:02:53like it has to consider a bunch of signals reported in real time,
00:02:59like the well-known time-to-first token, TTFT, time-between-output tokens,
00:03:05also known as token-throughput or time-between tokens,
00:03:08and other harshness and utilization signals.
00:03:11Besides, there's an important concept of a Kiwi cache, which is also well-known.
00:03:16But, for example, when the conversation already has a lot of the useful context cached in one engine,
00:03:22sending the follow-up turns of the same conversation back to the same engine
00:03:27will avoid recomputation, improve efficiency, and reduce latency.
00:03:32So the combination of performance, reliability, locality, cache awareness is what makes it such an interesting problem.
00:03:39So how we attempted in the problem, let's take a look at the early days.
00:03:45And to be honest, early days in this industry sounds a lot more historic than it really is.
00:03:51And the routing process at that time began with a filtering of, like, each request may not be served by all the engines
00:04:01because of constraints such as capabilities or geo-restrictions due to compute or data residency.
00:04:08And among the remaining engines, IRB used a weighted consistent hashing to select the best destination engine for a request for a certain user.
00:04:19Then the important question becomes, where are the weights come from?
00:04:24So they were generated by a periodic feedback loop.
00:04:27The inference engines, as mentioned earlier, reports all kind of the signals we care about.
00:04:33And the controller will periodically smooth out those signals and compute a performance score.
00:04:40The performance score then will be compared against the fleet average.
00:04:45Then the weight will be adjusted basically for each engine if the weight goes up,
00:04:52if the performance is better, or it goes down when the performance is worse than the fleet average.
00:04:57And this generated weight will impact the routing and then it's basically a control loop.
00:05:05Conceptually, it's very similar to the PID controller.
00:05:10And no, this PID controller will not help you cure a Linux process.
00:05:13But instead, it's a classic control theory technique that continuously steering the system towards its desired state.
00:05:20And we just borrowed this important concept, the proportional part of it, and applied it into the system.
00:05:27So it has a lot of nice properties.
00:05:33For example, it could combine the useful signals we care about into the single routing decision.
00:05:40And because it adapts to the observed performance, as we mentioned earlier, there's a lot of constraints.
00:05:45And those constraints might have some engines basier because they can serve more kind of requests than the remaining.
00:05:53But those basier signals will be fit into the next loop and resulting in the less constrained request that can go to more of those kind of engines.
00:06:03So basically, they're self-balanced out.
00:06:05And to some extent, this just means we don't need to do a lot of manual intervention.
00:06:11And it just works.
00:06:12However, that kind of adaptability comes with big trade-offs.
00:06:16Because of the same reason that it combines so many signals, it's also very hard to reason about
00:06:22a particular routing decision, or why such engines get higher weight than we expect.
00:06:28And every time we want to fine-tune towards some aspect, it's almost impossible to not impact something else.
00:06:36And the load is not always very evenly distributed, because sometimes a model is served by engines on different GPU skills,
00:06:48and they have different characteristics, then the problem becomes a lot more trickier.
00:06:53And the feedback loop sometimes creates bad oscillations, because when you shift an engine away from traffic,
00:07:03the engine turns a bit cooler, and this signal gets fit to the controller.
00:07:08The controller now thinks, "Hey, this engine can take a lot more traffic."
00:07:12Then some traffic is going to be shifted back and forth between a few engines and disrupting the KVCache utilization.
00:07:20So all those limitations motivated us to rethink about the architecture and see if we have new ways to address the problem.
00:07:29So I'm going to hand it over to Qianru to deep dive into the new architecture we tried out.
00:07:38Yeah, thank you, Lu.
00:07:40So I'm going to talk about the architecture of the load balancer, and how do we reduce the overall overhead with our routing algorithm.
00:07:50The load balancer answers one question.
00:07:53So for each request from a CPU cluster, which engine should serve it?
00:07:59One most naive baseline might be round robin, which send requests across engine evenly.
00:08:06But if you think a little bit more, that doesn't make sense.
00:08:09Because engines are not homogeneous, they can have different hardware and capacity,
00:08:15different health, and also different distance from CPU cluster.
00:08:19Also, run robin could break cache locality.
00:08:24Related requests that could reuse the same engine cache might be sent to different engines.
00:08:30A probably better solution might be for each CPU cluster, it choose the best engine from its own local view.
00:08:39But that's not enough either.
00:08:41Think about one extreme case, multiple CPU cluster route traffic to the same engines independently,
00:08:48which could overload that engine while leave other engines underutilized.
00:08:53So what we need is a globally optimized solution.
00:08:57A control plan that has a global view for all the CPU cluster and GPU engines,
00:09:02and could compute a globally optimized routing answers.
00:09:06And the data plane can make a routing decision quickly based on the answer pulled from the control plane.
00:09:18Now, let's look inside the control plane and data plane.
00:09:22In the data plane, there is an engine selector, which selects engine for each request.
00:09:28It reads the local routing state, which includes the candidate engines and the routing weights for each candidate engine.
00:09:37Both of them are refreshed asynchronously in the background.
00:09:41So we don't need to ask the control plane before we make a routing decision for each request.
00:09:48Also, the data plane collects real-time engine signal, such as number of ready replicas, engine house, etc.,
00:09:56to service fast local guardrails.
00:10:00In the control plane, the data loader combines those live engine signals and never overhead.
00:10:06And with offline regressions of capacity, TDFT, and TBOT, the optimizer could turn those data
00:10:14into routing weights into routing weights.
00:10:16And the control plane will publish the routing weight for each data plane to pull.
00:10:21In this way, no request need to wait on the data plane.
00:10:26The control plane continuously computes the next globally optimized routing weight snapshot,
00:10:32while the data plane makes a routing decision based on the latest snapshot already installed locally.
00:10:42In summary, there are three important paths through the system.
00:10:46The first path is the inference request path.
00:10:49The request arrives to the CPU cluster, and the data plane inside the CPU cluster will select
00:10:55engine for that request based on the local routing state, and forward the request to the selected engines.
00:11:03The second path is the engine signal path.
00:11:06The system continuously collects real-time engine signal, such as TTFT, TBOT, number of ready replica,
00:11:15and engine house, etc.
00:11:17Both planes need those real-time engine signals.
00:11:20The control plane need them to compute a globally optimized routing weight, while the data plane need
00:11:26them to service fast local guardrails.
00:11:29And the third path is the routing weight path.
00:11:32The control plane compute and publish the routing weight, and the data plane
00:11:38pull the updates to its local cache.
00:11:41So only the first path is synchronous, but it's fast and only local inside the data plane of the CPU cluster.
00:11:49The other two loops are asynchronous loop, and they are to improve future routing decision.
00:11:59So that's pretty much of the architecture part, but that still leaves one question.
00:12:04How do we compute those routing weights?
00:12:10But before answering that question, let's answer another question first.
00:12:15Why not just send a request to the nearest engine?
00:12:20That's because the traffic demand and GPU capacity are not geographically balanced.
00:12:27For example, in Region 1, CPU cluster A send 90 RPS, and the nearby engine A can serve 100 RPS.
00:12:36So in this case, nearest-release only is fine.
00:12:40While in Region 2, CPU cluster B send 120 RPS, and the nearby engine B could only serve 100 RPS.
00:12:49So in this case, if we insist on keeping everything local, the extra 20 RPS need to wait on an overloaded
00:12:57engine B. While in Region 3, we are only using 40 RPS of an 80 RPS engine C.
00:13:05That still leaves 40 RPS spare.
00:13:08So if we send the extra 20 RPS from cluster B to Engine C, that will add network distance.
00:13:16But it could also avoid a probably much larger engine-side waiting time.
00:13:22So in this case, a further engine might be faster end-to-end.
00:13:27That's why we need something better than the nearest-only routing.
00:13:34Now let's open the black box of the optimizer.
00:13:39The optimizer accepts four types of input.
00:13:44The request from each CPU cluster, the network latency to each engine,
00:13:50the available engine capacity and health, and also the TTFT/TPOT latency profiles.
00:13:57That tells us how's the engine-side latency change as the load increases.
00:14:04And with those inputs, the optimizer turn the input to the output routing weights.
00:14:10The routing weights say for each CPU cluster, what fraction of its traffic should go to each GPU engine.
00:14:19And the optimization goal is straightforward.
00:14:22It's to minimize the expected end-to-end latency across all routing traffic.
00:14:28The important part is that the end-to-end latency includes both the network distance and the engine-side latency.
00:14:36That means a nearby engine might be attractive when it still has room to serve traffic.
00:14:43While a further engine might be better if all the nearby engines are close to full.
00:14:50And the optimizer also needs to respect several hard constraints.
00:14:56First, it needs to route all the traffic demand.
00:14:59Second, it needs to ensure all the engines stay within the effective capacity.
00:15:05Third, it needs to keep the routing weights non-negative.
00:15:09With this, the control plane get the routing weights from the optimizer and publish them.
00:15:17And the data plane pull them and use them to make a globally optimized routing decision.
00:15:24And that's pretty much of my part.
00:15:26And Lu will continue to talk about the protection mechanisms in the system.
00:15:33Thanks, Chenru.
00:15:34So as AI engineers, we all kind of know that production in many cases are not behaving in the most ideal case.
00:15:45So clusters can fill, GPUs or individual nodes can degrade, and networking can just get to all kind of mysterious issues.
00:15:55So how do we keep our production system healthy as much as possible under the heavy load?
00:16:02The first thing we have is penalties.
00:16:05Basically, when an engine is an outlier, we detect the abnormally and try to reduce the routing weight to that engine.
00:16:13In that way, we give it a chance to either recover by themselves if there's some transient issue,
00:16:22or we can have a human intervene to rotate it out or replace the faulty hardware.
00:16:28And secondly, the retries, which is a very common technique used to mitigate problems.
00:16:34However, during some cases, it actually could make them even worse.
00:16:39Like when the system is very close to like a tip over or very heavily utilized.
00:16:45Retries will send more load.
00:16:47And this more load will cause more failures and cause more retries, which is an infamous retries storm.
00:16:52So we implemented caps or budget to constantly retries into an acceptable region.
00:17:00And this is actually even need to be dynamic because in the happy time or in the normal time,
00:17:07we can tolerate a lot more retries than when the system are heavily utilized.
00:17:12And finally, we have the load shedding, which is our last result when the production capacity
00:17:20couldn't meet the increasing amount of inference demands.
00:17:24So we instead will try to have all the system fill.
00:17:27We basically proactively load shed a portion of the traffic to have the system degrade gracefully.
00:17:35So that pretty much concludes our talk today.
00:17:38And thanks for joining us.
00:17:40Both of us will be around in our booth area this afternoon.
00:17:45So if you have further questions, feel free to walk to the area and chat with us.
00:17:50Thank you.
00:17:51Thank you.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video