Werkplaats / De werkplaats
In jouw eigen tempo
← AI in de wereld
MODULE 01 · LES 01

Verwerk verzoeken en meet staartlatenties

Bouw een eigen FIFO-batcher en onderscheid verzameltijd, echte wachttijd, latency-SLO en fictieve tokenkosten.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je simuleert begrensde FIFO-batches met expliciete aankomst-, start- en eindtijden.
  • Je berekent nearest-rank-percentielen, inclusieve SLO-fracties en afzonderlijke tarieven per miljoen tokens.

Een model kan een verzoek snel berekenen terwijl de gebruiker toch lang wacht. Er kunnen al andere verzoeken klaarstaan, en samen verwerken kan efficiënter zijn dan ieder verzoek apart starten. Daarom horen wachttijd, verwerkingscapaciteit en kosten bij het ontwerp van een AI-dienst.

Denk aan een bus: direct vertrekken helpt de eerste reiziger, even wachten kan meer mensen meenemen. Maar een grote bus helpt weinig als je op een lege halte onnodig blijft wachten. Bij batching zoek je zo'n afweging met expliciete aannames en metingen.

Wij gebruiken een eigen FIFO-simulator: wie het eerst aankomt, wordt het eerst meegenomen. Er is één uitvoerder en een batch eindigt voor al haar verzoeken tegelijk. De tijden en tarieven zijn oefenwaarden; het rekenmodel helpt je beleid vergelijken.

1. Scheid wachten van uitvoeren

Bekijk eerst hoe dezelfde verzoeken veranderen als je de batchgrootte of maximale verzamelwachttijd aanpast. Een volle batch vertrekt zodra de uitvoerder vrij is en genoeg verzoeken aanwezig zijn. Anders wacht zij tot de deadline van het oudste verzoek, of tot zij eerder vol raakt.

VERKEN HET ZELFMeer samen verwerken kan extra wachten kosten

Volg ieder verzoek vanaf de aankomst tot het batchresultaat. Vergelijk een piek met rustige aankomsten en onderzoek wat de verzamelgrens werkelijk begrenst.

02468R1R1: 1 ms wachten, 5 ms totale latency, batch 1R2R2: 0 ms wachten, 4 ms totale latency, batch 1R3R3: 2 ms wachten, 4 ms totale latency, batch 2Tijd (ms)

● Aankomst · gestreept: wachten · gevuld: verwerking. Verzoeken met hetzelfde batchnummer eindigen samen.

Batches
2
Verzoeken per seconde
375
p95 latency
5 ms
Binnen gekozen SLO
66,667%
Alle tijden in ms; latency = wachten + verwerking. Batchnummers worden hier vanaf 1 getoond.
R / batchStartEindeWachtLatency
1 / 11515
2 / 11504
3 / 26824

1 van 3 verzoeken overschrijden 4 ms; gelijk aan de grens telt als gehaald. Grootste wachttijd: 2 ms bij een verzamelgrens van 2 ms. Een bezette uitvoerder kan die wachttijd verder laten oplopen.

Eigen FIFO-oefensimulator: één uitvoerder, overhead 1 ms en 1 ms per grootste werktoken. Geen echte hardwaremeting of continuous LLM batching. Een SLO is hier een zelfgekozen grens voor de totale latency.

Noem aankomst A, start S en afronding F. Voor verzoek i is de wachtrijduur Q het stuk vóór de start; de totale latentie L loopt helemaal tot de afronding. Dezelfde batch kan daardoor verschillende latenties voor haar reizigers opleveren.

Qi=Si−AiLi=Fi−Ai

In onze simulator kost elke batch b een vaste overhead h plus c milliseconden per werktoken van het langste verzoek. De hoeveelheid werk van verzoek i heet W_i. Dit model maakt ook zichtbaar waarom zeer ongelijke verzoeklengtes ongunstig kunnen zijn: kort werk wacht mee tot het lange werk klaar is.

Db=h+cmaxi∈bWi

