Basis Lab

Answers · Putting a model into service

How do I tell whether a slow request spent its time waiting in line or actually being worked on?

In shortTime a request spent waiting is the total measured where it arrived minus the time anything was actually computing for it; whatever is left was queue.

A request's time in line is the total elapsed time, measured where the request arrived, minus the time anything was actually computing on its behalf. If that subtraction leaves most of the elapsed time unaccounted for, the request spent it queued, and no amount of tuning the model will help.

Take three timestamps at the edge: connection accepted, first word out, last word out. The first and last give the total. To get the serving part, count the tokens. Reading the prompt costs an amount proportional to its length, and each produced word costs an amount set by how fast the model's numbers can be read out of memory. Those two costs are measurable once on an idle service and then stay put; everything above them is waiting.

Suppose a word costs forty milliseconds on your model and reading the prompt costs eighty. A request that finishes 4800 milliseconds after it arrived, having produced 92 words, contains 92 × 40 = 3680 milliseconds of word production, plus 80 for the prompt, so 3760 milliseconds of work. The remaining 4800 − 3760 = 1040 milliseconds, a little over a fifth of the total, was queue. Run the same subtraction on the slowest request in the log and on the fastest and you have your answer per request, not just on average.

How occupancy decides the split

The serving term ss is a property of the model and the machine, and it barely moves. The waiting term is a property of how busy the service is, and it grows without bound as the busy share UU approaches one:

T=s1−UT = \frac{s}{1 - U}

With s=40s = 40 milliseconds, a half-busy service gives 40/0.5=8040/0.5 = 80 milliseconds, of which 40 is work and 40 is queue. At ninety percent busy it gives 40/0.1=40040/0.1 = 400 milliseconds: 40 of work, 360 of queue, nine times the serving time. The jump from half busy to ninety percent busy multiplies what the user feels by five (22 to 1010) while the hardware is untouched.

FIG 1
busy share Umultiplier 1/(1-U)total T (ms)waiting (ms)
0.5028040
0.805200160
0.9010400360
0.9520800760

Why the internal number stays flat

The natural guess is that the service's own latency metric will tell you, and it will not. A service that starts its timer when the request is dequeued is measuring only ss, the half that does not degrade. Its graph reads forty milliseconds all through the incident while users wait four hundred. The same blindness applies to the machine's CPU graph: nothing is being computed during the wait, so the hardware looks healthy precisely when the queue is at its worst.

Queue depth closes the loop and needs no per-request timing at all. The number inside the service equals the arrival rate times the average time each request spends there:

N=λ TN = \lambda \, T

At 200 arrivals per second and a steady depth of 60, the average request is inside for 60/200=0.360/200 = 0.3 seconds, or 300 milliseconds. Of those 60, the ones actually being served number 200×0.04=8200 \times 0.04 = 8, so 52 are waiting. Against a 40 millisecond serving time, 300 milliseconds of total is almost entirely queue, and you learned it from a depth gauge rather than a stopwatch. Run backwards, it lets you promise: 400 arrivals per second at half a second each means the service must hold 400×0.5=200400 \times 0.5 = 200 requests in flight.

The split stops being clean in two places. As UU approaches one the formula diverges, and arrivals are no longer steady either, so a burst raises both terms at once and the tidy subtraction overstates the queue. And batching or memory contention can make ss itself grow with the number of requests served alongside yours, at which point the two terms move together and a single total can no longer be attributed to either. It also stops being useful if the clock starts anywhere but the edge: a timer inside the service omits exactly the term you are trying to measure.

Watch two clocks rather than one, because they have different causes. The pause before the first word is dominated by the queue and by prompt length; the rate of words afterwards is dominated by the model and by how many others are being decoded next to you. A service can report a respectable average while every user stares at an empty box.

Worth keeping

  • Total time is waiting plus serving; serving is set by the model and the machine, waiting is set by how busy the machine is.
  • A queue depth gauge is a latency gauge: depth equals arrival rate times average time in the service.
  • A timer started inside the service measures only the half of the request that does not get worse under load.

Where this goes next

This question is one step of Serving a Model to Many People, a course that works through its subject from the beginning. The page that teaches this part of it is Where a Request's Time Goes, and it reads the whole way through whether or not you have an account.

What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.

Read that pageThe whole course

Other questions people ask about this

  1. Why does promising that 99% of requests are fast cost so much more capacity than promising the average is fast?A promise about the slowest requests forces you to keep machines idle so queues never build, while an average promise can be met by making ordinary requests slightly faster.
  2. Why does the model return JSON wrapped in extra text when I asked for JSON?A request for JSON does not force the model to return only JSON, so you need to validate the reply and repair it once rather than assume it will parse.
  3. Why do the numbers look good while users keep complaining?Because the score measures a fixed set of cases with a fixed rule, and the users are living through behaviour that the set and the rule never covered.
  4. How do I make the model answer from the documents I give it instead of its own memory?Fence the documents as labelled reference material, put them before the question, tell the model to prefer them over memory, and require a passage number for each claim.
  5. Why does the memory used by a model keep growing as the conversation gets longer?The model's weights do not grow. It keeps a cache of keys and values for every earlier token, so memory rises with conversation length and with how many conversations run at once.
  6. How many legitimate requests should I be willing to block in order to catch the bad ones?Block no more legitimate requests than one prevented harmful answer is worth: the break-even count is the harm cost divided by the refusal cost.

Read alongside