Simas Razinskas

Ilgai trunkančios inferencijos orkestravimas be serverių veikiančiuose GPU

Laikinų serverless GPU pavertimas patikimais ilgoms DI inferencijos užduotims: pažangos išsaugojimas, nutrūkusių WebSocket atkūrimas ir darbininkų valymas.

Paskelbta 2026-06-30Peržiūrėta 2026-07-02

Trumpai

Problema
Ilgos vaizdų generavimo užduotys galėjo prarasti darbą arba nutekinti GPU atmintį, kai nutrūkdavo WebSocket ar dingdavo laikinas darbininkas.
Apribojimas
Orkestravimas turėjo tinklo gedimą ir darbininko išjungimą laikyti įprastais įvykiais, o ne naudotojų atrandamomis išimtimis.
Sukurta sistema
Užduočių orkestravimo sluoksnis aplink RunPod ir ComfyUI su pažangos apklausa, atkuriama būsena, ribotu atidėjimu, S3 iškėlimu ir aiškiu išjungimu.
Rezultatas
Trumpalaikiai atsijungimai nebežudo naudingo darbo, o baigtos ar paliktos užduotys paleidžia GPU išteklius vietoj brangiai kabančių darbininkų.
Kur tai pritaikoma
Aktualu DI medijos ar inferencijos sistemoms, kur nuomojamiems GPU aplink modelį reikia eilių, atkūrimo, sąnaudų kontrolės ir valymo.
Technologijos
RunPod · ComfyUI · WebSockets · S3 offload

Kodėl tai svarbu

Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.

Inferencijos užduoties atkūrimo schema: užduotis įtraukiama į eilę, o pažanga stebima apklausa, ne pasitikima vienu lizdu; WebSocket nutrūkimas užduoties viduryje sukelia ribotą eksponentinį atidėjimą jungiantis iš naujo; klientas atsinaujina nuo paskutinio patvaraus kontrolinio taško vietoj perkrovimo; dideli dvejetainiai rezultatai iškeliami į S3 jiems pasibaigus; o darbininkas aiškiai išjungiamas užbaigus ar apleidus, kad būtų atlaisvinta GPU/VRAM.

Apžvalga

Aukščiau aprašytas apribojimas — tai visa užduotis: nuomojamas GPU darbininkas pigus būtent todėl, kad yra vienkartinis, o sistema, pastatyta virš jo, negali jo dingimo — ar tuo metu atviro lizdo praradimo — laikyti išimtiniu atveju, kurį galima sutvarkyti vėliau. Viena inferencijos užduotis čia gali trukti kelias minutes, generuoti didelį dvejetainį rezultatą, o tuo metu praeiti per mobiliojo ryšio perjungimą tarp bokštų, nešiojamojo kompiuterio užmigdymą ar apkrovos balansavimo įrenginio neveiklumo laiko limitą. Nė vienas iš šių atvejų nėra retas — tai vidutinis rezultatas, paleidus bet ką pakankamai ilgai ant nuomojamos, laikinos aparatinės įrangos. Orkestravimo sluoksnis stovi tarp kliento ir RunPod talpinamų ComfyUI darbininkų su viena užduotimi: padaryti taip, kad atsijungimas, darbininkų kaita ir apleistos užduotys atrodytų kaip įprasta veikimo būsena, o ne kaip incidentas, apie kurį turi pranešti naudotojas.

Įtraukimas į eilę ir apklausa vietoj pasitikėjimo lizdu

Užduotis neprasideda nuo lizdo atvėrimo ir tikėjimosi geriausio. Ji prasideda įtraukimu į eilę: klientas pateikia užduotį, gauna jos ID, ir būtent šis ID — o ne konkretus ryšys — yra tai, ką seka likusi sistemos dalis. Pažanga saugoma serverio pusėje, susieta su užduoties ID, ir yra užklausiama, o ne laikoma, kad siuntimas yra vienintelis tiesos šaltinis. „WebSocket“ prisegtas virš to grynai kaip mažos delsos pranešimų kanalas: kai jis atviras ir sveikas, pažangos atnaujinimai atkeliauja greitai; kai ne — klientas pereina prie to paties užduoties ID apklausos fiksuotu intervalu ir mato lygiai tą pačią būseną, kurią būtų pateikęs gyvas lizdas, tik šiek tiek vėliau.