Neem aankomsten 0, 1 en 4 ms met respectievelijk 2, 3 en 1 werktokens. De batchlimiet is twee, de verzamelwachttijd twee ms, overhead één en tokentijd één ms. Het tweede verzoek vult de eerste batch op tijdstip één; haar duur is vier en haar einde vijf.

Het derde verzoek komt tijdens die uitvoering aan. Zijn deadline is tijdstip zes, dus de onvolledige tweede batch start daar en eindigt op acht. De simulator gebruikt ook voor deze laatste batch dezelfde wachtregel.

| Rij | A | S | F | L | |---|---:|---:|---:|---:| | 0 | 0 | 1 | 5 | 5 | | 1 | 1 | 1 | 5 | 4 | | 2 | 4 | 6 | 8 | 4 |

requests = [[0,2],[1,3],[4,1]]
run = simulate_batches(
    requests,batch_limit=2,
    max_wait_ms=2,overhead_ms=1,
    per_token_ms=1)
print(run['starts_ms'].tolist())
print(run['finishes_ms'].tolist())
# [1.0, 1.0, 6.0]
# [5.0, 5.0, 8.0]

Een maximale verzamelwachttijd begrenst niet de wachttijd achter een bezette uitvoerder. Ook is een grotere batch niet automatisch beter: één verzoek met tien ms verzamelwachttijd moet gewoon wachten, terwijl er niemand bijkomt.

alone = simulate_batches(
    [[0,2]],batch_limit=8,
    max_wait_ms=10,overhead_ms=1,
    per_token_ms=1)
print(alone['latencies_ms'].tolist())
# [13.0]
01
CHECKPOINTVerwerk een FIFO-wachtrij

2. Kijk ook naar de staart

Het gemiddelde kan een groep trage verzoeken verbergen. Sorteer daarom de latenties en kies expliciet hoe je percentielen berekent. Onze rangmethode (nearest rank) gebruikt fractie q en aantal N; r is de nulgebaseerde index in de gesorteerde vector.

r=⌈qN⌉−1

De gesorteerde latenties zijn [4, 4, 5]. P50 is vier, p95 is vijf; bij een gekozen servicenorm (service level objective, SLO) van vier ms halen twee van de drie verzoeken hun grens. Gelijkheid telt als gehaald, en de functie geeft een fractie terug.

summary = latency_summary(
    [5,4,4],slo_ms=4)
print(summary['p50_ms'])
print(summary['p95_ms'])
print(round(summary['slo_met'],3))
# 4.0
# 5.0
# 0.667

Voor throughput telt ook het meetinterval. Met N verzoeken in T milliseconden, van eerste aankomst tot laatste afronding, is R het aantal verzoeken per seconde. Ons voorbeeld verwerkt drie verzoeken in acht ms en komt dus op 375 per seconde binnen dit model.

R=1000NT
02
CHECKPOINTMeet staartlatenties en een gekozen SLO

3. Houd rekeneenheden zichtbaar

Een tarief per miljoen tokens is geen tarief per verzoek. Noem de aantallen invoer- en uitvoertokens I en O, met tarieven a en b per miljoen. Totale kosten C volgen door beide soorten apart om te rekenen en op te tellen.

C=Ia+Ob106

Met fictieve tarieven twee en acht kosten 1000 invoertokens en 250 uitvoertokens elk 0,002 valuta-eenheid. Samen is dat 0,004. Dit oefenmodel rekent geen kortingen, belastingen of andere factuurregels mee.

cost = token_cost(1000,250,2,8)
print(cost['input_cost'])
print(cost['output_cost'])
print(cost['total_cost'])
# 0.002
# 0.002
# 0.004
03
CHECKPOINTReken met fictieve tarieven per miljoen

Wat neem je mee?

  • Vergelijk batchbeleid met dezelfde verzoeken en expliciete serviceaannames.
  • Scheid wachtrijduur, totale latentie, staartpercentielen en throughput.
  • Controleer aantallen en tariefeenheden voordat je kosten optelt.

Hierna meet je of snelle antwoorden ook goede antwoorden blijven en wat een verschoven verdeling werkelijk zegt.

DE VOLGENDE STAPScheid modelkwaliteit van verschoven verdelingen
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Verwerk een FIFO-wachtrij

