LOMONOSOV-ZENIT-27B-1M-INDEV / receipts /ADA_FLAGLESS_OPEN_DEFECT.md
Ddavidich's picture
Unpinning the KV arena was tested and rejected: it breaks the million everywhere
9f822ad verified
|
Raw
History Blame Contribute Delete
32.9 kB

Дефект: запуск без флагов падал при памяти уровня 4090 — починка найдена и замерена

Дата: 2026-07-26. Автор: Opus 5. Состояние: починка проверена, к внедрению. Квитанции: flagless_ada_equiv.json (отказ), fix_full.json (починка) на арендованной RTX 5090.


ПОЧИНКА: две правки вместе, проверено штатным путём

raw_1010k не влезает (23.03 ГиБ доступно) -> выбран long_262k
long_262k просит долю 0.95, свободно 23.50 из 31.84 -> прижато до 0.723
PASS, окно 262 144, чанк 4096, кодек turboquant_3bit_nc, модель ответила
значение
свободно на отборе 25 229 197 312меньше, чем 25 757 220 864 у 4090 всего
выбранное окно 262 144
доля памяти 0.723 (прижата с 0.95)
загрузка 73.7 с

Правки:

  1. long_393k.requires_free_bytes25 571 033 088 (замеренный порог: работает отсюда, не работает при 25 371 803 648). Теперь отбор отвергает его на 24 ГБ.
  2. long_262k.gpu_memory_utilization0.95. Без этого поля clamp_utilization возвращает None и прижимать нечего.

Ни одной переменной окружения профиля не задавалось — проверялся именно тот путь, которым пойдёт пользователь. Конфиг после прогона возвращён и возврат проверен.

Одной правки недостаточно, обе нужны вместе. Отдельная проверка «дописать только долю» провалилась, и провалилась дважды по разным причинам: первый раз скрипт был помечен «с явной долей», но долю нигде не задавал; второй раз доля была задана, но профиль форсировался через ZENIT_SERVING_PROFILE, а при явно затребованном профиле прижатие не срабатывает.

Требование long_262k тоже занижено — замерено

Оно объявляет requires_free_bytes = 22 378 037 226 (20.84 ГиБ). Лестница:

свободно на отборе итог
20 470 759 424 (19.06 ГиБ) отказ — ожидаемо, меньше самих весов с ареной (19.79)
21 471 100 928 (20.00 ГиБ) отказ
22 471 442 432 (20.93 ГиБ) отказ — а это уже БОЛЬШЕ объявленного требования

Третья строка доказывает занижение: памяти больше, чем профиль просит, а он не поднимается. Значит после починки long_393k дефект просто переехал бы на карту поменьше — там отбор дошёл бы до long_262k, и тот бы упал.

Верхние ступени идут. Прогноз, записанный до чисел: если запас сверх весов и арены такой же, как замерен у long_393k (нужно от 2.42 ГиБ), порог лежит около 23.85 млрд байт.

Это заодно разрешит вопрос, который я оставил открытым выше: постоянен ли запас. Прежний вывод «не постоянен» опирался на оценку свободной памяти безголовой 5090, которую я никогда не мерил. Третья замеренная точка решит по-настоящему.



Что случилось

Балласт в отдельном процессе занял память на 5090 так, чтобы свободными осталось 25 229 197 312 байт — на полгигабайта меньше, чем 25 757 220 864 байта, что есть у 4090 всего. Затем LLM(model=...) без единого дополнительного аргумента.

Блок профилей отработал правильно, и это надо сказать прямо:

raw_1010k не влезает (23.03 ГиБ доступно) -> выбран long_393k, окно 393 216
long_393k просит долю 0.95, свободно 23.50 из 31.84 -> прижато до 0.723
профиль подал: kv_cache_memory_bytes=5200000000, skip_mm_profiling=True, ...

Дальше движок: «Initial free memory 23.0 GiB, reserved 4.84 GiB for KV Cache as specified by kv_cache_memory_bytes and **skipped memory profiling. This does not respect the gpu_memory_utilization config.**»

И падение на автотюнере FP4: OOM detected, falling back to default tactic, затем torch.OutOfMemoryError: Tried to allocate 272.00 MiB. GPU 0 has ... 149.06 MiB free, и Engine core initialization failed.

Почему это важно, а не просто «мало памяти»

Прошлый успешный прогон на живой 4090 шёл мимо блока профилей. ADA_SM89_RECEIPT передавал --max-model-len и долю 0.92 явно, получил арену на 365 524 токена и оставил 1.09 ГиБ свободными. То есть путь с флагами работает, а путь без флагов — тот самый, который карточка объявляет главным достоинством, — на той же памяти падает.

