Routing LLM Inference in Production: From Engine Signals to Policy — Qianru Lao & Lu Zhang, OpenAI

Read the talk

Routing LLM Inference in Production: From Engine Signals to Policy

Qianru Lao and Lu Zhang explain how OpenAI moved from feedback-driven routing weights to a global latency optimizer, while keeping engine selection local and protecting the fleet against failures, retry storms, and excess demand.

From a talk by Lu Zhang and Qianru Lao

At a glance

Ideas worth remembering

  • Inference routing must account for reusable conversation context as well as performance, health, and geography. Bouncing traffic between engines can undermine KV cache reuse.

  • Global coordination can stay outside request-time selection: the control plane computes weights asynchronously, and data planes select engines from locally cached snapshots with live guardrails.

  • Compare network latency plus engine-side delay when choosing a destination. A farther engine can serve a request sooner, provided the allocation respects its effective capacity.

  • Production needs separate controls for unhealthy engines, retry amplification, and insufficient total capacity: penalties, dynamic retry budgets, and last-resort load shedding.

An engine choice changes latency and future cache reuse

A follow-up conversation turn can become cheaper simply by returning to the engine that served it before. That engine may already hold useful context in its KV cache, avoiding recomputation and reducing latency. A destination choice therefore affects more than the distribution of incoming work: it determines whether previously computed context remains useful. OpenAI inference engineers Lu Zhang and Qianru Lao explain how their routing system evolved to handle performance, reliability, geography, and cache locality together. 0:12

The inference load balancer, or ILB, runs in front-end CPU clusters. These clusters receive user requests and prepare inference requests for engines, which usually run in GPU clusters containing multiple engines. ILB selects an engine and processes the request; this discussion concentrates on selection. A request typically targets a model backed by several engines, potentially spread across regions or continents. That distribution provides resilience against localized degradation and cluster failures.

Several signals describe different parts of the serving experience:

  • Time to first token (TTFT): The delay before generation produces its first token.
  • Time between output tokens (TBOT): The spacing between tokens once output is underway.
  • Health and utilization: Whether an engine is functioning and how busy it is.
  • KV cache locality: Whether the selected engine can reuse context from an earlier turn.

These considerations can pull routing in different directions. Returning to a cached conversation avoids work, while the engine's current health and load still affect how quickly it can serve that conversation. 2:41

Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

0:12 · section reference included

Weighted hashing worked—until the weights became hard to explain

The early routing design first filtered the engine set. Capabilities, compute restrictions, and data residency could prevent a request from using particular engines. Among the eligible destinations, ILB used weighted consistent hashing to select an engine for a request or user. The important policy question was upstream of selection: where did those weights come from? Zhang notes that early days in this industry sounds more historic than it really is. 3:41

A periodic controller turned engine signals into weights. It smoothed the reported signals, computed a performance score, and compared each engine's score with the fleet average. Better relative performance increased an engine's weight; worse relative performance decreased it. Those weights changed incoming traffic, which changed the signals observed in the next cycle. The design borrowed the proportional part of a PID controller. Zhang's aside: this PID will not help you kill a Linux process.

The loop had useful properties. Many signals could influence a single routing decision, and uneven eligibility could partly balance itself. An engine capable of serving a wider range of request types might become busier. Its signals would then discourage additional traffic that could go elsewhere, moving less constrained requests toward other eligible engines. This reduced the need for manual intervention.

The cost was coupled behavior. Combining many signals into one score made it difficult to explain why an engine received a particular weight. Tuning one aspect could move another. Engines serving the same model could also have different characteristics because they used different GPU SKUs, making even load distribution harder to reason about.

The most concrete failure was oscillation. Consider a busy engine holding useful conversation context. The controller moves traffic away, so the engine cools. Its improved signals then look like permission to send more traffic back. Repeating that sequence bounces requests between engines and disrupts KV cache utilization—the reuse that made returning to an engine valuable. 6:41

Why does the controller reverse its own action? The cycle below makes the relationship visible: moving traffic changes the engine's observed condition, and that changed condition drives the next routing adjustment. An improvement caused by removing work can become the reason to put that work back.

How it fits togetherRouting changes the signal that changes routing

Reported performance influences its routing weight.

Moving traffic away cools the engine; improved signals encourage traffic to return. Repeated movement between engines disrupts cache reuse.

Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

3:41 · section reference included

Compute policy globally, select engines locally

