Vissza a blogra

Nemdeterminisztikus Qwen3.8-Flash-Next: megtaláltuk a hibás vLLM-kernelt, és megjavítottuk

Temperature 0, azonos prompt, futásonként más kimenet egy DGX Sparkon. A gyökérok a sparse-attention indexer persistent_topk kernele; a javítás egy kernelcsere, 1,35× prefill-áron.

Greedy dekódolásnál (temperature=0) ugyanarra a promptra ugyanannak a kimenetnek kell jönnie. Nálunk nem így volt: a Qwen3.8-Flash-Next (RadixArk NVFP4 checkpoint) vLLM-en, GB10-es DGX Sparkon 50 feladatból 13-nál futásonként más kimenetet adott, és ötnél a kinyert dátum vagy összeg is más lett. Ez a cikk arról szól, hogyan szűkítettük le a hibát egyetlen CUDA-kernelre, mibe került a javítás, és mi minden derült ki menet közben, ami addig zajnak látszott.

Röviden:

  • Nem a mérés és nem a modell: három kontrollmotor, köztük ugyanez a checkpoint llama.cpp-n, 0/50. A hiba a kiszolgáló stackben van.
  • A gyökérok a QSA sparse-attention indexer persistent_topk CUDA-kernele: versenyhelyzetben futásonként más top-2048 pozícióhalmazt választ, ezért már a prefill logitjai sem determinisztikusak.
  • A javítás: a kiválasztás cseréje torch.topk + kanonikus rendezésre (egyfájlos overlay, VLLM_QSA_EXACT_TOPK=3). Bitre determinisztikus, a prefill 2 360 → 1 760 tok/s (1,35×), az MTP és a cudagraph marad. A fő suite 98,00/100 (az elérhető maximum), 0/50 instabil; ez egy ponttal jobb is a hibás kernelnél, mert ott a többségi szavazás egy rossz dátumot hozott ki győztesnek.
  • Ráadás-lelet: a determinisztikus prefill láthatóvá tett egy második, addig véletlennek álcázott hibamódot (greedy + thinking ismétlési hurok), és kiderült, hogy az MTP spekulatív dekódolás nem kimenet-ekvivalens a greedy-vel ezen a modellen.
  • Gyorsteszt a saját stackedre: ugyanaz a prompt tízszer, temperature=0, max_tokens=1, top_logprobs=20, egyszerre egy kérés. Ha a top-20 logprob-lista nem bitre azonos, a prefilled nemdeterminisztikus.

Környezet

  • Modell: RadixArk/Qwen3.8-Flash-Next-NVFP4; 177 B paraméteres MoE, amiből ~51 milliárd egy n-gram tábla (PLE). Ez a memóriába nem fér, ezért mmap-pel lemezről megy.
  • vLLM 0.1.dev20073+g8e685d198 + a blazux/qwen3.8-Flash-DGX recept (82ed48d) mmap-PLE patche (a QSA-kódutat nem érinti)
  • Hardver: GB10 / DGX Spark, driver 580.173.02, 121 GB egyesített memória
  • Indítás: --max-model-len 262144, --max-num-seqs 2, --no-enable-prefix-caching, chunked prefill 8192, cudagraph PIECEWISE, MTP=2 spekulatív dekódolás, NVFP4 MoE backend FLASHINFER_CUTLASS
  • Mérés: magyar dokumentum-kinyerési (KIE) eval, 65 feladat, mindegyik greedy, a fő 50 item háromszor futtatva

Hogy vettük észre

A harness minden itemet háromszor küld el, a futásokat külön pontozza, és a kimenetek SHA-ját is tárolja. Itt jelent meg az, aminek greedy dekódolásnál nem szabadna léteznie: 13/50 itemen futásonként eltérő kimenet. A 13-ból ötnél a kinyert érték is más lett: határidő 2026-11-10 az egyik futásban, 2026-11-05 a másikban; végösszeg 6 300 000, majd 5 940 000. Egy itemnél a három futásból kettő tévedett, vagyis a többségi szavazás a rossz választ hozta volna ki győztesnek. Két futás pedig 16 384 tokenen át üres gondolkodásba szállt el (finish_reason=length, üres tartalom); erről később kiderült, hogy önálló hibamód, nem a kernelhiba tünete.