Механизм понятен из лога. Закреплённая арена kv_cache_memory_bytes отменяет профилирование памяти, поэтому прижатая доля 0.723 ни на что не влияет: вместо неё берётся жёсткие 4.84 ГиБ под кэш. Веса 16.2 ГиБ плюс арена 4.84 ГиБ — это 21.04 ГиБ, и на активации с рабочей памятью автотюнера остаётся около двух, чего не хватило.

Ирония в том, что закреплённая арена появилась ради надёжности: без неё vLLM гонял свой профилирующий проход, а он падал в humming_gemm с CUDA_ERROR_INVALID_VALUE. Лечение одного отказа поставило другой.

Чего этот замер НЕ доказывает

Что падает настоящая 4090. Мой движок видел 23.0 ГиБ свободных, а безголовая 4090 даёт около 23.9: два лишних контекста CUDA (мой родительский процесс 498 МиБ и процесс балласта) делают пробу жёстче настоящей карты почти на гигабайт.

Откуда 23.9. Из ADA_SM89_RECEIPT: при доле 0.92 движок взял арену на 365 524 токена, то есть 4.35 ГиБ при 12 785 Б/токен, плюс веса 16.2 ГиБ, и оставил free_bytes_after_load = 1 169 948 672, то есть 1.09 ГиБ. Сумма 21.64 ГиБ занятого и 1.09 свободного при полных 23.99 ГиБ карты даёт изначально свободные почти 23.9.

Первая оценка в этом файле («23.5–23.7») была занижена, я её исправил. Разница существенна: гигабайт — это ровно тот масштаб, на котором отказ переворачивается, и потому ступени лестницы 26.5 и 27.25 ГБ (движку достанется примерно 23.7 и 24.4 ГиБ) обхватывают настоящую 4090 с обеих сторон.

Но и обратного он не доказывает. Запас здесь меньше гигабайта, и до сегодня никто его не мерил, потому что все пробы передавали параметры явно и шли мимо профилей — та же слепая зона, которая в прошлый раз спрятала четыре ошибки.

Лестница: порог найден с точностью до 199 МБ

Все числа — свободная память на отборе, то есть то, что видит torch.cuda.mem_get_info() до запуска движка. Именно с этой величиной сравнивается requires_free_bytes.

свободно на отборе итог окно доля
25 229 197 312 (23.50 ГиБ) FAIL
25 371 803 648 (23.63 ГиБ) FAIL
25 571 033 088 (23.81 ГиБ) PASS 393 216
25 971 589 120 (24.19 ГиБ) PASS 393 216 0.744
26 720 272 384 (24.88 ГиБ) PASS 393 216 0.766
27 471 052 800 (25.58 ГиБ) PASS 393 216 0.787

Порог: между 25 371 803 648 и 25 571 033 088. Профиль объявляет requires_free_bytes = 24 053 735 802 — то есть занижает примерно на 1.4 ГБ.

Что это значит для настоящей 4090

У неё всего 25 757 220 864 байта. Из них уходит драйверный резерв и контекст CUDA процесса, где работает отбор, — в сумме порядка 0.5–0.7 ГБ. Значит на отборе остаётся примерно 25.0–25.2 млрд, а порог — 25.5 млрд.

Не хватает нескольких сотен мегабайт. Оговорка честная: настоящей 4090 у меня нет, и величина драйверного резерва на ней не замерена, а замерена только на 5090. Но даже самая благоприятная оценка кладёт 4090 вплотную к порогу, где её сдвинет любая мелочь — графическая сессия, другой драйвер, вторая программа на карте.

Установлено твёрдо, независимо от точного порога: профиль объявляет requires_free_bytes = 24 053 735 802 (22.40 ГиБ), а при 25 229 197 312 байтах свободных (23.50 ГиБ) не работает. Объявленное требование занижено минимум на 1.09 ГиБ. Отбор из-за этого пропускает профиль на картах, где он не поднимется.

Заметить стоит и то, что даже в проходящих случаях доля прижимается до 0.744 и 0.766: объявленные профилем 0.95 недостижимы ни в одном сценарии с 24-гигабайтной картой.

Это одна заниженная константа, а не одно плохое число

Числа взяты из самого движка, а не из моих оценок:

gpu_model_runner: Model loading took 16.44 GiB memory
gpu_worker:       Initial free memory 23.0 GiB, reserved 4.84 GiB for KV Cache  -> ОТКАЗ
gpu_worker:       Initial free memory 23.7 GiB, reserved 4.84 GiB for KV Cache  -> РАБОТАЕТ