Lao begins the replacement architecture with one question: for a request arriving at a CPU cluster, which engine should serve it? Round robin ignores differences in hardware, capacity, health, and network distance. It can also scatter related requests across engines and lose cache locality. Letting each CPU cluster independently choose its locally preferred engine creates another problem: several clusters can converge on the same destination, overloading it while other engines remain underused. 7:38

The newer design gives a control plane a global view of CPU clusters and GPU engines. It computes routing weights for the fleet. A data plane in each CPU cluster uses those weights to make individual engine choices quickly. Its engine selector reads locally stored candidate engines and their routing weights; both refresh asynchronously in the background. Selecting an engine therefore does not require a synchronous call to the control plane.

Engine signals still matter, but their jobs become more explicit. The data plane uses live information such as ready replica counts and engine health as fast local guardrails. The control plane's data loader combines live signals with network overhead and offline regressions of capacity, TTFT, and TBOT. An optimizer turns these inputs into the next routing-weight snapshot, publishes it, and lets data planes pull the update into their local caches. 9:18

The architecture separates three paths:

  • Request path: A request arrives at a CPU cluster, the data plane selects an engine from local routing state, and the request is forwarded to that engine.
  • Signal path: Engine measurements feed both global optimization and local guardrails.
  • Weight path: The control plane publishes weights, and data planes pull them into local caches.

Only request-time selection is synchronous. Signal collection and weight updates improve future decisions asynchronously. Each request uses the latest snapshot already installed locally, while live engine signals provide a faster check on changing conditions.

Where does global coordination enter without delaying each engine choice? The diagram separates the request flow from the update flows. The optimizer affects selection through cached weights, rather than sitting between every incoming request and its destination. Engine signals also reach the data plane directly, allowing global planning and fast local protection to operate on different timescales.

How it fits togetherThree paths, one local engine choice

Arrives at a front-end CPU cluster.

Request-time selection reads local state. Engine signals and routing-weight snapshots travel asynchronously to improve subsequent choices.

Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

7:38 · section reference included

A farther engine can finish sooner

Before opening the optimizer, Lao tests a tempting shortcut: send each request to its nearest engine. This works when demand and nearby capacity line up. In region one, CPU cluster A sends 90 requests per second (RPS), and nearby engine A can serve 100 RPS. Keeping that traffic local fits within the engine's capacity. 12:10

Region two changes the outcome. CPU cluster B sends 120 RPS toward nearby engine B, which can serve only 100 RPS. Nearest-only routing sends more work to B than it can serve, creating engine-side waiting. A farther engine C has unused capacity. Sending some traffic there adds network latency but can avoid a larger delay at B.

Follow the change for the diverted traffic: its destination changes from B to C, its network journey becomes longer, and it avoids waiting at the overloaded local engine. The farther route can finish sooner when the added travel time is smaller than the engine-side delay it avoids. How much traffic can move depends on C's spare capacity. The example teaches the comparison without establishing a measured latency saving. 13:10

Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

11:59 · section reference included

Make the objective and capacity limits explicit

The optimizer receives four kinds of input:

  • Demand: Requests arriving from each CPU cluster.
  • Network latency: The cost of reaching each engine from each cluster.
  • Available capacity and health: How much traffic engines can effectively serve.
  • Latency profiles: TTFT and TBOT profiles describing how engine-side latency changes as load increases.

Together, these inputs let the policy compare the extra travel to engine C with the waiting created by keeping too much traffic at engine B. 13:35

The output is a set of routing weights: for each CPU cluster, what fraction of its traffic should go to each GPU engine? The objective is to minimize expected end-to-end latency across all routed traffic, counting both network latency and engine-side latency. A nearby engine is attractive while it has room; a farther engine can become preferable as nearby engines approach full utilization.

Three hard constraints keep the allocation meaningful:

  • Route all demand: Every cluster's traffic must receive an allocation.
  • Respect effective capacity: The traffic allocated to an engine must stay within what it can serve.
  • Keep weights non-negative: Traffic fractions cannot be negative.

In the region-two example, the capacity constraint prevents the optimizer from solving B's overload by creating another overload at C. Latency determines which feasible allocation is preferable.

This is the move from engine signals to policy. Signals continue to inform routing, but the control plane uses them within an explicit optimization problem and publishes weights for local selection. The solver and the way cache affinity enters the newer optimization are not specified here. Cache locality remains a routing concern, without a demonstrated cache term in the objective described in this talk.

Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

13:35 · section reference included

