De ring verhuizen naar p4
Zeven contractwijzigingen tussen een engine en zijn aanroeper. Elk ervan faalde anders, en maar één ervan zag eruit als een fout.
We hebben een nieuwe release van de p4-engine samengevoegd en de ring stopte met serveren. Niet met een crash — de lader meldde succes, de agents meldden ready, en er gebeurde niets. Terugwerken vanuit die stilte kostte een dag en leverde zeven plekken op waar onze aanroeper en de engine uit elkaar waren gedreven. Wat ze het opschrijven waard maakt is niet het aantal. Het is dat zes van de zeven geen enkele fout opleverden.
Een event voor een node die niet bestaat wordt doorgestuurd, niet geweigerd
Onze lader adresseerde het LOAD-commando aan de node die hij wilde aanmaken. Maar een node bestaat niet totdat LOAD hem aanmaakt, en de regel van de broker voor een event dat een onbekende node noemt is om het naar buiten door te sturen in plaats van het te weigeren. Het commando verliet de agent op zoek naar ergens anders om heen te gaan, vond niets, en werd weggegooid. Geen logregel, want vanuit het standpunt van de broker was er niets misgegaan.
De oplossing was om LOAD aan de *agent* te adresseren, verpakt in de backend-neutrale levenscyclus-envelop van de engine, met het eigen commando van de adapter als ondoorzichtige body. Achteraf vanzelfsprekend; van buitenaf onzichtbaar.
Een getal dat gelijk moet zijn aan een ander getal
Een model wordt geladen onder een load generation, en elke node wordt geregistreerd met een node generation. Wij behandelden die als onafhankelijk — een timestamp voor de een, 1 voor de ander — en alles werkte. De ring laadde. Hij beantwoordde een verzoek correct. Toen stierf de kopnode.
De check, in de release-boekhouding van de adapter.
let Endpoint::Node { generation, .. } = &event.envelope.source;
if *generation != receipt.load_generation {
return Err("release owner census generation differs from source");
}De bon die een afgerond verzoek afsluit draagt de load generation, en de node die hem verstuurt draagt de zijne. Verschillen ze, dan wordt de node gestopt. De vorm van de bug is dus: laden slaagt, het eerste verzoek slaagt, de kop sterft, en elke sessie daarna blijft halfgeladen hangen. Dat leest precies als een crash onder belasting en totaal niet als een mismatch. Onze lader weigert nu een plan waarvan de twee getallen niet overeenkomen, vóórdat er iets geladen wordt.
Een grens die nooit gehaald kon worden
De adapter vergelijkt het grootste resultaat dat een stage mag teruggeven met het getal dat de stage-server meldt zodra hij opkomt, op exacte gelijkheid. Dat getal konden we niet kennen zonder het model te laden — dus laadden we met een gok, en het falen vertelde ons de echte getallen van alle vier de stages in één keer:
Eén run, vier antwoorden.
step37-s0: profile=33554432, READY=34419218444 step37-s1: profile=33554432, READY=34419218444 step37-s2: profile=33554432, READY=34419218444 step37-s3: profile=33554432, READY=59136012
34 GB. De behouden stores van de agent zijn 256 MiB, dus die ring had nooit toegelaten kunnen worden. De afleiding uit de stage-server lezen liet zien waarom: de grens schaalt met n_batch × n_ubatch, en wij hadden een batchbreedte van 2048 geërfd uit een configuratie van vóór deze check. Bij 128 rijen — de breedte die de productie-layout gebruikt — is de grens 138 MB en past hij wél. We leiden hem nu in het plan af uit dezelfde formule in plaats van een onthouden constante mee te dragen, en het voorspelde getal van de staartstage kwam exact uit op het getal dat een andere deployment had vastgelegd — het soort overeenstemming dat je wilt hebben voordat je twintig minuten aan een load besteedt.
Kort
- Loopback-adressen in een ring van twee hosts. Een stage belt de agent van de volgende stage op het adres dat die agent adverteert. Adverteer
127.0.0.1en host A belt zichzelf. De vastgelegde configuratie van de ring was al die tijd loopback — hij had nooit over hosts heen kunnen werken. - Het journaal is verplicht. Een model laadt niet zonder het operationele journaal van de agent. We zetten het uit terwijl we een andere fout achtervolgden, en maakten het symptoom erger op een manier die op vooruitgang leek.
- De apparaatnaam is backend-specifiek. De referentie-planbouwer mikt op CUDA en zendt
--device CUDA0uit. De HIP-build noemt zijn apparatenROCm0. Dat plan laadt het hele model en vindt *daarna* het apparaat niet. - De native server hoort bij de release. Een agent die uit een nieuwere tree is gebouwd wil capaciteiten die de geïnstalleerde stage-server niet meldt. Ook ontdekt na een volledige modelload.
Stilte is de dure faalmodus
Elk van deze was goedkoop te repareren en duur te vinden, en het patroon is consistent: de kostbare fouten waren die waarbij een correct ogend systeem niets deed, of iets één keer deed. De guards die we hebben toegevoegd hebben allemaal dezelfde vorm — vroeg weigeren, op de plek waar de vergissing nog leesbaar is. De lader schrijft de load generation naar schijf *voordat* het eerste commando vertrekt, want anders is hij onherstelbaar. Hij weigert een generation-mismatch in plaats van hem na het eerste verzoek te ontdekken. Hij leidt de resultaatgrens af in plaats van hem te onthouden.
De ring serveert. Vier stages over twee machines, 113 GiB aan gewichten resident, het eerste token in 1.4 s koud en ~0.3 s warm, en bijdrage per node die voor het eerst doorstroomt naar het verrekeningsgrootboek.