Веса 16.44 плюс арена 4.84 — это 21.28 ГиБ. Значит сверх них нужно больше 1.72 и достаточно 2.42 ГиБ на активации, рабочую память ядер и автотюнер FP4.

Сколько закладывают профили (при тех же 16.44 ГиБ весов):

профиль объявлено веса + арена заложено сверх замер говорит
long_262k 20.84 ГиБ 19.79 1.05 мало
long_393k 22.40 ГиБ 21.28 1.12 мало, замерено
raw_1010k 29.97 ГиБ 28.69 1.28 мало

Константа всё-таки есть — замерено, и это отменяет мою же ретракцию ниже

Порог long_262k замерен лестницей: отказ при 23 461 298 176, работает от 23 970 906 112. Теперь есть две настоящие точки, и запас считается без всяких оценок:

профиль веса + арена запас при отказе запас при работе
long_262k 21 252 315 587 2 208 982 589 2 718 590 525
long_393k 22 852 315 587 2 519 488 061 2 718 717 501

Совпадение до 127 килобайт, то есть до 0.005%. Запас постоянен и равен примерно 2.72 ГБ независимо от профиля.

Раздел ниже, где я это опроверг, неверен. Он опирался на оценку свободной памяти безголовой 5090, которую я никогда не мерил, — и я сам отметил тогда, что это оценка, но вывод всё равно сделал. Замер восстановил гипотезу, которую я поспешил снять. Текст оставляю как есть, чтобы ход рассуждения был виден.

Тонкость единиц: отбор сравнивает не с сырой свободной памятью

memory_budget заранее скидывает свободную память на _UTILIZATION_SAFETY = 0.98, и скидка задумана: в комментарии к коду записан замер, где прогон видел 29.06 ГиБ при отборе, а к моменту собственной проверки vLLM оставалось 28.45 — графическая сессия разрастается, пока движок грузится.

Значит requires_free_bytes сравнивается с free × 0.98, а мои пороги замерены в сырой свободной памяти. Одно на другое не делится молча:

профиль требование нужно сырых замерено, что работает от отбор строже на
long_262k 23 972 315 586 24 461 546 516 23 970 906 112 490 МБ (2.0%)
long_393k 25 572 315 586 26 094 199 578 25 571 033 088 523 МБ (2.0%)

Оставляю как есть, и это осознанное решение, а не недосмотр. Замер снимался с неподвижным балластом, а на живой машине память подтекает между отбором и запуском — ровно на те 0.5 ГБ, что даёт скидка. Профиль предлагается только когда сверх замеренного минимума есть подушка на этот дрейф.

Цена: на картах со свободной памятью между 25.57 и 26.09 млрд байт выберется 262 144 вместо 393 216, хотя 393 216 там поднялся бы. Полоса узкая, а несимметричность очевидна: предложить профиль, который падает, много хуже, чем предложить окно поуже.

Что из этого ставить в конфиг

профиль ставить объявлено занижено на
long_262k 23 972 315 586 22 378 037 226 1.59 ГБ
long_393k 25 572 315 586 24 053 735 802 1.52 ГБ
raw_1010k не трогать 32 176 640 076

raw_1010k: замерен настолько, насколько инструмент вообще способен

Сперва я записал, что проверить его нечем — «карты больше 32 ГиБ нет». Это была ошибка рассуждения: порог ищется и снизу, занижением балласта. Лестницу я прогнал, и вот что она дала.

Свободно на карте без балласта — 33 136 836 608. Профиль форсировался (requires_free_bytes=1), иначе лестница мерила бы не его.

свободно итог
32 470 663 168 отказ
32 072 204 288 отказ
31 671 648 256 отказ

Но эти отказы ничего не говорят о рабочем конфиге. Отбор сравнивает с free × 0.98, поэтому raw_1010k выбирается только при сырых free ≥ 32 833 306 200. Все три ступени ниже этого — при настоящем конфиге профиль там просто не был бы предложен, и отказ вызван моим принудительным обнулением требования.

Непроверенной остаётся полоса от 32 833 306 200 до максимума карты 33 136 836 608, шириной 304 МБ. И она недостижима: балласт держит собственный контекст CUDA, который больше этой полосы. Инструмент не может измерить этот профиль, потому что съедает ровно тот запас, который проверяет.

Что отсюда следует. Объявленное требование 32 176 640 076 не показано неверным, а единственный участок, где оно могло бы быть неверным, этим инструментом не достаётся. Профиль остаётся нетронутым — и теперь не «на всякий случай», а потому что замер уперся в свой предел, и предел назван.

