ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 170
Numărul 169 Numărul 168 Numărul 167 Numărul 166 Numărul 165 Numărul 164 Numărul 163 Numărul 162 Numărul 161 Numărul 160 Numărul 159 Numărul 158 Numărul 157 Numărul 156 Numărul 155 Numărul 154 Numărul 153 Numărul 152 Numărul 151 Numărul 150 Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 170
Abonamente

Servicii IA auto-sustenabile: de la capacitate concurentă alocată la capacitate variabilă

Crina Boitor
Java & Cloud DevOps Engineer @ BMW TechWorks Romania



PROGRAMARE

Modelele generative de limbaj (LLM) au dat complet peste cap profilul de latență al serviciilor de tip enterprise. Un request care înainte aștepta 40 ms pentru acces I/O, acum este blocat timp de 20 de secunde pentru a primi răspunsul de la un model. În arhitectura clasică care implică un thread-per-request, această realitate transformă concurența într-o constantă rigidă, care trebuie determinată în avans. Rezultatul? Un pod saturat raportează un consum CPU aproape de zero, deși nu mai poate primi trafic adițional. Decizii arhitecturale calibrate pentru zeci de milisecunde au fost determinate pentru un status-quo care nu mai corespunde realității.

În acest articol, urmărim să măsurăm trei modele de execuție ale unui software Java simplu având la bază frameworkul Quarkus. Un pool de 200 de threaduri, la o calibrare corect adaptată, se blochează complet la doar 9.96 requesturi/sec (cu o precizie de 0.3% comparativ cu aproximarea în baza Legii lui Little) și are nevoie de 51 de poduri acolo unde utilizarea modelului reactiv sau a unuia bazat pe threaduri virtuale implică un singur pod.

Cele două abordări non-blocking sunt identice ca latență. Totuși, modelul reactiv consumă ceva mai puține resurse, ceea ce înseamnă că alegerea threadurilor virtuale se justifică mai degrabă prin simplitatea codului și reducerea costului de dezvoltare. În plus, adaptarea limitei de concurență la nivelul unui semafor în cadrul procesului permite preluarea de încărcare din upstream prin degradarea controlată a calității modelului. Astfel, păstrăm 100% din throughput, în timp ce o strategie clasică simulată de scalare orizontală (HPA) pierde 58% din trafic și rămâne complet blocată timp de 35 de secunde.

Să ne imaginăm un pod care rulează la 3% CPU, dar totuși nu mai poate accepta niciun request.

Pare neverosimil. Și cu toate acestea procesorul este inactiv pentru că execuția în sine este suspendată/blocată: fiecare thread este blocat la citire, așteptând ca un LLM să termine de generat tokenuri. Podul este, în același timp, complet saturat și total inactiv.

Un endpoint REST clasic, care comunică cu o bază de date, așteaptă în medie 40 ms per I/O. Un apel către un LLM durează între 2 și 60 de secunde, iar arhitecturile de tip RAG și pipeline-urile diferitelor modele agentice folosesc între trei și zece astfel de apeluri înlănțuite pentru un singur request din partea utilizatorului. Latența a crescut astfel exponențial, trecând la alt ordin de mărime.

Dacă aplicăm Legea lui Little [1] (L = λ × W), putem vedea numărul de threaduri de sistem pe care podul trebuie să le țină active simultan.

Tip serviciu λ (requesturi) W (Latență) L (threaduri în execuție)
REST clasic + bază de date 500 0.04 s 20
RAG / Aplicații LLM 500 20 s 10 000

Tabelul 1 - Același trafic, la câteva ordine de mărime distanță

Traficul este identic, s-a schimbat doar serviciul pe care îl apelăm. Problema este că pentru 10.000 de threaduri de sistem de operare, la aproximativ 1 MB per thread, am avea nevoie de aproximativ 10 GB de RAM doar pentru aceasta, în cazul fiecărui pod. Astfel că serviciul este limitat artificial (de exemplu, la un pool de 200 de threaduri), în timp ce requesturile așteaptă sau sunt respinse direct.