Protect the fleet when routing cannot fix the problem

A globally planned allocation still runs on a production fleet where clusters fail, GPUs and nodes degrade, and networking develops mysterious problems. Zhang closes with protections for three different failure mechanisms. Routing can redistribute work, but faulty hardware and insufficient total capacity require additional controls. 15:33

  • Outlier penalties: Detect an anomalous engine and reduce its routing weight. Less incoming traffic gives a transient problem room to recover or allows an operator to rotate the engine out and replace faulty hardware.
  • Dynamic retry budgets: Cap retries so failure recovery does not add unlimited work. Under heavy utilization, failures trigger retries, retries raise load, and the added load produces more failures. A budget interrupts that retry storm. It must tighten as utilization rises because the fleet can tolerate more retries during normal operation than near overload.
  • Load shedding: When production capacity cannot meet demand, proactively shed a portion of traffic as a last resort. This allows graceful degradation rather than letting excess work cause the whole system to fail.

Load shedding gives a practical limit to the optimizer's requirement to route all demand. An allocation that routes everything while keeping every engine within capacity requires enough available capacity. When that condition fails, the system must reduce the work it attempts to serve. Penalties reduce traffic to unhealthy engines, retry budgets constrain extra attempts, and load shedding removes a portion of incoming traffic. These controls address separate sources of pressure so the fleet can continue providing useful service under stress. 17:03

Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

15:33 · section reference included

Resources