Būtent šis atskyrimas leidžia visa, kas seka toliau. Kadangi užduoties būsena — tai eilutė, apie kurią serveris gali atsakyti į klausimus, „ar ši užduotis dar vykdoma“ tampa užklausa, o ne kažkokio naršyklės skirtuko atvirumo savybe. Klientas gali užsidaryti, atsidaryti iš naujo, atnaujinti puslapį ar perduoti darbą visai kitam įrenginiui, o atsakymas į „kur mano užduotis“ nuo to nepasikeičia.

Kodėl nepasitikėti vien WebSocket ryšiu

Laikyti lizdą vieninteliu tiesos šaltiniu — užduotis egzistuoja lygiai tiek, kiek egzistuoja lizdas — reiškia pririšti kelias minutes trunkančią GPU užduotį prie mažiausiai patikimos visos sistemos dalies. Lizdai užsidaro dėl priežasčių, neturinčių nieko bendra su užduoties būkle: užsidaro nešiojamojo kompiuterio dangtis, mobiliojo ryšio operatorius perjungia įrenginį prie kito bokšto, įmonės tarpinis serveris ar apkrovos balansavimo įrenginys taiko savo neveiklumo laiko limitą, nepaisant to, kas tuo metu dar vykdoma. Jei užduotis gyventų tik to ryšio atmintyje, kiekvienas iš šių įprastų įvykių tyliai nužudytų užduotį, dėl kurios naudotojas neturėtų jokio pagrindo manyti, kad ji nepavyko. Dar blogiau — pats darbininkas neturėtų jokio būdo atskirti „klientas vis dar stebi, tiesiog laikinai atsijungęs“ nuo „kliento nebėra, išjunk mane“ — būtent nuo šio atskyrimo teisingumo priklauso visa sistema. Padarius įtraukimą-į-eilę-ir-apklausą tiesos šaltiniu, o lizdą — vien patogumu virš jo, ryšio nutrūkimas tampa tik naudotojo sąsajos nepatogumu — pažangos indikatorius akimirkai sustingsta — o ne prarasta užduotimi.

Atkuriama, kontroliniais taškais pažymėta būsena

Įtraukimas-į-eilę-ir-apklausa išsprendžia užduoties suradimą iš naujo; savaime tai neišsprendžia jau atlikto darbo neprarandimo. Ilga inferencijos užduotis — daugiapakopė difuzija, kadras po kadro vykdomas vaizdo apdorojimas, bet kas, kas trunka minutes, o ne sekundes — turi realią, brangią pažangą, kelyje į pabaigą esančią GPU atmintyje, o startavimas iš nulio kiekvieną kartą nutrūkus ryšiui švaisto lygiai tą skaičiavimo galią, kurios ir siekiama nešvaistyti.

Sprendimas — padaryti pažangą patvarią kiekviename reikšmingame žingsnyje, o ne tik gyvoje darbininko atmintyje. Užduočiai judant į priekį, darbininkas įrašo kontrolinį tašką — kuriame žingsnyje jis yra, koks tarpinis rezultatas ar dalinis artefaktas egzistuoja, kas dar liko atlikti — į saugyklą, kuri egzistuoja nepriklausomai nuo bet kokio lizdo, bet kokio vieno darbininko proceso ar bet kokio kliento ryšio. Klientas, kuris prisijungia iš naujo — ar tai po dviejų sekundžių trikdžio, ar po penkių minučių pertraukos — neklausia „ar mano užduotis išgyveno“, jis klausia „koks dabartinis kontrolinis taškas“, ir tęsia savo rodomą būseną — o ten, kur tai leidžia pati užduotis, ir skaičiavimą — nuo to taško, o ne nuo nulio.

checkpoint = {
  job_id: "job_8f2c",
  step: 34,
  total_steps: 50,
  last_artifact_ref: "s3://inference-out/job_8f2c/step_34.png",
  updated_at: "2026-06-30T14:22:07Z",
}

Tipinis patvaraus kontrolinio taško įrašo pavidalas

Kontrolinio taško formatui nebūtina būti egzotiškam — žingsnio skaitiklio, nuorodos į paskutinį patvarų artefaktą ir laiko žymos paprastai pakanka teisingai atsakyti „nuo kur tęsti“. Svarbu tai, kad įrašas gyvena už ribų tos dalies, kuri labiausiai linkusi sugesti — gyvo ryšio.

Ribotas eksponentinis atidėjimas jungiantis iš naujo