Problema esențială este că acest blocaj rămâne complet invizibil pentru marea majoritate a mecanismelor de auto-scalare. Threadurile blocate nu consumă CPU, astfel că podul raportează un load de câteva procente, deși nu mai poate primi niciun request adițional. Prin urmare, mecanismul de auto-scalare orizontală reacționează chiar prin sub-alocarea de resurse în momentul în care serviciul este cel mai solicitat - metrica evoluează în direcția greșită relativ la limitarea impusă. Echipele de dezvoltare ajung astfel să compenseze prin supra-alocarea de resurse, sau prin adaptarea mecanismului de auto-scalare, ceea ce reintroduce la nivelul infrastructurii o decizie pe care aplicația putea să o facă deja în avans.

Pentru comparația de față am folosit un pool de threaduri setat la 200, pentru a simula un proces de producție. Numărul a fost ales doar pentru a susține ideea că o configurare aleasă înainte de deploy introduce o limitare ce poate fi depășită prin intermediul unui proces de auto-guvernanță. În prezent, concurența este definită rigid după formula număr poduri × dimensiune pool, ambele fiind fixate înainte ca procesul să pornească. Practic, nu există nicio posibilitate de control în cadrul procesului.

Propunem astfel un proces de auto-guvernanță în cadrul procesului, prin care serviciul rulează un ciclu autonom de control asupra consumului de resurse propriu, în trei etape distincte.

Pentru etapa de detecție se iau în considerare patru variabile disponibile în cadrul procesului: numărul de permise la nivelul semaforului care gestionează concurența, latența medie raportată de Micrometer, rata de răspunsuri cu status 429 primite de la furnizorul modelului și numărul de tokenuri utilizate per oră raportat la bugetul alocat.

Pasul decizional utilizat este deliberat simplist și anume: în cazul în care numărul de permise la nivel de semafor a fost epuizat sau dacă numărul de tokenuri utilizate depășește bugetul, se trece la utilizarea unui model de nivel scăzut. Urmează etapa de auto-guvernanță, în ordinea priorității: modificarea în timp real a numărului de permise la nivel de semafor, nivelul de direcționare a traficului către un model mai sofisticat sau mai simplist, iar ca ultimă soluție, răspunsul cu statusul 503, doar dacă primele două configurări nu au efect.

Ordinea de execuție este esențială, dat fiind că un serviciu clasic blocking de tip thread-per-request poate aplica doar direct pasul 3 (503), deoarece pașii 1 și 2 necesită o etapă decizională care în acest caz s-a luat deja, când s-a blocat orice request adițional.

Threadurile virtuale nu accelerează inferența. Nimic din acest mecanism nu optimizează viteza unui forward pass.

Modelul reactiv este mai eficient din punctul de vedere al resurselor, iar cifrele o demonstrează. La o rată a fluxului λ=500, varianta reactivă a consumat cu 13% mai puțină memorie (556 vs. 631 MB) și cu 20% mai puțin CPU (35% vs. 42%) comparativ cu threadurile virtuale, latența Micrometer p99 având o diferență de doar 0,1 ms de-a lungul a 30.001 eșantioane. La λ=1.000, diferența crește: cu 9% mai puțină memorie și cu 41% mai puțin CPU.

Oricine urmărește exclusiv optimizarea resurselor pentru acest tip de problemă ar trebui să aleagă modelul reactiv. Argumentul în favoarea threadurilor virtuale constă în faptul că ciclul de guvernanță descris se reduce la o simplă ramură decizională într-un cod de proces liniar; fiind o paradigmă axată pe costul de dezvoltare și depanare, nu pe maximizarea traficului de vârf.

Alegerea dintre threaduri virtuale și programarea reactivă în pipeline-urile generative și arhitecturile de tip RAG nu este o dispută privind performanța brută a algoritmilor de inteligență artificială, ci o decizie de management al resurselor și al complexității codului.