Read the complete timestamped transcript
  1. 0:12

    Hi everyone. Thanks for joining our talk. I'm Lu, and this is my colleague, Qianru. So we- today we're gonna talk about the in, uh, we both work on the inference team at OpenAI, and today we are going to talk about routing LLM inference in production, specifically how our system evolved from routing based on feedback loops driven by engine signals to a more explicit and predictable policy, which is still informed by engine signals, however, it's more like, uh, the way we use it is different.

  2. 0:43

    So, uh, for the agenda today, we're gonna begin by introducing the inference load balancer, what it is, what it does, and how it has evolved. And then, Qianru will walk us through the newer control plane and the data plane-driven architecture, uh, what are the responsibilities of each, and followed by a concrete case study of how we reduce the global network overhead. And in the end, I return to discuss the protection mechanisms that help keep the system stable under production level

  3. 1:12

    stress. So to begin with, what is the inference load balancer and where does it sit? So this is a very high level, uh, diagram of the system we are talking about. On the left-hand side are the front-end clusters. Those are the GPU clusters, uh, sorry, those are the CPU clusters that act as gateways into our system, and they receive user requests, then prepare them into the inference request that can be

  4. 1:42

    processed by the inference engines. And on the right-hand side are the engine clusters, uh, which are usually GPU clusters, and each hosting multiple inference engines. So that's why we, they got the name of engine clusters. And as you may already heard, nowadays, GPUs are pretty popular and expensive. So, um, sitting in the middle, it is the ILB or inference load balancer. It actually runs on the front-end clusters, but is also a

  5. 2:12

    bridge into our inference stack. It has two main responsibilities, select an engine and processing the request. For this talk, we are going to focus on the engine selection part. So in some ways, ILB resembles a very traditional load balancer because a request usually targets a model, and a model is backed by multiple engines. They may live on different clusters in different regions or even across the continents, because that

  6. 2:41

    gives us a good resiliency towards localized degradation or cluster failures. However, the inference, uh, stack or the uniqueness of the inference introduces a lot of nuances, like, uh, it have to consider a bunch of signals reported in real time, like, uh, the well-known time to first token time between output tokens, also known as token throughput or time between tokens, and other healthiness and utilization signals.

  7. 3:11

    Besides, there's an important concept of a KV cache, which is also well known. But for example, when the conversation already has a lot of the useful context cached in one engine, sending the follow-up turns of the same conversation back to the same engine will avoid recomputation, improve efficiency and, uh, reduce latency. So the combination of performance, reliability, locality, cache awareness is what makes it such an interesting problem. Uh, so how we

  8. 3:41

    attempted in the problem, let's take a look at the early days. And to be honest, early days in, in this industry sounds a lot more historic than it really is. And the routing process at that time began with a filter of like, uh, each request may not be served by all the engines because of, uh, constraints such as capabilities or due, uh, restrictions due to compute or data residency. And among the remaining engines,

  9. 4:11

    ILB used a weighted consistent hashing to select the best destination engine for a request or, or for, for certain user. Then the important question becomes where are the weights come from? So they were generated by a periodic feedback loop. The inference engines, as mentioned earlier, uh, reports all kind of the signals we care about. And the controller will periodically smooth out those signals and compute a performance score.

  10. 4:41

    The performance score then will be compared against the fleet average. Then the weight will be adjusted basically for each engine as the weight goes up if the performance is better, or it goes down when the performance is worse than the fleet average. And this generated weight will impact the routing, and then it's basically a, uh, control loop. Um, conceptually, it's, it's very similar to the PID controller. And no, this

  11. 5:11

    PID controller will not help you kill a Linux process, but instead it's a classical control theory technique that continuously steering the system towards its desired state. And we just borrowed this important concept, the proportional part of it, and, uh, applied into our, uh, contr- our load balancer. So it has a lot of nice properties. For example, it could combine the useful signals we care about into the single routing decision. And because of the, it

  12. 5:41

    adapt to the ob- observed performance, as what me- we mentioned earlier, there's a lot of constraints, and those constraints might have the, some engines busier because they can serve more requests, more kind of requests than the remaining. But those busier signal will be fed into the next loop and resulting in the less constrained sig- less constrained request can go to more of those kind of engines. So basically, they self-balance it out. And to some extent, this just means we don't need to- Do a lot to manual

  13. 6:11

    intervention, and it, it just works. However, that kind of adaptability comes with big trade-offs. Because of the same reason that it combines so many signals, it's also very hard to reason about a particular routing decision, or, like, why search engine gets a higher weight than we expect. And every time we want to fine-tune towards some aspect, it's alm-almost impossible to not impacting something else. And the load is not always w- uh, where, where

  14. 6:41

    evenly distributed because, uh, sometimes a model is served by engines on different GPU SKUs, and they have different characteristics. Then the problem becomes a lot more trickier. And the f- the feedback loop sometimes creates bad oscillations because when you're shifting an engine away some traffic, the engine turns a bit cooler, and this signal get fed to the controller. The controller now thinks like, "Hey, this

  15. 7:10

    engine can take a lot more traffic." Then the, some traffic gonna be shifted back and forth between a few engines and disrupting the KV cache utilization. So all those limitations motivated us to rethink about the architecture and, uh, see if we have new ways to address the problem. So I'm going to hand over to Qianru to, uh, deep dive into the new architecture we tried out.

  16. 7:38

    Yeah. Thank you, Lu. So I'm going to talk about the architecture of the load balancer and how do we reduce the overall overhead with our routing algorithm. The load balancer answers one question. Uh, for each request from a CPU cluster, which engine should serve it? One most naive baseline might be round robin, which send requests across engines evenly. But if you think a little bit more, that doesn't

  17. 8:08

    make sense. Because engines are not homogeneous, they can have different hardware and capacity, different health, and also different distance from CPU cluster. Also, round robin could break cache locality. S- related requests that could reuse the same engine cache might be sent to different engines. A probably better solution might be for each CPU cluster, it choose the best engine from its own local view.

  18. 8:39

    But that's not enough either. Think about one extreme case. Multiple CPU cluster route traffic to the same engines independently, which could overload that engine while leave other engines underutilized. So what we need is a globally optimized solution, a control plane that has a global view for all the CPU cluster and GPU engines and could compute a globally optimized routing answers. And the data plane can make a

  19. 9:09

    routing decision quickly based on the answer pulled from the control plane.

  20. 9:18

    Now let's look inside the control plane and data plane. In the data plane, there is an engine selector which select engine for each request. It reads the local routing state, which includes the candidate engines and the routing weights for each candidate engines. Both of them are refreshed asynchronously in the background, so we don't need to ask the control plane before we make a routing decision for each request.

  21. 9:48

    Also, the data plane collects real-time engine signal, such as number of ready replica, engine health, et cetera, to serve as fast local guardrail. In the control plane, the data loader combines those live engine signals and network overhead. And with offline regressions of capacity, TTFT, and TBOT, the optimizer could turn those data into routing weights, and the control plane will publish the

  22. 10:18

    routing weight for each data plane to pull. In this way, no request need to wait on the data plane. The control plane continuously compute the next globally optimized routing weight snapshot while the data plane make a routing decision based on the latest snapshot already installed locally.

  23. 10:42

    In summary, there are three important paths through the system. The first path is the inference request path. The request arrive to the CPU cluster, and the data plane inside that CPU cluster will select engine for that request based on the local routing state and forward the request to the selected engines. The second path is the engine signal path. The system continuously collects real-time engine signal, such as TTFT,

  24. 11:12

    TBOT, number of ready replica, and engine health, et cetera. Both planes need those real-time engine signals. The control plane need them to compute a globally optimized routing weight, while the data plane need them to serve as fast local guardrail. And the third path is the routing weight path. The control plane compute and publish the routing weight, and the data plane pull the updates to its local cache. So only

  25. 11:42

    the first path is synchronous, but it's fast and only local inside the data plane of the CPU cluster. The other two loops are asynchronous loop, and they are to improve future routing decision.

  26. 11:59

    So that's pretty much of the architecture part, but that still leaves one question. How do we compute those routing weights?

  27. 12:10

    But before answer that question, let's answer another question first. Why not just send the request to the nearest engine? That's because the traffic demand and GPU capacity are not geographically balanced. For example, in region one, CPU cluster A send ninety RPS, and the nearby engine A can serve one hundred RPS. So in this case, nearest-only is fine.

  28. 12:40

    While in region two, CPU cluster B send one hundred and twenty RPS, and the nearby engine B could only serve one hundred RPS. So in this case, if we insist on keeping everything local, the extra twenty RPS need to wait on an overloaded engine B. While in region three, we are only using four RPS of an eight RPS engine C. That still leaves four RPS spare. So if we send the extra twenty

  29. 13:10

    RPS from cluster B to engine C, that will add network distance. But it could also avoid a probably much larger engine-side waiting time. So in this case, a further engine might be faster end-to-end. That's why we need something better than the nearest-only routing.

  30. 13:35

    Now, let's open the black box of the optimizer. The optimizer accepts four types of input: the request from each CPU cluster, the network latency to each engine, the available engine capacity and health, and also the TTFT, TBOT latency profiles that tell us how's the engine-side latency change as the load increases. And with those

  31. 14:05

    input, the optimizer turn the input to the output routing weights. The routing weights say, for each CPU cluster, what fraction of its traffic should go to each GPU engine. And the optimization goal is straightforward. It's to minimize the expected end-to-end latency across all routed traffic. The important part is that the end-to-end latency includes both the network distance

  32. 14:35

    and the engine-side latency. That means a nearby engine might be attractive when it still has room to serve traffic, while a further engine might be better if all the nearby engines are close to full. And the optimizer also need to respect several hard constraints. First, it need to route all the traffic demand. Second, it need to ensure all the engines stay within the effective capacity.

  33. 15:05

    Third, it need to keep the routing weights non-negative. With this, the controller, control plane get the routing weight from the optimizer and publish them, and the data plane pull them and use them to make a globally optimized routing decision. And that's pretty much of my part, and Lu will continue to talk about the protection mechanisms in the system.

  34. 15:33

    Thanks, Qianru. So as AI engineers, we all kind of know that production, in many cases, are not behaving in the most ideal case. So clusters can fail, GPUs or individual nodes can degre-degrade, and networking can just get to all kind of mysterious issues. So how do we keep our production system, uh, healthy as much as p-possible under the heavy load? The first design we

  35. 16:03

    have is, uh, penalties. Basically, when an engine is an outlier, we detect the anomaly and try to reduce the routing weight to that engine. In that way, we give it a chance to either recover by themselves if there's a, uh, s- if, if some transient issue, or we can have a human intervene to rotate it out or replace the faulty hardware. And secondly, the retries, which is a very common technique used to mitigate

  36. 16:33

    problems. However, during some cases, it actually could make sense even worse, like when the system is very close to, like, a tip over or very heavily utilized. Retries will send more load, and these more loads will, uh, cause more failures and cause more retries, which is a infamous retry storm. So we incru- implemented caps or budget to constrain retries into a acceptable region. And this is actually even need to be

  37. 17:03

    dynamic because in the happy time or in the normal time, we can tolerate a lot more retries than when the system are heavily utilized. And finally, we have the load shedding, which is our last resort when the production capac- uh, capacity couldn't meet the increasing amount of, uh, inference demands. So we instead, uh, we try to have all the system fail. We basically proactively load shed a portion of the traffic

  38. 17:33

    to have the system degrade gracefully. So that pretty much concludes our talk today, and, uh, thanks for joining us. Uh, both of us will be around in our, uh, booth area this afternoon. So if you have further questions, feel free to walk, uh, to the area and ta- and chat with us. Thank you.