ПРЕДУПРЕЖДЕНИЕ: Автору 14 лет, он плохо разбирается в технологиях и программировании, но основную теорию проекта придумал сам. Очень хотелось бы услышать конструктивную критику
Всем здравствуйте, я Titled и я создал форк llama.cpp, где реализовал все свои заветные мечты. Сейчас расскажу, что это было и почему я написал такой длинный заголовок.
Сначала стоило бы пройтись по терминам вообще, потому что без них читать эту статью будет не так интересно. Основная речь пойдет о движке запуска нейросети, в который вшиты все архитектуры, и вы можете сами его скачать и запустить любую нейросеть. Его название — llama.cpp, и как принято, все делают форки успешных проектов. Я не стал исключением и создал свой форк, но с нестандартной идеей.
Что помогает нейросети так думать?
Я решил использовать рекуррентность — очень интересный и гипернедооценённый инструмент, который я пытаюсь продвинуть уже очень много времени. Если кратко объяснять, как это работает, то легче показать это наглядно.

Рисовал от руки, так что простите, но здесь наглядно видно, что мы получаем вход, проходим через слои и получаем выход. Чтобы добавить больше ума, нам нужно добавить больше слоев, но тогда будет больше вычислений. Из-за ограниченных возможностей моей видеокарты я физически не смог поставить больше слоев дообучением. Тогда появилась идея: я буду зацикливать слой! Ладно, идея не моя — я украл её из OpenMythos но, в отличие от них я применил это на реальных моделях. И это не всё: я модифицировал систему!

Так происходит на OpenMythos. Это странно, ведь они зацикливают один слой, который может технически отвечать за что-то неважное. Поэтому я подумал и решил делать сразу 3 слоя, которые я буду зацикливать. Хардкод здесь не поможет, и я придумал (попросил дипсик и обговорил с ним) формулу адаптивной выборки слоёв!
Для умных людей я просто оставлю эту формулу здесь, а для нормальных поясню короче:
Она делит все на 4 блока, и один блок выкидывает, зацикливает средние слои. Сумма циклов всегда равна параметру D, а чтобы выбрать слой — параметр эффективности S от 1 до 100.
формула
1. Разбиение на 4 блока
N — всего слоёв.
k = ⌊N/4⌋,r = N mod 4.Размеры блоков:
[k, k, k, k+r].Стартовые индексы:
0, k, 2k, 3k.
2. Выбор слоя в каждом блоке (параметр S ∈ [0,100])
Для блока
i:offset_i = ⌊ S/100 · (size_i − 1) ⌋L_i = start_i + offset_iL₁— отбрасывается (используется только для начального «понимания»).
3. Рекуррентные слои и циклы
Активные слои:
L₂, L₃, L₄.Общая глубина
D(число циклов).
Распределение по весам(1: 3: 2):
text
c₂ = round(D · 1/6) c₃ = round(D · 3/6) c₄ = D − c₂ − c₃
Итог: слой L₂ повторяется c₂ раз, L₃ — c₃, L₄ — c₄.
При D=12 получаем (2, 6, 4), при D=16 → (3, 8, 5) и т.д.
Вот разобрались с формулой, но что нам делать, если мы просто так зациклим слои? Два варианта: или получится бред, или прироста не будет, или мы взорвёмся (градиенты, имеется в виду). В общем, мы должны юзать вот такую имбу: -
Эйлерово масштабирование
Вдохновлено численным решением ОДУ:
h_{t+1} = h_t + (1/iters) · f(h_t)Мы применяем это как адаптивный шаг внутри цикла:
cur = alpha cur + (1 - alpha) inpSA, гдеalpha = 1.0 / iters.Это не даёт сигналу взорваться и сохраняет баланс residual-потоков, даже при большом
D.
Идёт коррекция: с каждой последующей итераций цикла получаются меньшие величины.
KV КЕШ В ДВЕНАДЦАТЬ РАЗ БОЛЬШЕ ЧТОООО?!?!?!?!?
Звучит пугающе, но для этого мы взяли крутую штуку — это KV-декапплинг (отвязка кеша от рекуррентности). То есть кеш не связан с рекуррентностью. Я могу вам объяснить это сам. Конечно же, а давайте я это и сделаю!
Если кратко: запись кеша доступна только на первой итерации, на всех остальных — только чтение. Но зачем это, кроме веса? Всё из-за того, что на каждой итерации перезапись ключей/значений (KV) ломала внимание. Это было неприятно, но легко чинится. Но это не конец: идём далее.
Как нам не взорвать градиенты
Надеюсь, объяснять, что такое градиенты, мне не нужно, поэтому просто скажу: они взрываются без инъекций, поэтому мы даём ей лекарство:
Добавляем линейную комбинацию:
h_new = alpha h_cur + beta h_inp, гдеbeta = 1 - alpha.Это якорь, который возвращает состояние к исходному вектору, предотвращая семантический дрейф.
Вот таким образом мы, получается, убеждаемся, что мы всегда меньше 1, а значит, взрыва и брейнрота у нас не будет.
Пруфы
Смотря эту статью, вы зададитесь двумя вопросами: «Почему так кратко?» и «Где пруфы?» На второй я отвечу прямо сейчас:
А чтобы подтвердить всё, я сделал базовый бенчмарк gms8k и получил на маленькой модели впечатляющие результаты, поэтому вот и они:
GSM8K Benchmark Results (N=500)
Evaluating DeepSeek-R1-Distill-Qwen-1.5B-Q4_K_M:
Configuration | GSM8K Accuracy | Correct Answers |
|---|---|---|
Baseline ( D = 0 ) | 39.20% | 196 / 500 |
Recurrent ( D = 12 ) | 77.20% | 386 / 500 |
Using Euler step scaling and KV cache decoupling to prevent semantic drift across iterations.
Да это вырезка из README!!
Да, это явно пруфы. Я думаю насчёт второго вопроса: отвечу — я сделаю вторую часть, где объясню больше крутых моментов именно по разработке и переписи кода на ручной C++, а не ИИ-шный. В общем, так я надеюсь, что кто-то найдёт время зайти, посмотреть репозиторий и дать советы по улучшению, и да, по моим тестам данный форк увеличивает скорость где-то на ~10%, так что юзайте на здоровье!
Спасибо за прочтение!
Важное уточнение: в этой работе я не делал дообучение (fine‑tuning) модели. Все изменения касаются только этапа инференса: я модифицировал вычислительный граф, добавив рекуррентное применение слоёв с адаптивным масштабированием и отвязкой KV-кеша. Веса модели остаются неизменными