Schrijf simulate_batches(requests,batch_limit=4,max_wait_ms=2.,overhead_ms=1.,per_token_ms=.5). Requests is na np.asarray een reële N×2-matrix: aankomsttijd in ms en werktokens, N 1–512. Tijden zijn eindig, oplopend inclusief ties en binnen [0,3600000]; werktokens numeriek geheel binnen 1–4096. Valideer de geconverteerde matrix: dtype i/u/f, geen pure bool/complex/object; gemengde bool/int-cellen kunnen integers worden. Batch_limit is Python-int 1–16; overige configuraties Python-int/float zonder bool: wachten [0,1000], overhead [0,1000], per_token [.000001,100]. Eén uitvoerder, FIFO en geen overlap: start zodra een volle batch beschikbaar is, anders op max(uitvoerder_vrij,oudste_aankomst+max_wait_ms). Een volle batch die vóór die deadline ontstaat start eerder. Neem maximaal batch_limit aanwezige verzoeken in invoervolgorde; aankomst op de grens telt mee. Duur=overhead_ms+per_token_ms*max(werktokens); iedereen in een batch eindigt tegelijk. Geef zelfstandige arrays arrivals_ms,starts_ms,finishes_ms,queue_ms,latencies_ms als floats, work_tokens en batch_ids als integers, allemaal lengte N. Batch-IDs beginnen bij 0. Batches is een lijst dicts met indices,start_ms,finish_ms,service_ms; throughput_per_second=N*1000/(laatste_finish-eerste_aankomst). Queue=start-aankomst, latency=finish-aankomst. Behoud invoer/RNG; ongeldige invoer geeft ValueError. Dit is een eigen oefensimulator, geen hardwarebenchmark of exacte Triton-implementatie.

Een hint

Werk het handvoorbeeld na; houd eenheden, grenzen en behoud van de invoer expliciet.

02
Meet staartlatenties en een gekozen SLO

Schrijf latency_summary(latencies_ms,slo_ms=100.). Latencies_ms is een reële eindige 1D-vector/lijst van 1–4096 waarden binnen [0,1e9], dtype i/u/f na NumPy-coërcie. Slo_ms is Python-int/float binnen [0,1e9], geen bool. Geef count,mean_ms,p50_ms,p95_ms,p99_ms,violations,slo_met. Count en violations zijn integers, de overige waarden floats. Gebruik nearest rank: gesorteerde index ceil(q*N)-1 voor q=.5/.95/.99, dus geen interpolatie. Violations telt latency>slo_ms; slo_met is de fractie latency<=slo_ms, geen percentage 0–100. Behoud invoer/RNG; ongeldige invoer geeft ValueError.

Een hint

Werk het handvoorbeeld na; houd eenheden, grenzen en behoud van de invoer expliciet.

03
Reken met fictieve tarieven per miljoen

Schrijf token_cost(input_tokens,output_tokens,input_per_million,output_per_million). Beide aantallen zijn Python-int binnen 0–1e9; beide tarieven Python-int/float binnen [0,10000], geen bool. Geef een dict met input_cost,output_cost,total_cost als floats: aantallen maal hun eigen tarief gedeeld door 1e6, dan optellen. Behoud invoer/RNG en geef bij ongeldige invoer ValueError. De tarieven komen van de oefening; geen actuele providerprijzen, korting, belasting of echte factuurclaim.

Een hint

Werk het handvoorbeeld na; houd eenheden, grenzen en behoud van de invoer expliciet.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

import math

# Jouw functies