Deși ambele paradigme permit implementarea unor mecanisme de control al concurenței (prin semafoare) și strategii de degradare controlată, comportamentul lor sub sarcină critică dictează scenariul optim de utilizare.

Verdictul arhitectural

Pentru sistemele moderne enterprise care integrează LLM-uri, threadurile virtuale reprezintă adesea compromisul pragmatic. Ele acceptă o penalizare controlată în eficiența resurselor hardware în schimbul unei reduceri drastice a complexității software-ului. Cu toate acestea, dacă constrângerile de buget sau volumul de trafic (λ) impun o densitate maximă a cererilor per watt/procesor, paradigma reactivă rămâne bariera tehnologică de neatins pentru modelul thread-per-request.

Sistem de evaluare Am rulat o aplicație cu Quarkus 3.35.2 pe JDK 21 unde un singur serviciu expune în patru moduri diferite același pipeline RAG - o interogare de 20ms în baza de date vectorială, urmată de o așteptare de 20s de la LLM. Toate configurațiile folosesc același WebClient Vert.x și un pool comun de 50.000 de conexiuni, asigurându-ne astfel că poolul upstream nu reprezintă în niciun caz factorul limitator. Ca sursă avem un HttpServer pe threaduri virtuale. Limitele simulate pentru pod sunt -Xmx2g XX:ActiveProcessorCount=4 fără setarea -Xms, ceea ce face ca RSS să reflecte consumul real de memorie. Requesturile au fost generate cu k6 în regim constant-arrival-rate, iar faza de warmup a fost rulată și apoi eliminată din rezultate.

La λ =500 (Tabelul 2), procesarea unui milion de cereri durează 0,556 ore, ceea ce înseamnă că configurația blocantă consumă 28,3 pod-hours per milion de cereri, față de doar 0,556 în cazul celei optimizate — un factor de eficiență de 50,9. Același raport se aplică și în ecuația indicatorului de sustenabilitate software SCI = (E×I + M)/R, deoarece atât componenta energetică, cât și cea a emisiilor încorporate scalează direct proporțional cu numărul de noduri utilizate.

Configurație λ max /
pod
p99 la
capacitate
Amprentă
RAM
(RSS)
Consum
CPU
Thread-uri
OS
Concurență susținută Pod-uri pt.
λ=500
Rata
prezisă
Worker pool (200) 9,96
req/s
20.082 ms 181 MB 3,0% 210 182 51 9,99
req/s
Reactiv (Mutiny) >1.000
req/s
20.126 ms 841 MB 20,0% 29 21.369 1 1,00
req/s
Virtual
Threads
>1.000
req/s
20.168 ms 917 MB 34,0% 38 21.118 1 1,00
req/s

Tabelul 2 - Date experiment

Referințe

  1. J. D. C. Little. "A Proof for the Queuing Formula: L = λW." Operations Research 9(3), 1961, pp. 383-387.

  2. JEP 491: Synchronize Virtual Threads without Pinning. OpenJDK, Java 24. https://openjdk.org/jeps/491

  3. Quarkus. Ghidul Virtual Threads. https://quarkus.io/guides/virtual-threads

  4. G. Tene. How NOT to Measure Latency. Azul Systems. (Coordinated omission; motivul pentru care este necesar un generator de încărcare cu model deschis.)

  5. Netflix. concurrency-limits — limite adaptive de concurență pentru servicii JVM - https://github.com/Netflix/concurrency-limits

  6. Green Software Foundation. Software Carbon Intensity (SCI) Specification, publicată ca ISO/IEC 21031:2024 - https://sci.greensoftware.foundation

  7. P. Lewis et al. "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks." NeurIPS, 2020. arXiv:2005.11401.

NUMĂRUL 166 - AI for Programmers

Sponsori

  • Banca Transilvania
  • Betfair
  • MHP
  • .msg systems
  • P3 group
  • Cognizant Softvision
  • BMW TechWorks Romania

INTERVIURI