17. Binary Search for the Largest Batch Size Under a Latency Budget
Problem
A lab's inference serving team is tuning the batch size used to group concurrent requests on a GPU. Larger batches use the GPU more efficiently (higher throughput per dollar), but they also increase per-request p99 latency, since a request has to wait for the whole batch to finish. Given measured p99 latency for each candidate batch size (latency is non-decreasing as batch size grows, since bigger batches always do at least as much work per step), find the largest batch size whose latency still meets an SLA — without measuring every single batch size, since each measurement in production means an actual load test.
Source: src/17_binary_search_max_batch_size.py
def max_batch_size_under_latency(latencies: list[float], max_latency: float) -> int:
>>> max_batch_size_under_latency([10, 15, 22, 40, 90], 30)
3
>>> max_batch_size_under_latency([10, 15, 22, 40, 90], 5)
0
Step-by-Step Approach
- Notice that "is batch size b feasible" (i.e.
latencies[b-1] <= max_latency) is a monotonic predicate: it'sTruefor a run of small batch sizes, then flips toFalseforever once latency crosses the budget, sincelatenciesis non-decreasing. That's the precondition for binary search on the answer. - Set up the search range over candidate batch sizes themselves,
lo = 1tohi = len(latencies), rather than over array positions with some other meaning. - At each step probe the midpoint
mid: checklatencies[mid - 1] <= max_latency. - If feasible,
midis a valid answer but maybe not the largest one — record it as the current best and search the upper half (lo = mid + 1) for something even bigger. - If infeasible,
midand everything larger is guaranteed infeasible too (monotonicity) — discard the upper half entirely (hi = mid - 1). - Continue until
lo > hi; the last recordedbestis the largest feasible batch size, or 0 if nothing was ever feasible (including the empty-list case, where the loop body never runs).
The key insight is that this isn't "binary search for a value in a sorted array" — it's "binary search over a range of candidate answers, using a monotonic feasibility check as the comparison," which turns an O(n) measurement scan into O(log n) probes, exactly the win that matters when each probe is an expensive real-world load test rather than a free array lookup.
Reference solution
def max_batch_size_under_latency(latencies: list[float], max_latency: float) -> int:
# binary search on the answer: latencies is monotonic non-decreasing, so
# "is batch size b feasible" is a step function (True...True, False...
# False) -- we're hunting the last True. O(log n) probes vs. O(n) scan.
lo, hi = 1, len(latencies)
best = 0
while lo <= hi:
mid = (lo + hi) // 2
if latencies[mid - 1] <= max_latency:
# batch size mid works -- it might not be the largest, keep it
# and search the upper half for something even bigger
best = mid
lo = mid + 1
else:
# too slow at this batch size, no point trying anything bigger
hi = mid - 1
return best
Key Functions & Tricks
lo, hi = 1, len(latencies)— the search range is over batch sizes (the answer space), not array indices directly.mid = (lo + hi) // 2— standard midpoint probe; each iteration halves the remaining candidate range.best = mid; lo = mid + 1— records a feasible answer before continuing to search for a possibly-larger one, instead of stopping at the first hit.hi = mid - 1— prunes the entire upper half once infeasibility is confirmed, relying on monotonicity rather than checking each value individually.best initialized to 0— doubles as the correct answer for both "nothing is feasible" and "the list is empty," with no separate edge-case branch needed.
How to Recognize This Pattern
Reach for "binary search on the answer" whenever a problem asks you to find the largest/smallest value satisfying some condition, and checking that condition for one candidate value is monotonic (once it flips from feasible to infeasible, it never flips back) — the signal is language like "maximum X such that Y holds" combined with an expensive or awkward-to-invert feasibility check. Common variations include finding the minimum resource (workers, days, capacity) needed to meet a deadline, or the maximum threshold that still satisfies a global constraint. A common pitfall is applying this pattern when the feasibility function isn't actually monotonic (e.g. latency that first drops then rises with batch size due to caching effects) — the halving logic silently produces a wrong answer without erroring; another is off-by-one errors in the lo/hi/mid update rules, which either infinite-loop or exclude the true boundary value.