Косвенное подтверждение, что константа всё-таки держится и здесь: по ней требуется 33 522 315 586. Обычный vllm serve на безголовой карте не создаёт ни процесса балласта, ни родительской пробы, поэтому видит примерно на 0.5 ГБ больше моего максимума — то есть около 33.6 млрд, и профиль впритык помещается. Ровно это и записано в карточке словами «графическая сессия держит около гигабайта, и тогда миллион не влезает».


Поправка к заголовку этого раздела: константы не получилось (СНЯТО, см. выше)

Сначала я написал, что это одна заниженная константа на все три профиля, и хотел пересчитать требования вычитанием. Данные этого не поддерживают, и я снимаю формулировку.

Считая по тем же логам: у арендованной 5090 всего 31.36 ГиБ (движок сам сообщает это в тексте OOM). Безголовый запуск на ней поднял raw_1010k, у которого веса с ареной дают 28.69 ГиБ, — то есть он сработал с запасом около 1.7 ГиБ. А long_393k с запасом 1.72 ГиБ упал.

Один профиль падает там, где другой работает при том же запасе. Значит запас зависит от профиля — вероятно, от размера и характера выделения арены, — и выводить его вычитанием из одного замера нельзя.

Что из этого следует практически: чинить можно только то, что замерено. Порог long_393k замерен и его требование надо ставить по нему. Пороги long_262k и raw_1010k не замерены, и подставлять им выведенное число — это ровно та ошибка, которая уже привела к нынешнему дефекту: требование, полученное оценкой вместо замера. Их надо промерить такой же лестницей.

Попутно: карточка говорит «веса занимают 16.2 ГиБ». Движок сообщает 16.44, на диске файлы весят 15.68. Верно движковое, и это надо поправить.

Третий дефект, отдельный: long_262k не задаёт долю памяти

Гипотезу «отбор должен был выбрать long_262k» я проверил принудительно, и она не спасает: этот профиль на той же памяти падает тоже, но по другой причине.

ValueError: Free memory on device cuda:0 (23.0/31.36 GiB) on startup is less than
desired GPU memory utilization (0.92, 28.85 GiB).

Доля 0.92 — это умолчание vLLM. В конфиге у long_262k поля gpu_memory_utilization нет вообщеlong_393k и raw_1010k стоит 0.95). А clamp_utilization начинается со строки wanted = body.get(...) и при None сразу возвращает None, то есть прижимать нечего и прижатия не происходит. Профиль, которому нужно 19.79 ГиБ, падает на карте с 23.0 свободными — не от нехватки памяти, а потому что некому было опустить умолчание.

Складывая всё вместе: на 24-гигабайтной карте без флагов не работает ни один профиль. long_393k выбирается и падает с OOM на автотюнере; long_262k, если бы отбор до него дошёл, упал бы с ValueError ещё раньше. Карточка при этом объявляет запуск без флагов главным удобством и обещает 4090 окно 393 216.

Три причины, три разные починки:

  1. long_262k обязан нести явную gpu_memory_utilization, иначе прижатие для него мертво. Проверяется прогоном, он поставлен в очередь.
  2. requires_free_bytes надо ставить по замеренному порогу, а не по оценке.
  3. Прижатие доли бессмысленно, пока арена закреплена: kv_cache_memory_bytes отменяет профилирование памяти целиком. Либо арена ужимается вместе с долей, либо профиль должен признаваться неподходящим и уступать место следующему.

Чего пока не знаю

Вилка порога — 742 МБ, от 25 229 197 312 до 25 971 589 120. Свободное на отборе у безголовой 4090 (всего 25 757 220 864) попадает внутрь этой вилки, поэтому вопрос «работает ли настоящая 4090 без флагов» пока открыт. Идёт тонкая лестница на 25.9 / 26.1 / 26.3 ГБ, которая сузит вилку примерно до 200 МБ и вопрос закроет.

Параллельно проверяется гипотеза, что на такой памяти отбор обязан был выбрать long_262k: у него веса плюс арена дают 19.37 ГиБ против 21.04 у long_393k, то есть на активации остаётся 3.63 ГиБ вместо 1.96.

