Skip to content

6. The cliff

Sweep the arrival rate to find where latency rises sharply, then vary memory to see how cache pressure affects that threshold.

The sweep

for L in 1.5 1.7 1.8 1.9 2.0; do
  serq run docs/tutorial/programs/05-engine.sq --horizon 20000 --warmup 2000 --seed 1 --set Lambda=$L --json
done
sessions/s hit rate TTFT (s) preemptions stuck utilisation
1.5 0.450 0.145 906 154 0.597
1.7 0.327 0.404 2,439 406 0.725
1.8 0.230 1.276 4,506 754 0.813
1.9 0.125 4.457 7,274 1,198 0.902
2.0 0.041 17.921 9,780 1,582 0.971

A mean from one run is a level only if a longer run gives about the same. The same seed at longer horizons:

for H in 20000 40000 80000 160000 320000; do
  for L in 1.5 1.7 1.8 1.9 2.0; do
    serq run docs/tutorial/programs/05-engine.sq --horizon $H --warmup 2000 --seed 1 --set Lambda=$L --json
  done
done
sessions/s TTFT (s) at 20 000 40 000 80 000 160 000 320 000 hit rate at 320 000
1.5 0.145 0.130 0.128 0.132 0.133 0.455
1.7 0.404 0.327 0.295 0.295 0.313 0.326
1.8 1.276 0.815 0.670 0.615 0.647 0.240
1.9 4.457 2.682 2.175 1.815 2.288 0.125
2.0 17.921 26.486 33.251 70.388 195.907 0.002

Up to 1.9 the TTFT has a level: about 0.13 s at 1.5, 0.30 s at 1.7, 0.63 s at 1.8 and 2.2 s at 1.9. The 20 000 s runs at 1.8 and 1.9 caught an early excursion that a longer run dilutes. The level at 1.9 is reached slowly and is noisy: at 320 000 s, seeds 1–3 give 2.29, 2.22 and 2.08 s, each with a 95 % interval of ±0.5–0.6 s within its run (at 1.8 they give 0.65, 0.62 and 0.61 s). Only 2.0 has no level: its TTFT keeps growing with the run length, so 17.92 s is one moment of a queue without bound.

The cliff is between 1.9 and 2.0 sessions/s, and latency rises steeply before it: the level doubles from 1.7 to 1.8 and grows about 3.5 times from 1.8 to 1.9. The hit rate at 320 000 s falls from 0.46 at 1.5 to 0.13 at 1.9, and to 0.002 at 2.0.

These runs also report stuck sessions: requests preempted again without progress. The latency samples therefore do not describe all offered traffic. Inspect these counters, and the run length, before using a mean for capacity planning, and repeat the experiment across seeds.

Is it just saturation?

To separate compute saturation from memory pressure, run the same loads with ten times the KV pool and the same cost model:

serq run docs/tutorial/programs/05-engine.sq --horizon 20000 --warmup 2000 --seed 1 --set Lambda=2.0 --set blocks=40000
serq run docs/tutorial/programs/05-engine.sq --horizon 20000 --warmup 2000 --seed 1 --set Lambda=3.0 --set blocks=40000
sessions/s KV blocks hit rate TTFT (s) stuck utilisation
2.0 4,000 0.041 17.921 1,582 0.971
2.0 40,000 0.800 0.020 0 0.470
3.0 40,000 0.800 0.023 0 0.633

At 2.0 sessions/s, the larger pool turns a queue that grows without bound (17.92 s at this horizon) into a TTFT of 0.020 s. Both larger-pool runs have no preemptions or stuck sessions. Memory capacity changes the amount of recomputation, even with the same arrival rate and engine cost model.

The loop

    memory is tight
          │
          ▼
 prefixes get evicted ──────┐
          │                 │
          ▼                 │
   turns miss, and a        │
   miss recomputes the      │
   whole context            │
          │                 │
          ▼                 │
  each turn costs the       │
  engine more              │
          │                 │
          ▼                 │
  turns stay resident       │
  longer, holding KV ───────┘

Eviction and recomputation can reinforce each other: longer service keeps more KV resident, leaving less room for cached prefixes. The sweep shows a sharp change in observed latency, but does not establish multiple stable states.

For comparison with a measured A100 deployment and a cache-policy experiment, see the vLLM use case.

What to try

Vary one parameter at a time:

# more memory
serq run docs/tutorial/programs/05-engine.sq --horizon 20000 --warmup 2000 --seed 1 --set Lambda=1.8 --set blocks=8000
# fewer concurrent requests
serq run docs/tutorial/programs/05-engine.sq --horizon 20000 --warmup 2000 --seed 1 --set Lambda=1.8 --set max_seqs=8
# shorter thinking time
serq run docs/tutorial/programs/05-engine.sq --horizon 20000 --warmup 2000 --seed 1 --set Lambda=1.8 --set Z=1

Next: a real system written in it, or the reference.