Ugyanez az itemsor, ugyanez a harness, három kontrollmotoron:

motorinstabil item
Flash-Next IQ4_XS · llama.cpp (ugyanaz a modell!)0/50
Qwen3.6-35B FP8 · vLLM · MTP=20/50
Qwen3.5-122B NVFP4 · vLLM0/50
Flash-Next NVFP4 · vLLM · MTP=213/50

Ellenőriztem, hogy nem én rontottam el

Mielőtt a stacket gyanúsítanám: a payload minden motoron explicit temperature: 0.0, top_p: 1; a promptok statikusak, a kérés a futásciklus előtt épül fel egyszer; a vLLM-naplóban végig Running: 1 reqs, tehát a köteg összetétele nem változhatott. A hossz-hipotézist („a hosszabb generálás instabilabb”) az adat cáfolta: az instabil és a stabil itemek prompthossza gyakorlatilag azonos (medián 6 038 és 6 072 token), és egy 10 178 tokenes generálás is bitre azonosan ismételt háromszor.

Hol ágazik el

A harness a gondolkodási tartalmat (reasoning_content) is tárolja, így megnézhető, hol válik szét a kimenet: 13-ból 12 itemnél a gondolkodás legelső tokenjeinél, nem felhalmozódó driftként. A modell futásonként más nyelven kezd gondolkodni ugyanarra a magyar kérdésre:

futás 0:  A felhasználó azt kérdezi, hol találom meg azt a szabályt…
futás 1:  We need answer user's question in Hungarian, based only on…

Greedy dekódolásnál az első token a prefill kimenete. Determinisztikus prefill mellett egy holtverseny is mindig ugyanúgy dől el; itt nem így történt, tehát a prefill logitjai változnak futásról futásra, a dekódolás csak örökli.

Egyszerre egy kapcsoló

Izoláció a 13 instabil itemen, futásonként ötször, körönként pontosan egy változtatással:

kapcsolóinstabilítélet
spekulatív dekódolás ki (MTP=0)11/13nem gyökérok
cudagraph_mode=NONE13/13kizárva
az indexer top-k kernelének cseréje0/13gyökérok megerősítve

Mellék-leletként az is feltűnt, hogy az üres, 16 384 tokenes elszállás csak MTP alatt fordult elő (MTP nélkül 0/65 futás); hogy ez valójában mit jelent, a validációnál derült ki (lásd a második hibamódot lentebb).

A szonda, ami két perc alatt megmutatta volna

A suite-nál sokkal érzékenyebb és olcsóbb műszer egy prefill-szonda: azonos prompt, temperature=0, max_tokens=1, logprobs=true, top_logprobs=20, tízszer, egyszerre egy kérés, majd a top-20 (token, logprob) listák bitre hasonlítása. Hat prompton futtattuk, a stock és a cserélt kernellel:

stock persistent_topktorch.topk + kanonikus rendezés
10/10 bitre azonos top-20 vektor0/6 prompt6/6 prompt
első token3/6 prompton futásonként másfix
top-2 logit-rés futások közöttakár 3 nat ingadozásállandó

A kulcs-lelet: a szondába két, a suite-ban végig stabilnak látszó itemet is beválogattunk, és azok prefillje is tíz futásból tízféle logit-vektort adott. A nemdeterminizmus tehát univerzális volt; a suite csak ott mutatta meg, ahol két token holtversenyben állt. Ezért érdemes minden új kiszolgáló-receptet előbb a szondával mérni, és csak utána suite-tal: két perc, és a batch-hatásoktól függetlenül, közvetlenül a prefill determinizmusát méri.

Mi a persistent_topk, és miért pont a Sparkon

A Flash-Next a hosszú kontextust ritka figyelemmel (QSA) kezeli: egy indexer minden lekérdezéshez kiválasztja a 2048 figyelendő pozíciót. A kódút a vLLM-ben qsa.py::qsa_select_paged_tokens, és GB10-en (compute capability family 12x) a use_cooperative_topk ág hamis, így a torch.ops._C.persistent_topk kernel fut, amely a slot-kiosztást atomikus műveletekkel végzi (42 atomicAdd a kernelforrásban).