Nutrūkęs ryšys vis tiek turi sugrįžti, ir kaip jis sugrįžta, svarbu beveik tiek pat, kiek ir pats faktas, kad sugrįžta. Bandyti jungtis iš naujo iškart ir tvirtu ciklu — klaidingas numatytasis elgesys: darbininkas, atsigaunantis po savo paties trikdžio, ar tinklo kelias, kuris ne miręs, o tiesiog pablogėjęs, apiberiamas pakartotinių bandymų srautu tiksliai tą akimirką, kai mažiausiai gali jį sugerti. Laukti be galo — vienodai klaidinga priešinga kryptimi: klientas, kuris niekada nepasiduoda dėl iš tikrųjų dingusio darbininko, tiesiog kabo ten, o naudotojas, tai stebintis, neturi jokio signalo, kad kažkas tikrai negerai.

Vidurinis kelias — ribotas eksponentinis atidėjimas: kiekvienas nepavykęs bandymas jungtis iš naujo laukia ilgiau nei ankstesnis, iki tam tikros viršutinės ribos, todėl pats pirmasis bandymas yra greitas — tikras trikdis atsigauna beveik nepastebimai — o užsitęsęs sutrikimas atsitraukia iki protingo intervalo, vietoj to, kad toliau apiberinėtų sistemą. Didžiausias bandymų skaičius ar didžiausia praėjusi trukmė riboja visą procesą, todėl iš tikrųjų mirusi užduotis anksčiau ar vėliau, ir sąmoningai, iškyla kaip nesėkmė, o ne bando jungtis iš naujo amžinai fone.

delay = min(base_delay * 2^attempt, max_delay)
delay = delay * random(0.5, 1.0)   # jitter
if elapsed > backoff_ceiling: surface_as_failed(job_id)

Prisijungimo iš naujo atidėjimas: eksponentinis augimas, viršutinė riba ir „jitter“, kad bandymai nesutaptų vienu metu

Schema: inferencijos užduoties atkūrimas — užduotis įtraukiama į eilę, o pažanga apklausiama, o ne pasitikima vienu lizdu; užduoties viduryje nutrūkęs „WebSocket“ suveda ribotą eksponentinį atidėjimą jungiantis iš naujo; klientas tęsia nuo paskutinio patvaraus kontrolinio taško, o ne pradeda iš naujo; didelis dvejetainis rezultatas iškeliamas į S3, vos jis baigiamas; o darbininkas aiškiai išjungiamas užbaigus ar apleidus užduotį, kad atlaisvintų GPU/VRAM.

„Jitter“ lengva praleisti ir brangu praleisti: be jo tinklo trikdis, tuo pačiu metu paveikiantis daug lygiagrečių užduočių, sukelia sinchronizuotas pakartotinių bandymų bangas, kurios atakuoja galinę sistemą vienu ritmu, o ne tolygiai pasiskirsto laike.

Didelių dvejetainių rezultatų iškėlimas į S3

Kai užduotis pasibaigia, ryšys, buvęs mažiausiai patikima visos sistemos dalimi, yra paskutinė vieta, kuriai turėtų tekti atsakomybė pristatyti didžiausią naudingąją apkrovą. Sugeneruotas vaizdas, o ypač sugeneruotas vaizdo įrašas ar kadrų paketas, gali siekti nuo kelių megabaitų iki kelių šimtų megabaitų — lygiai tokio dydžio apkrova, kuri ribinį ryšį paverčia iš „tikriausiai atsigaus“ į „nutrūks kelias minutes trunkančio perdavimo viduryje“.

Vietoj to, kad baigtas dvejetainis rezultatas būtų srautu siunčiamas atgal tuo pačiu kanalu, kuris naudojamas pažangos atnaujinimams, darbininkas jį tiesiogiai įkelia į objektų saugyklą, vos jis sugeneruojamas, o užduoties kontrolinio taško įraše atnaujinama nuoroda į tą objektą, o ne patys baitai. Kliento darbas susiaurėja iki nuorodos parsisiuntimo įprastu, atkuriamu HTTP užklausu, kai užduotis praneša, kad baigta — gerai žinoma problema su brandžiais įrankiais, o ne pasirinktinis perdavimas tuo pačiu trapiu kanalu, kuriam jau ir taip prireikė pakartojimo logikos.

Tai taip pat reiškia, kad užduotis, generuojanti rezultatą palaipsniui — pavyzdžiui, kadras po kadro — neturi laikyti viso rezultato atmintyje ar viename ryšyje iki pačios pabaigos. Kiekviena baigta dalis gali būti iškelta ir susieta nuoroda vos ji atsiranda, todėl vėlyva nesėkmė praranda tik tai, kas dar nebuvo iškelta, o ne viską, kas sugeneruota iki tol.