Что править, когда порог будет известен

  1. Если порог выше памяти безголовой 4090 — long_393k непригоден на 24 ГБ, и нужен профиль поуже (окно 262 144 при меньшей арене), а строка «RTX 4090 → 393 216» в карточке неверна и должна стать либо меньшим окном, либо честным «требует явных флагов».
  2. Независимо от порога: requires_free_bytes профиля посчитан по прогону, где арена задавалась иначе, и его надо пересчитать из замеренного порога, а не из оценки накладных расходов.
  3. Рассмотреть уменьшение kv_cache_memory_bytes у long_393k: 5.2 ГБ дают 406 726 токенов ёмкости при окне 393 216, то есть запас 3.4%. Он не бесплатен, и на 24 ГБ его, похоже, не на что тратить.

Урок, снова тот же

Слепая зона в замерах — там, где путь пользователя отличается от пути пробы. Каждая проба передавала параметры явно, поэтому блок профилей ни разу не исполнялся под замером. Первый же запуск по-пользовательски нашёл четыре ошибки в прошлый раз и пятую сейчас.


Четвёртый дефект, найденный последним: бюджет насыщается и перестаёт различать

Обнаружен, когда raw_1010k был выбран при 33 568 063 488 свободных байт и упал, а при 33 167 507 456 — правильно уступил long_393k. То есть отбор отвергал профиль там, где памяти меньше, и предлагал там, где её больше, но всё равно недостаточно.

Причина в memory_budget:

usable_free = float(free_bytes) * _UTILIZATION_SAFETY      # 0.98
return int(min(float(total_bytes) * float(utilization), usable_free))

Доля профиля raw_1010k была 0.95, то есть меньше защитного множителя 0.98. Значит при большой свободной памяти связывает первое слагаемое, бюджет упирается в потолок total × 0.95 и перестаёт зависеть от свободной памяти вообще:

свободно бюджет чем связано
33 568 063 488 (падает) 32 481 371 750 total × 0.95
34 188 820 480 (работает) 32 481 371 750 total × 0.95

Точка отказа и точка работы дают одно и то же число. Никакое requires_free_bytes их не различит: сравнение идёт с величиной, которая насытилась.

Починка, и она в конфиге, а не в коде

Сперва я записал это как «дефект логики, править не буду в третьем часу». Это было неверно: достаточно поднять долю профиля выше защитного множителя, и связывать начнёт free × 0.98, а бюджет снова станет следить за свободной памятью.

Поставлено: gpu_memory_utilization 0.99, requires_free_bytes 33 200 000 000. Тогда бюджет при отказе 32 896 702 218 — отвергается, при работе 33 505 044 070 — принимается. В сам vLLM уйдёт 0.980: прижатие считает посильную величину отдельно и опустит 0.99 до неё.

Урок

Защитный множитель 0.98 и доля профиля 0.95 выглядели независимыми числами, а на деле их порядок решает, работает ли отбор. Пока доля ниже множителя, проверка «влезет ли» слепа ко всему, что выше потолка. Это не опечатка и не просчёт в арифметике — это связь между двумя константами, которую никто не записал.


Открепление арены: проверено и отвергнуто

Гипотеза была хорошая. Движок сам пишет в лог, что закреплённая kv_cache_memory_bytes отменяет профилирование памяти и «does not respect the gpu_memory_utilization config». Значит и отбор, и сам vLLM слепы к одной величине — тем 570 МБ, что появляются после старта процесса движка. Открепить арену — и зрение вернулось бы хотя бы движку.

Причина, по которой её закрепили, к тому же отпала: без закрепления профилирующий проход падал в humming_gemm, но skip_mm_profiling появился позже и снимает это. Проверено — падение не вернулось.

На краю открепление дало заметно лучший отказ:

Available KV cache memory: 11.76 GiB
ValueError: To serve at least one request with the model's max seq len (1010001),
12.02 GiB KV cache is needed, which is larger than the available (11.76 GiB)

Обе цифры, причина, никакой трассировки — вместо torch.OutOfMemoryError: Tried to allocate 272.00 MiB посреди инициализации.

Но на полной карте оно ломает всё:

Available KV cache memory: 11.85 GiB   -> окну нужно 12.02 -> отказ

Собственная оценка vLLM слишком осторожна: она даёт 11.85 ГиБ там, где закреплённая арена берёт 12.15. Без закрепления миллион не поднимается нигде, включая пустую карту.

Вывод: закрепление остаётся. Это не рудимент, а именно то, что делает окно миллиона возможным — оно перекрывает заниженную оценку движка. Цена известна и записана: на краю отказ выглядит как OOM, а не как объяснение.

Показательная деталь напоследок: профилировщик vLLM даёт 11.85 ГиБ на полной карте и 11.76 на краю — 90 МБ разницы, ровно столько же, сколько видит отбор. Оба слепы к одному и тому же. Это независимо подтверждает, что край предельный по существу, а не создан пробой.