Hogy a hiba nem csak a slotok sorrendje, azt egy köztes kísérlet zárta ki: a stock kernel kimenetét érintetlen halmazzal, utólag kanonikusan újrarendezve 6-ból 3 prompt továbbra is instabil maradt. A kernel versenyhelyzetben a kiválasztott halmazt változtatja, nem csak a sorrendet: a figyelem futásonként más kontextusból dolgozik. Ez konzisztens a kernel ellen már nyitva lévő korrektségi hibajeggyel (vLLM #51782: a hisztogram-alapú kiválasztás közös binbe eső értékeknél némán elejt valódi top-k jelölteket); a bin-határon lévő versenyhelyzet futásonként más jelölteket tart meg, és a leghosszabb, 24k tokenes prompt volt a legrosszabb. A logit-eltérés mértéke (0,25–3 nat) is ezt igazolja: ez nem lebegőpontos kerekítési zaj, hanem más attention-kontextus.

A javítás ára

A csere egyfájlos overlay a telepített qsa.py-ra, env-kapcsolóval: VLLM_QSA_EXACT_TOPK=3 = torch.topk(sorted=False) radix-kiválasztás, majd két stabil rendezés (index szerint növekvő, érték szerint csökkenő), -1 padding, ha length < k. Egységteszten és a szondán bitre azonos a teljes sortos referenciával.

stock persistent_topkteljes sorttorch.topk + rendezés
determinisztikusnemigenigen
prefill, 6k prompt2 360 tok/s8201 760
lassulás2,9×1,35×

A csere csak a prefill-oldali indexert érinti: az MTP és a PIECEWISE cudagraph marad, a suite kimeneti sebessége 26,4-ről 24,9 tok/s-ra változott (a válaszidő-alapú mérőszámban a lassabb prefill is benne van). A 13 instabil item a javítással 0/13, öt futás bitre azonos. A teljes újramérés minden suite-on:

suitestock persistent_topkjavított (3. mód)
fő (50 item × 3 futás)97,00 · 13/50 instabil · 1 csonkolt98,00 · 0/50 instabil · 0 csonkolt
nehéz (10 × 3)100,00 · 0/1090,00 · 0/10 · 1 hurok-item
hosszú, 217k token (5 × 1)100,00100,00

A fő suite 98,00 pontja az elérhető maximum (a pontozó két LLM-bírói pontja technikai okból egyik motornak sem jár), és a pontszám a javítással nőtt: a stock kernel alatt a T3-05 itemen a többség a rossz dátumot szavazta meg, a determinisztikus futás a helyeset adja. A nehéz suite 90 pontja mögött pedig nem a top-k csere regressziója áll, hanem egy második hibamód, amit éppen a determinizmus tett láthatóvá; erről a következő szakasz szól.

A maradék 26% prefill-költség a nem natív kiválasztás ára. Az upstream kérésünk a vLLM felé: determinisztikus (index-sorrendű tie-break) út a persistent_topk-ban, ahogy a FlashInfer tette a saját sparse-attention top-k-jánál (flashinfer #2661), vagy dokumentált kapcsoló az egzakt kiválasztásra. A hibát a reprodukcióval és a javítással együtt a recept repójában jelentettük: blazux/qwen3.8-Flash-DGX #3.

A determinizmus mellékterméke: egy második, addig rejtett hibamód

A nehéz suite hurok-itemje mindhárom futásban ugyanúgy szállt el: 16 384 token gondolkodás, üres válasz, bitre azonos tartalom. A gondolkodás vége ugyanaz a ~180 karakteres bekezdés 22-szer ismételve: a modell a kimeneti séma egyik mezőjének formátumán pörög, és sosem zárja le a gondolkodást. Ez nem a top-k kernel hibája, hanem a greedy dekódolás és a thinking mód ismert rossz párosítása (a Qwen dokumentációja kifejezetten nem ajánl greedy-t thinking módhoz).

A tanulságos rész: ez a hibamód a hibás kernel alatt is létezett, csak véletlennek látszott: 150 futásból egyszer jött elő, és a nyelvértési tesztsorban is más-más tételen (a stock kernellel az egyik, a javítottal egy másik feladaton). A nemdeterminisztikus kernel zaja ugyanis néha kilökte a modellt a hurokból. A determinisztikus prefill tehát nem okozta ezt a hibát, hanem reprodukálhatóvá tette: ami eddig szórványos, foghatatlan jelenség volt, az most 3/3 futáson azonosan jelentkezik, így detektálható (ismétlési arány a gondolkodásban) és kezelhető (újrapróba T>0-val, presence_penalty, vagy a prompt egyértelműsítése).

Az item-szintű utóteszt egy második beállítási tanulságot is adott: a javított kernel mellett, MTP nélkül a hurok eltűnik (10/10 futás, 864 token, helyes JSON), és az MTP=2 gondolkodása már az ötödik token környékén letér az MTP=0 pályájáról. Vagyis a spekulatív dekódolás ezen a modellen és builden nem kimenet-ekvivalens a tiszta greedy dekódolással; a hurokba a letérített pálya vitte bele a modellt. A suite-szintű MTP-összevetés még fut, ezért ennél többet nem állítunk róla.

A pontos összegzés tehát nem az, hogy a kernelcsere minden hibát megoldott, hanem ez: a nemdeterminizmus megszűnt, és ezzel két, addig zajnak álcázott beállítási probléma (a greedy + thinking hurokhajlam és az MTP-letérés) vált mérhetővé és kezelhetővé.

Mellékszál: és akkor melyik modell a jobb?

A mérés eredetileg négyes összehasonlítás volt a 128 GB-os, saját vason futtatható kategóriában, 65 magyar feladaton: Flash-Next NVFP4 297/300, Qwen3.6-35B FP8 (a jelenlegi prod) 294, Flash-Next IQ4_XS 289,33, Qwen3.5-122B 275. A sorrendről lényegében négy item dönt, a mezőny ennyire szoros. Két üzemi adat a pontszámok mögül: az IQ4_XS-t egyetlen nehéz itemen kvantálási kár húzza le (3,33/10 ott, ahol az NVFP4 hibátlan), a 217 ezer tokenes iratcsomagot viszont a vLLM-stack 157 s alatt dolgozza fel, a llama.cpp 1 121 s alatt. A részletes összevetés külön cikk lesz; a lényeg itt annyi, hogy a legjobb pontszámú konfiguráció volt az egyetlen nemdeterminisztikus, és ezt a ranglistából nem lehet látni.

Összefoglalva

Egy greedy futás, ami futásonként mást ad, nem „LLM-es bizonytalanság”, hanem hiba a stackben, és megkereshető. Nálunk egy sparse-attention top-k kernel volt, és a javítás egyetlen fájl meg egy környezeti változó. Ami ebből a konkrét modellen túl is megmarad:

  1. Greedy alatt az eltérő kimenet mindig hibajel, sose természetes szórás. Ha a kontrollmotorok 0/50-et adnak, a hiba a kiszolgálóban van, nem a modellben és nem a mérésben.
  2. A prefill-szonda olcsóbb és érzékenyebb, mint a suite. Tíz kérés, egy token, top-20 logprob: két perc alatt megmutatja, amit a suite csak a holtversenyes itemeken hoz elő. Új kiszolgáló-receptnél előbb ezt futtasd.
  3. A ranglista nem mutatja a determinizmust. A négy jelölt közül a legjobb pontszámú volt az egyetlen instabil; egy pontszám mellé stabilitás-mérés is kell.
  4. A determinizmus nem csak javít, hanem leleplez is. Ami zajos rendszerben szórványos hibának látszik (hurok, MTP-letérés), az determinisztikus rendszerben reprodukálható, tehát kezelhető.

A mérés GB10 / DGX Sparkon készült (driver 580.173.02, 121 GB egyesített memória), vLLM 0.1.dev20073+g8e685d198 alatt, a RadixArk/Qwen3.8-Flash-Next-NVFP4 checkpointtal, MTP=2 spekulatív dekódolással és PIECEWISE cudagraph móddal. A korpusz 65 magyar dokumentum-kinyerési feladat, mindegyik greedy; a fő 50 item háromszor futott, az izoláció a 13 instabil itemen ötször, a prefill-szonda hat prompton tízszer. A kontrollmotorok ugyanazt az itemsort kapták ugyanazzal a harness-szel. A suite-szintű MTP-összevetés a cikk írásakor még fut, ezért az MTP-re vonatkozó állítás item-szintű utótesztre épül.