Aiškus darbininkų išjungimas

Serverless GPU kainodara — tiesioginė funkcija to, kiek laiko darbininkas veikia, todėl darbininkas, kuriam aiškiai nepasakyta sustoti, nesustoja — jis tiesiog kabo, apmokestinamas ir laikantis VRAM, kol kas nors tai pastebi. Palikus tai numanomu dalyku, tas „kas nors“ dažniausiai būna žmogus, patikrinantis sąskaitą — o tai per vėlu.

Išjungimas suveikiamas aiškiai, ir iš daugiau nei vienos krypties. Užduotis, kuri baigiasi, išjungia savo darbininką, kai patvirtinama, kad rezultatas iškeltas. Užduotis, kuri nepavyksta, iškart išjungia savo darbininką, o ne palieka jo veikti „tam atvejui“. O užduotis, kurios klientas taip ir nebesugrįžta — jokio prisijungimo iš naujo, jokios apklausos, nieko ilgiau nei tas pats ribotas laiko langas, kurį jau naudoja prisijungimo iš naujo atidėjimas — laikoma apleista ir išjungiama pasibaigus laiko limitui, o ne paliekama veikti neribotai, darant prielaidą, kad kažkas galbūt vis dar stebi.

Būtent pastarasis atvejis vertas sąmoningo dėmesio. Apleidimo laiko limitas turi būti pakankamai ilgas, kad klientas, teisėtai atsigaunantis po lėto tinklo ar atidėjimo ciklo, nebūtų išjungtas iš po savęs pačiu atkūrimo momentu, bet pakankamai trumpas, kad tikrai apleista užduotis nekabotų, užimdama brangų GPU tuo atveju, jei kas nors sugrįš. Susiejus jį su tuo pačiu laiko horizontu, kurį jau naudoja prisijungimo iš naujo logika, abi taisyklės nepradeda veikti viena prieš kitą.

Kompromisai ir apribojimai

Nė vienas iš šių sprendimų nėra nemokamas, ir verta aiškiai įvardyti, ką jie kainuoja ir ko neišsprendžia:

  • Apklausa turi delsos apatinę ribą, kurios neturi gyvas siuntimo kanalas: net trumpu intervalu pažangos atnaujinimai šiek tiek atsilieka nuo to, ką sveikas lizdas pateiktų akimirksniu.
  • Kontroliniai taškai turi granuliuotumo kompromisą — žymėk juos per retai, ir nutrūkimas praranda daugiau darbo; žymėk per dažnai, ir patys įrašymai tampa papildoma apkrova užduotims, kurios jau ir taip apribotos GPU.
  • Ribotas atidėjimas reiškia, kad pakankamai ilgas sutrikimas anksčiau ar vėliau vis tiek tampa nesėkme. Tai suteikia laiko laikiniems trikdžiams išsispręsti; tai nepaverčia iš tikrųjų mirusio tinklo kelio ar visam laikui dingusio darbininko atkuriamu.
  • Aiškus išjungimas tiek geras, kiek geras už jo stovintis apleidimo aptikimas — suklysk viena kryptimi, ir lėtas, bet teisėtas klientas išjungiamas atkūrimo viduryje; suklysk kita kryptimi, ir iš tikrųjų apleista užduotis toliau apmokestinama ilgiau, nei turėtų.
  • S3 iškėlimas pašalina ryšį kaip kliūtį, bet prideda naują priklausomybę su savo pačios gedimo scenarijumi: pats įkėlimas gali nepavykti ar užstrigti, o užduotis gali pasiekti būseną „baigta“ anksčiau, nei patvirtinama, kad jos rezultatas patvariai išsaugotas, jei ta tvarka sąmoningai neužtikrinama.
  • Visa tai prideda realaus sudėtingumo — eilė, kontrolinių taškų saugykla, atidėjimo logika, iškėlimo taikinys, išjungimo taisyklės — virš to, kas kitu atveju būtų viena užklausa ir vienas atsakymas; tai pateisinama todėl, kad alternatyva — tyliai prarasti jau apmokėtą GPU skaičiavimo laiką, o ne todėl, kad taip paprasčiau.

Šaltiniai ir tolesnis skaitymas

Susijusios atvejų analizės

Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.

Technologijos: RunPod · ComfyUI · WebSockets · S3 offload← Visos atvejų analizės

Turite sistemą, kuri privalo veikti?

Rezervuokite pokalbį — susidėliosime, ką ji turi daryti, kur ji lūš ir kokią mažiausią versiją verta kurti pirmiausia.