def simulate_batches(requests,batch_limit=4,max_wait_ms=2.,overhead_ms=1.,per_token_ms=.5):
    raw=np.asarray(requests)
    if raw.ndim!=2 or not 1<=len(raw)<=512 or raw.shape[1]!=2 or raw.dtype.kind not in 'iuf':raise ValueError('Een matrix met aankomsttijd en werktokens vereist')
    if not np.isfinite(raw).all():raise ValueError('Eindige verzoekwaarden vereist')
    data=raw.astype(float,copy=True);arrivals=data[:,0];work=data[:,1]
    if np.any(arrivals<0) or np.any(arrivals>3600000) or np.any(np.diff(arrivals)<0):raise ValueError('Oplopende niet-negatieve aankomsttijden vereist')
    if np.any(work<1) or np.any(work>4096) or np.any(work!=np.floor(work)):raise ValueError('Werktokens moeten geheel zijn binnen 1–4096')
    batch_limit=_ops_int(batch_limit,1,16);max_wait_ms=_ops_number(max_wait_ms,0,1000)
    overhead_ms=_ops_number(overhead_ms,0,1000);per_token_ms=_ops_number(per_token_ms,.000001,100)
    starts=np.empty(len(data));finishes=np.empty(len(data));batch_ids=np.empty(len(data),dtype=int);batches=[];ready=0.;index=0
    while index<len(data):
        now=max(ready,float(arrivals[index]));deadline=max(now,float(arrivals[index])+max_wait_ms)
        available=index
        while available<len(data) and available-index<batch_limit and arrivals[available]<=now:available+=1
        if available-index==batch_limit:start=now;stop=available
        elif index+batch_limit<=len(data) and arrivals[index+batch_limit-1]<=deadline:
            start=max(now,float(arrivals[index+batch_limit-1]));stop=index+batch_limit
        else:
            start=deadline;stop=index
            while stop<len(data) and stop-index<batch_limit and arrivals[stop]<=start:stop+=1
        duration=overhead_ms+per_token_ms*float(np.max(work[index:stop]));finish=start+duration;batch_id=len(batches)
        starts[index:stop]=start;finishes[index:stop]=finish;batch_ids[index:stop]=batch_id
        batches.append({'indices':list(range(index,stop)),'start_ms':start,'finish_ms':finish,'service_ms':duration})
        ready=finish;index=stop
    elapsed=float(finishes[-1]-arrivals[0])
    return {'arrivals_ms':arrivals.copy(),'work_tokens':work.astype(int),'starts_ms':starts,'finishes_ms':finishes,'batch_ids':batch_ids,
        'queue_ms':starts-arrivals,'latencies_ms':finishes-arrivals,'batches':batches,'throughput_per_second':float(len(data)*1000/elapsed)}

def latency_summary(latencies_ms,slo_ms=100.):
    raw=np.asarray(latencies_ms)
    if raw.ndim!=1 or not 1<=len(raw)<=4096 or raw.dtype.kind not in 'iuf':raise ValueError('Een niet-lege latentievector vereist')
    if not np.isfinite(raw).all() or np.any(raw<0) or np.any(raw>1e9):raise ValueError('Eindige niet-negatieve latenties vereist')
    slo_ms=_ops_number(slo_ms,0,1e9);values=raw.astype(float,copy=True);ordered=np.sort(values)
    result={'count':len(values),'mean_ms':float(np.mean(values))}
    for name,fraction in [('p50_ms',.5),('p95_ms',.95),('p99_ms',.99)]:result[name]=float(ordered[math.ceil(fraction*len(ordered))-1])
    result['violations']=int(np.count_nonzero(values>slo_ms));result['slo_met']=float(np.mean(values<=slo_ms))
    return result

def token_cost(input_tokens,output_tokens,input_per_million,output_per_million):
    input_tokens=_ops_int(input_tokens,0,10**9);output_tokens=_ops_int(output_tokens,0,10**9)
    input_per_million=_ops_number(input_per_million,0,10000);output_per_million=_ops_number(output_per_million,0,10000)
    incoming=input_tokens*input_per_million/1e6;outgoing=output_tokens*output_per_million/1e6
    return {'input_cost':incoming,'output_cost':outgoing,'total_cost':incoming+outgoing}

# Gegeven helpers

def _ops_int(value,low,high):
    if type(value) is not int or not low<=value<=high:raise ValueError('Gehele oefenwaarde buiten bereik')
    return value

def _ops_number(value,low,high):
    if type(value) not in (int,float) or not low<=value<=high or not math.isfinite(value):raise ValueError('Eindige oefenwaarde buiten bereik')
    return float(value)

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.