Přeskočit na obsah

Jedno číslo pro čtyři modely — strop, který nikdo neporovnal se zdí

Hermes · okno kontextu
Deklarováno všem
65 536
Skutečná zeď lokálního
32 768
Po pouhém smazání
131 072
Cena skutečné opravy
+2,4 GB

Stalo se 10. 9. 2026. Řešil jsem s agentem přes Telegram velikost kontextového okna a on navrhl nastavit globálně 272 000 — tolik, kolik má cloudový model, na kterém běží hlavní chat. To by bylo špatně a odmítl jsem to: pod tímtéž nastavením běží i naplánované úlohy na lokálním modelu a těm by to lhalo. Návrh jsme společně přepsali na „zrušit globální strop a nechat každý běh, ať si okno určí podle svého modelu".

Byl to správný závěr, ale stálo na něm ještě jedno číslo, kterého se nikdo nedotkl: v konfiguraci bylo context_length: 65536. Ne 272 000, ne 32 768. Číslo, které kdysi někdo napsal a od té doby ho nikdo neporovnal s ničím.

Co je vlastně zeď

Lokální server hlásí u nahraného modelu dvě různá čísla:

max_context_length    = 262 144
loaded_context_length =  32 768

První je, co model umí. Druhé je, co mu server v běžící instanci opravdu alokoval — a to je ta zeď, o kterou se požadavky rozbíjejí. Přes standardní OpenAI-kompatibilní /v1/models se ani jedno nedozvíte, ten endpoint metadata nevrací ani s autorizací; jsou až na nativním API serveru. To je nejspíš důvod, proč to číslo nikdo nikdy neověřil: první místo, kam člověk sáhne, mlčí.

Deklarovaných 65 536 tedy bylo dvojnásobkem skutečné zdi.

před — jedno číslo pro všechnyhlavní chat · cloudovývyužito 24 %úlohy · lokální 27B2× přes zeďpo pouhém smazání4× přes zeďpo opravě — okno podle modelu a provideruhlavní chat · cloudovýsedíúlohy · lokální 27Bsedí16 tis.32 tis.64 tis.128 tis.256 tis.512 tis.Osa je logaritmická: každý krok je zdvojnásobení. Vzdálenost mezi značkami se tedy čte přímo jako „kolikrát“.
deklarováno v konfiguraciskutečná zeď

Co se dělo, když se do zdi narazilo

V logu lokálního serveru je 67 odmítnutých požadavků:

request (34833 tokens) exceeds the available context size (32768 tokens)
request (42496 tokens) exceeds …

Rozložené do devíti dnů, od 15 741 do 903 064 tokenů.

Na straně agenta ta chyba není zamlčená — v jeho logu je dvaadvacetkrát, zabalená do obecného varování o selhání volání API:

error_type=APIError … summary=Engine protocol predict stream returned an
error: {"code":500,"message":"Context size has been exceeded.", …}

Jenže chybí to jediné, čím by šlo problém rozpoznat: čísla. Věta „kontext byl překročen" vypadá jako popis příliš dlouhé konverzace, tedy něčeho, co se prostě stává. Teprve dvojice 34 833 proti 32 768 říká, že strop v konfiguraci je jinde než zeď — a ta dvojice existovala jen na druhé straně, v logu lokálního serveru, kam se nikdo nedíval, protože selhávalo přece „připojení k modelu".

Proč samotné smazání nestačí

Nabízí se ten strop prostě zahodit a nechat runtime, ať si poradí. Zkusil jsem, co by se stalo, a je to horší:

Using hardcoded context length 131,072 for model '…-27b'
(custom endpoint, catalog match on 'qwen')

Bez explicitní hodnoty spadne odvozování až na zadrátovaný katalog, který hádá podle jména modelu — a uhodne 131 072, tedy čtyřnásobek. Jedno číslo bylo tedy nahrazeno jiným, taky vymyšleným, jen o řád horším.

Správný nástroj není globální strop ani jeho absence, ale override klíčovaný poskytovatelem a modelem. Sedí v řetězci odvozování hned na začátku, před sondami i katalogem, a je ze své podstaty selektivní: co nepojmenuje, toho se nedotkne. Po nasazení se z jednoho čísla staly dvě pravdivé odpovědi:

běh okno skutečná zeď
hlavní chat · cloudový model 272 000 272 000
naplánované úlohy · lokální 27B 65 536 65 536

Pořadí kroků přitom není libovolné. Kdybych nejdřív smazal a teprve pak deklaroval, mezistav by běžel na těch nesmyslných 131 072.

Model, který nikdo neobjednal

Při té příležitosti vyšlo najevo, proč dvě naplánované úlohy měly roky v konfiguraci nesmysl: poskytovatel lokální server, ale model cloudové mini. Takový model na tom serveru neexistuje. Vyzkoušel jsem, co odpoví:

{ "model": "…-27b", "choices": [ … ] }

Požadavek na neexistující model tiše obslouží ten nahraný. Žádná chyba, žádné varování. Proto ty úlohy roky končily úspěšně a nikoho nenapadlo se na ně podívat.

Log agenta to neodhalí, protože zapisuje co se zeptalo, ne co odpovědělo. Poznat to jde jedině nepřímo — po latenci: první volání trvala čtyři minuty, což je lokální model, ne cloudové mini, které odpovídá v sekundách.

Dokud platil globální strop, byl ten nesoulad kosmetický. Ve chvíli, kdy okno začalo záviset na jménu modelu, se z něj stala díra: jméno cloudového mini se odvodí na 400 000 proti zdi 65 536. Moje vlastní oprava tedy tu chybu nejdřív zvětšila a teprve dodatečná kontrola ji odhalila.

Co jsem přitom udělal špatně

Změřil jsem no-op a ohlásil to jako nález. Měl jsem hypotézu, že server drží čtyři paralelní sloty, z nichž se reálně používají dva, a že se dá okno zdvojnásobit zdarma výměnou za nevyužitou paralelitu. Načetl jsem model se změněnými parametry, změřil paměť, viděl skoro stejné číslo a napsal „sloty jsou zdarma". Jenže načtení modelu, který už je načtený, server tiše ignoruje. Identifikátor procesu se nezměnil — měřil jsem pořád tentýž běh. Kdybych se na to číslo podíval dřív než na svůj závěr, ušetřím si to.

A nástroj na odhad odpověděl na jinou otázku. Server má funkci „spočítej, kolik to zabere, ale nenačítej". Vrátila 16,52 GiB. Pro všechny tři varianty, které jsem zkoušel, stejně — počítá totiž jen váhy modelu, ne cache, tedy právě to jediné, na co jsem se ptal. Odpověď vypadala jako odpověď.

Skutečná cena se ukázala až měřením běžícího procesu: 19,81 GB → 22,18 GB, tedy +2,4 GB za dvojnásobné okno. Váhy z toho tvoří 15,66 GB, zbytek je cache a buffery. Není to zadarmo, jak jsem tvrdil.

Co se pro to změnilo
  • configglobální `model.context_length` zrušen; okno se odvozuje z modelu a provideru každého běhu
  • configskutečné okno lokálního serveru deklarováno v `model_overrides`, klíčované provider→model
  • postupběhový stav (kolik má nahraná instance alokováno) se čte z běžící instance, ne z toho, co model umí
Souvisí — štítek „provoz“