В апреле OpenAi проводила конкурс Parameter Golf на самую эффективную нейросеть. Были выставлены ограничения — 16 мегабайт на веса и обвязку, 10 минут обучения на восьми H100. Единственный критерий — самый низкий bpb. Бэйслайном конкурса был код от OpenAi, собранный в классической архитектуре классическими инструментами, его bpb составил 1,2244, победитель добился 1.0565

Идея конкурса мне очень понравилась по весьма прозаическим причинам — размер весов позволял провести обучение на моей старенькой Geforce 1660 с шестью гигабайтами памяти на борту, а чёткая цель позволяла легко ориентироваться среди конкурентов.

Дальше обзор победителей и мой результат.

Кто с чем пришёл на конкурс

Целевой показатель bpb (bits per byte) хорош тем, что он абсолютно объективен. Микросети принципиально неспособны к удержанию семантики в том виде, в котором вы привыкли при общении с LLM, но этот показатель демонстрирует, как модель научилась сжимать информацию через выучивание базовых грамматических инвариантов. 

Сразу поясню, что в перечень победителей попадали только те, кто улучшил последний лучший результат bpb на 0,005. 

Чтобы втиснуться в 16 МБ и выжать bpb 1.05, подавляющее число участников пошли путем жёсткой оптимизации: 2-битное квантование (разрушающее логику), хардкодные N-граммы из 90-х, костыли для регистров (CaseOps) и обучение на лету (TTT). Задача — минимизация bpb на старых архитектурах.

Моя цель была другой: я хотел добиться, того же самого, но через чистую нейросетевую топологию, без скриптовых костылей и статистических махинаций. По сути, только за счет правильной внутренней динамики. В принципе, мой результат bpb 1.075 оказался очень близок к рекордному.

Run (Ключевые инструменты)

BPB Скор

Автор

Детали реализации архитектуры и методов

1

Calib32 Token-Only N-gram + AsymLogit Stack

1.0565

codemath3000

На базе PR 2135. Перезапуск архитектуры из PR 2130 на чистых каноничных данных CaseOps с батчем калибровки GPTQ_CALIBRATION_BATCHES=32. Усредненный результат по 3 сидам: 1.05651 в рамках льготного периода. Улучшение p=0.014 по сравнению с PR 2014.

2

Progressive Context Growth + Short-Doc Score-First TTT

1.0576

simonbissonnette

На базе PR 2014. Использован стек CaseOps (PR 1855 / 1953) с прогрессивным увеличением контекста до 3k токенов. Включает адаптацию во время инференса (TTT) с предварительным скорингом для коротких документов. В основе ветка AWQ-lite / AsymLogit. Среднее по 3 сидам: 1.05759.

3

Long-Context No-Q/V TTT + QK-Gain 5.25

1.0586

andrewbaggio1

На базе PR 1953 (база V21 из 1945). Использован длинный контекст (2560) для оценки и TTT. Применена маска TTT без обновления Q/V весов (no-Q/V), learning rate для TTT = 0.75, инициализация QK_GAIN=5.25. Среднее по 3 сидам: 1.05855.

4

AWQ-Lite GPTQ + AsymLogit on PR1855 Stack

1.0594

alertcat

На базе PR 1945. Классический стек PR 1855, дополненный смешанным квантованием AWQ-lite GPTQ (PR 1908) и асимметричными логитами AsymLogit. Среднее по 3 сидам для базы V21 v2: 1.05943 после строгого перезапуска с seed=42.

5

BOS-Fixed SmearGate + LQER + SparseAttnGate + 9-Hparam Stack

1.0611

codemath3000

На базе PR 1855. Стек с исправленными границами BOS (начало последовательности), добавлены LQER и база SparseAttnGate / PolarNS / FusedCE. Применено погрупповое сжатие lrzip и жадное переопределение 9 гиперпараметров.

6

BOS-Fixed SmearGate + LQER Asymmetric + PR1787 SparseAttn + Phased TTT

1.0614

aquariouseworkman

На базе PR 1851. Применен фикс границ BOS к стеку SmearGate + LQER. Используется база SparseAttnGate / PolarNS / FusedCE, токенизация CaseOps и фазированный TTT с предварительным скорингом (phased score-first TTT).

7

PR1736 + PolarNS + MIN_LR + SparseAttnGate + FusedCE + Warm-A TTT

1.0634

nprime06

На базе PR 1787. Стек CaseOps, к которому добавлены: коэффициенты Newton-Schulz (PolarNS), MIN_LR=0.1, SparseAttnGate, фьюзд-кросс-энтропия с софткаппингом (fused softcapped CE) и TTT с теплым стартом матрицы A (warm-start-A).

8

CaseOps + MLPClip12 + SmearGate/LoRA-TTT

1.0645

dexhunter

На базе PR 1769. Стек CaseOps с доработками SmearGate и адаптацией LoRA-TTT, добавлен клиппинг перцептрона MLPClip12. Среднее по 5 сидам сдвигает границу эффективности (frontier) CaseOps.

9

SP8192 + CaseOps + GatedAttn + QuantGate + Loop45 + Phased TTT

1.0655

dexhunter

На базе PR 1736. Внедрено биективное преобразование CaseOps без потерь с учетом BPB через байтовые сайдкар-файлы (byte-sidecar). Добавлены гейтированное внимание (GatedAttn) и масштабирование квант-гейтов поверх стека фазированного TTT.

10

CaseOps Tokenizer + Tapered WD + Phased TTT

1.0678

romeerp

На базе PR 1729. Биективное преобразование регистра (CaseOps) с валидационными байтовыми сайдкарами, плюс мягкое позднее затухание weight-decay (taper) для оптимизатора Muon на легальном стеке фазированного TTT.

11

SmearGate + Attention Output Gate + Legal TTT

1.0714

MarioPaerle

На базе PR 1667. Использованы: SmearGate, гейт выхода механизма внимания (attention output gate), рекуррентность по глубине (depth recurrence), параллельные остаточные связи, QK-Gain=5.25, квантование и score-first TTT.

12

VarLen Attention + Fused MLP + Multi-Phase Global SGD TTT

1.0719

dexhunter

На базе PR 1626. Использованы: механизм внимания с переменной длиной (VarLen Attention), fused MLP, многофазный глобальный SGD для TTT, урезанный (trimmed) GPTQ, MLR 0.026, int7 эмбеддинги и адаптивный клиппинг.

13

VarLenAttn + PhasingTTT

1.0728

romeerp

На базе PR 1610. Стек в стиле PR 1530 (внимание переменной длины + fused MLP), к которому добавлен фазированный TTT поверх уже отскоренных валидационных чанков контекста.

(перевод топа с сайта конкурса)

Когда я увидел первые рекорды с bpb на уровне ещё 1,21 — 1,22, то немного испугался. Люди работали с нейросетями объемом более 100 миллионов параметров (которые моя видеокарта даже бы не смогла пошевелить), применяли жесточайшее квантование, и чем дальше, тем хуже. Использовались инструменты о которых я, честно говоря, и не слышал.

Вот саммари основных направлений оптимизации, желающие могут изучить дополнительно:

  1. Test-Time Training (TTT) и его мутации: Настройка весов прямо в процессе инференса на конкретном промпте. Участники используют Score-First TTT (сначала оцениваем, потом учим), Phased TTT (обучение по фазам контекста), LoRA-TTT и экзотические фиксы вроде Warm-A (теплый старт матриц) или заморозку Q/V матриц во время TTT (No-Q/V).

  2. Агрессивное квантование и компрессия: AWQ-lite, GPTQ (с калибровочными батчами), int7 для эмбеддингов и погрупповое сжатие lrzip.

  3. Механизм внимания: Отказ от классического Attention в пользу SparseAttnGate, внимания с переменной длиной (VarLen Attention), гейтирования выходов внимания (Attention Output Gate) и подгонки QK-Gain для стабилизации softmax-распределения.

  4. Работа с регистрами и логитами: Внедрение CaseOps (биективные преобразования токенизатора для умной обработки регистра без потери байтовой информации) и AsymLogit (асимметричные логиты).

  5. Нормализация и регуляризация: PolarNS (применение коэффициентов Ньютона-Шульца вместо стандартного LayerNorm), фьюзд кросс-энтропия с мягким ограничением выбросов (softcapped CE), управление затуханием весов (tapered WD для Muon) и клиппинг градиентов внутри MLP (MLPClip12).

Надо отдать должное OpenAi, понимая, что весь топ будет забит хардкорными ml-инженерами, живущими оптимизацией, был добавлен стек альтернативных архитектур. OpenAi предложила направлять код, который может быть хуже текущего лучшего результата, или не проходящего по размерам нейросети, но с оригинальной архитектурой. Я больше ориентировался именно на этот вариант.

Run (Оригинальные архитектуры)

BPB Скор

Автор

Детали реализации архитектуры и методов

1

1 Bit Quantization

1.1239

CiprianFlorin-Ifrim

106 миллионов параметров, квантованных до 1 бита, плюс различные архитектурные изменения и 2 часа обучения.

2

MDLM Text Diffusion

1.1465

agalimova

На базе PR 1106. Маскированная диффузионная языковая модель (MDLM) с оценкой в стиле ELBO через поглощающую маску. Провалидирована на двух ускорителях H100 как дискретная диффузионная модель.

3

Hymba-8L + Sliding Attention at 32K

1.1467

mkenney2

На базе PR 1245. Гибридная архитектура Mamba SSM со скользящим окном внимания и контекстом на 32 тысячи токенов. Усреднение по 3 сидам, размер менее 16 МБ, адаптация score-first TTT.

4

Mamba-3 Hybrid SSM + SP8192 + Legal TTT

1.1473

mradassaad

На базе PR 1644. 7-слойная гибридная модель Mamba-3: 5 блоков пространства состояний (SSM) и 2 слоя внимания. Использованы SP8192, AR GPTQ, чанковый score-first TTT и оценка с перекрытием состояний (stateful-overlap eval).

5

Differential-Gated Attention

1.1898

ddavidgao

На базе PR 542. Альтернативный механизм внимания, передающий дифференциальную полезную нагрузку (акцент на новизну) в глубоких слоях. Включает анализ избыточности, зависящей от глубины сети.

6

Learned Adapters on Random Linear Maps

1.1971

pranavxiyer

На базе PR 2058. Перцептроны-адаптеры со случайными сидами и обучаемыми адаптерами в стиле LoRA ранга 160. 12 слоев, множитель MLP 3x, смешанная компрессия int6/int8. Подтверждено на 3 сидах с 10-минутными логами.

7

JEPA + Mamba2 LeWorldModel

1.2064

CiprianFlorin-Ifrim

На базе PR 903. Амбициозная заявка, объединяющая архитектуру SSM и JEPA для предсказания скрытых состояний (latent-prediction). Представлена с доказательствами долгих вычислений и 10-минутными логами.

(перевод топа с сайта конкурса)

Здесь лучший результат 1,12, что существенно хуже инженерного топа. Впрочем, по собственному опыту скажу, здесь вина больше не архитектур, а разницы в подходах. Классические методы оптимизации плохо или вообще не работают на оригинальных нейросетях, а средства оптимизации уникальных архитектур недостаточно проработаны ни математически, ни экспериментально.

Саммари подходов:

  1. Альтернативы Трансформеру (SSM и Гибриды): Сразу несколько участников экспериментируют с Mamba (State Space Models). Применяются гибридные сборки (например, 5 слоев SSM + 2 слоя Attention), позволяющие получить линейную сложность при длинном контексте (до 32К токенов), сохраняя способность к локальному фокусу через скользящее окно внимания.

  2. Текстовая Диффузия: Попытка перенести успех диффузионных моделей из генерации картинок в генерацию текста. Используется дискретная маскированная диффузия (MDLM) вместо классической авторегрессии.

  3. Предиктивные Мировые Модели (World Models): Эксперименты с архитектурой JEPA от Яна Лекуна, соединенной с Mamba2. Обучение идет не на уровне предсказания токенов (пикселей), а на предсказании абстрактных скрытых представлений.

  4. Экстремальное сжатие: Радикальная квантизация до 1 бита (бинаризация весов). Несмотря на серьезную потерю в скоре (1.12), модель всё еще работает, что доказывает высокую избыточность параметров в LLM.

  5. Локальные инъекции рандома и LoRA: Использование случайных линейных отображений в качестве базиса, на котором обучаются низкоранговые адаптеры (LoRA) для экономии памяти, плюс инъекция дифференциальных гейтов во внимание, чтобы слои передавали только новую, а не избыточную информацию.

Что делал я

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

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

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

В итоге получился код с bpb около 1.075, accuracy (10) — 88.8%. Словарь 1024 токена, датасет Fineweb. Сеть около 17 миллионов параметров. Размер весов до квантования 66 мб, квантованием я в связи с окончанием конкурса заморачиваться не стал. Для меня уже был важен только результат.

Результат, даже с учетом потенциальных потерь после оптимизации, выглядит удивительно в инженерном топе, и лучшим в топе альтернативных архитектур. 

Итак, как выглядит архитектура:

По сути основная идея это гибридизация достоинств рекуррентной нейросети и трансформера. Я разделил нейросеть на три основных элемента. 

  1. Сенсорный блок (Sensor) — двухслойный трансформер, отвечает за базовую токенизацию и первичная семантическая обработка

  2. Рекуррентный модулятор (Cortex) — рекуррентная нейросеть вычисляет и удерживает долгосрочные инварианты

  3. Моторный блок (Motor) — трехслойный трансформер, собирает выходы первых двух элементов и формирует финальные логиты.

Подробнее под спойлером:

Скрытый текст

1. Контур сенсорной обработки (SENSOR)

Осуществляет первичное нелинейное преобразование дискретной входной последовательности токенов размерности [Batch, SEQ_LEN].

  • Элемент Embed: Слой статической проекции токенов в латентное пространство размерности D_MODEL = 512, совмещенный с аддитивным наложением обучаемых позиционных эмбеддингов.

  • Элемент Transformer Front (TF): 2-слойный блок Transformer Encoder. Применяет механизмы многоголового самовнимания (Multi-Head Self-Attention) и полносвязные сети (FeedForward) с каузальной маской, ограничивающей доступ к токенам из будущего. На выходе формирует промежуточный тензор признаков mid_l2_seq размерности [Batch, 64, 512].

  • Элемент FrontHead: Изолированная линейная голова, вычисляющая локальную стохастическую неопределенность на выходе сенсорного блока без пропускания градиента назад в рамках контекста torch.no_grad. Из распределения извлекаются нормализованная энтропия Шеннона по формуле:

H_norm = - sum(P_i * ln(P_i)) / ln(VOCAB_SIZE)

и максимальная вероятность токена Top1. Данные скаляры усредняются по окнам CHUNK_SIZE = 4 и конкатенируются в управляющий вектор mode_signal размерности [Batch, 16, 2].

2. Рекуррентный модулятор (CORTEX)

Контур вычисления и удержания долгосрочных контекстных инвариантов. Работает на пониженной частоте дискретизации по времени.

  • Элемент CortexIn: Входной линейный мост. Принимает тензор mid_l2_seq, сжатый по временной оси путем усреднения токенов внутри каждого чанка до размерности [Batch, 16, 512]. Оператор detach полностью изолирует веса сенсорного блока от градиентов рекуррентной сети. Слой проецирует признаки в вектор core_input размерности [Batch, 16, 128].

  • Элемент FastRNN: Рекуррентный блок GRU с размерностью скрытого состояния CORE_DIM = 128. Принимает конкатенированный вектор fast_rnn_input размерности [Batch, 16, 130], состоящий из core_input и mode_signal. Обновляет состояние на каждом шаге чанка (каждые 4 токена).

  • Элемент SlowRNN: Рекуррентный блок GRU верхнего уровня со скрытым состоянием 128. Принимает выходы FastRNN с шагом децимации SLOW_UPDATE_INTERVAL = 4. Фиксирует макроструктурные семантические изменения на интервале в 16 токенов.

3. Спектральная модуляция и узел суммирования (Σ)

Механизм прямого воздействия рекуррентных инвариантов на остаточный поток (residual stream) Трансформера.

  • Каузальный сдвиг (Causality Shift): Для предотвращения утечки данных (data leakage) выходы рекуррентных слоев смещаются на один шаг назад. На шаге t применяются состояния, вычисленные на шаге t-1. Тензоры интерполируются методом repeat_interleave до исходной длины последовательности (64 токена) и проецируются через линейные слои bridge_fast и bridge_slow в размерность 512.

  • Узел синтеза (Σ): Выполняет аддитивное смещение промежуточного представления mid_l2_seq. Амплитуда рекуррентного воздействия регулируется весовыми скалярными параметрами gate_fast и gate_slow, которые изначально инициализированы нулями:

modulated_mid_seq = mid_l2_seq + mod_fast gate_fast + mod_slow gate_slow

4. Моторный блок (MOTOR) и выходные интерфейсы

Финезационный контур авторегрессионного декодирования и вычисления целевых функций потерь.

  • Элемент Transformer Back (TB): 3-слойный блок Transformer Encoder. Обрабатывает модулированный поток modulated_mid_seq, формируя финальное контекстное представление с учетом рекуррентных поправок.

  • Элемент FinalHead: Линейный слой проекции выходов TB в пространство словаря [Batch, 64, 1024]. Вычисляет итоговые логиты, на основе которых рассчитывается базовая кросс-энтропия языкового моделирования L_CE.

  • Элемент Монитор хаоса (PredH): Линейный слой motor_monitor, подключенный к объединенным выходам FastRNN и SlowRNN. Генерирует локальный прогноз энтропии моторного блока preds_H_tensor для каждого чанка.

  • Узел оптимизации L_Pred: Вычисляет среднеквадратичную ошибку (MSE) между прогнозом preds_H_tensor и фактической энтропией распределения FinalHead, усредненной по чанкам:

L_Pred = MSE(preds_H_tensor, real_H_chunks)

Этот сигнал ошибки оптимизирует исключительно веса рекуррентного контура и спектральных врат, настраивая их на минимизацию неопределенности в моторном блоке. Общий шаг градиентного спуска выполняется по композитному лоссу:

L_Total = (L_CE + w_fut L_Fut + w_pred L_Pred) / GRAD_ACCUM_STEPS

Если вы обратите внимание на названия элементов, то наверняка увидите аналогию с человеческим мозгом. Как ни странно, это не было задумано специально, а произошло естественно после массы проб и ошибок.

  1. Разделение на SENSOR и MOTOR воспроизводит архитектуру афферентных (воспринимающих) и эфферентных (исполнительных) путей нервной системы. Первичный анализ признаков изолирован от финального формирования моторных команд (токенов).

  2. Блок CORTEX выполняет функцию ассоциативной коры. Двухуровневая структура GRU (Fast и Slow контуры) реализует иерархическое удержание контекста на разных временных шкалах, что необходимо для планирования долгосрочных семантических зависимостей.

  3. Элемент PredH (слой motor_monitor) технически является аналогом эфферентной копии (efference copy) в нейробиологии. Модель передает копию исполнительного плана обратно в прогнозирующий контур. Это позволяет оценить рассогласование между ожидаемым и реальным хаосом (неопределенностью) моторного блока и минимизировать его через ошибку L_Pred.

Более того, в процессе обучения нейросеть выступила несколько неожиданно, кортекс не обогащал представления сети, а занимался тем, что подавлял шум и неверные векторы. Что весьма подозрительно напомнило мне концепцию Свобода-вето Бенджамина Либета — сознание не создает желания, а лишь отвергает неприемлемые.

А это ссылки на код, логи, веса. Обратите внимание, инференс нейросети требует передачи state рекуррентной сети, вот пример кода для расчета bpb. 

В перспективе, я полагаю, необходимы эксперименты с разморозкой весов, что возможно улучшит инференс и поставит вопрос о возможности реализации continual learning.

Философская база

Этот раздел полезен, но может быть не интересен. Читатели моих предыдущих статей знают, что я придерживаюсь достаточно оригинальных взглядов на сознание, нейросети и философию в целом. Конкретно этот код не опирался на математику, а вполне органично вырос из моих концептуальных гипотез.

В первую очередь, это концепция Апофатического ИИ, концепция утверждает, что нейросеть обучается не на запоминании позитивных примеров, а на отбрасывании ошибочных через проведение границы (формирование инварианта). Подробнее можно прочитать в статье Апофатический ИИ.  Наиболее неожиданный результат экспериментов состоит в следующем: оба gate (G_fast и G_slow) в процессе обучения ушли в устойчивый минус и продолжали углубляться. Кортекс не добавлял смысл к представлениям автомата, а подавлял лишнее.

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

Во-вторых, это концепция двухслойного сознания, которую я формулирую так: интеллектуальная система состоит из двух принципиально разных уровней. Первый — байесовский автомат, предиктивная машина, которая непрерывно моделирует внешний мир и реагирует на него. Второй — глубинный слой, который не имеет прямого доступа к внешнему миру вообще. Он видит только первый слой — и именно это меняет его качество..

Глубинный слой не содержит модель мира. Он содержит модель того, как первый слой моделирует мир. Функции глубинного слоя: разрыв зацикливания, выбор менее вероятного, остановка реакции.

В коде это реализовано буквально. Трансформер — это автомат первого уровня. GRU-кортекс — это наблюдатель второго уровня, который не читает входные токены напрямую. Он получает только усреднённые представления из скрытого пространства, и на основании этого строит модель того, как автомат справляется с текстом.

Ключевое следствие: кортекс вынужден предсказывать выход на следующем шаге. Чтобы делать это хорошо, он строит внутреннюю модель поведения автомата. Это и есть операционализация второго уровня сознания через задачу предиктивного кодирования.

Ну и много всего остального, но достаточно и этого.

Заключение

Естественно, в коде и интерпретациях могут быть ошибки или пространство для улучшения, если кто-то заинтересуется и их найдёт, обязательно сообщите мне, буду благодарен. 

Дальнейшее развитие предполагается, но масштабирование упирается в железо. Если кто-то может дать доступ к приличным GPU или предоставить грант (а вдруг), был бы благодарен ещё больше.

Перспективы: 

  • более устойчивое обучение на малых данных за счёт усиления роли hard negatives и структурных границ;

  • снижение деградации при обучении на синтетических датасетах, поскольку модель учится не только корреляциям, но и топологии запретов;

  • появление более экономичных моделей, где часть интеллекта формируется не ростом параметров, а организацией внутренней динамики;

  • развитие архитектур с внутренним мониторингом неопределённости (энтропии), где генерация управляется не только вероятностью токена, но и состоянием собственной когнитивной геометрии;

  • потенциальное уменьшение галлюцинаций через усиление апофатических границ («что недопустимо»).

Если гипотеза верна хотя бы частично, то дальнейшая эволюция LLM может идти не только через масштабирование Transformer’ов, но и через появление архитектур, где ключевым вычислением станет динамическая система внутренних отрицаний — своего рода «свобода вето» внутри самой модели. Более фантастические перспективы я приводить не стал за их фантастичностью.

Комментарии (5)


  1. bambazamba
    26.05.2026 18:27

    Потестил опубликованный чекпоинт.

    90% output'а кортекса константа, не зависящая от входных токенов. Мотор подстроился под постоянный сдвиг от кортекса, а не под его "смысл".

    Проблема многих публикуемых комбинированных архитектур (трансформеры + hi_dim vector bottleneck "для смысла") - модель находит обходной путь. Tрансформеры делают всю работу, а bottleneck коллапсирует в константу или около того, от которой мотор формально зависит, но информации оттуда толком не получает. Но лучше перепроверьте сами, я тестил поверхностно.


    1. Kamil_GR Автор
      26.05.2026 18:27

      Ага, скиньте, плз, описание как тестировали, пойдет и LLM описание, сюда, в личку или ТГ, как будет удобнее

      Просто при коллапсе, градиент по гейтам и сами гейты ушли бы к нулю... А если посмотреть логи, они вполне себе живы.

      Ну и 10% вполне себе влияет на мотор.


    1. Kamil_GR Автор
      26.05.2026 18:27

      Протестировал код, который вы направили мне в личку. Спасибо большое!

      И пришел к забавным выводам, 87% константа - это маска, которую кортекс создал для гашения структурного шума трансформера. Сенсорный блок использует абсолютные позиционные эмбеддинги, на каждой позиции в окне от 0 до 63 генерируется специфический структурный шум (краевые эффекты, ритм абсолютных позиций, и т.д.). Вот их и структурные шумы самого датасета эта константа и давит через отрицательные гейты.

      Я немного расширил ваш код, вы проверяли на шуме (случайных токенах), я добавил тестирование на тексте ( с передачей стэйта):

      Веса загружены: checkpoints_vortex_hybrid/vortex_26_4_final_step_31000.pt gate_fast=-1.8633, gate_slow=-2.5850

      ================================================== РЕЖИМ ТЕСТИРОВАНИЯ: NOISE

      [A] Baseline (Полная модель) CE = 8.8635, Acc@10 = 8.65%

      [B] Ablation (Врата = 0, Кортекс отключен) CE = 15.7139, Acc@10 = 0.97% (Delta: +6.8504)

      [MODULATION STATS] mod_total = 85.6461 mod_mean (const) = 81.0749 energy: constant/total = 0.9083 (90.8%) position variance: mean=1.3537

      ================================================== РЕЖИМ ТЕСТИРОВАНИЯ: REAL

      [A] Baseline (Полная модель) CE = 1.6128, Acc@10 = 90.27%

      [B] Ablation (Врата = 0, Кортекс отключен) CE = 7.1746, Acc@10 = 35.22% (Delta: +5.5618)

      [MODULATION STATS] mod_total = 90.9642 mod_mean (const) = 83.9554 energy: constant/total = 0.8727 (87.3%) position variance: mean=2.1394

      То есть большая часть усилий кортекса уходило на очистку сигнала для трансформера. И только 10-13% на непосредственно модуляцию. Обратите внимание, как падает точность при работе с текстом в момент отключения кортекса. На шуме сравнение конечно некорректно. Забавна разница в константе при тесте на шуме и тексте - похоже для текста кортекс отдает больше переменных сигналов, что снижает долю константы.

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

      Код который я использовал:

      Скрытый текст
      """
      Ablation test v2: TriuneVortex 26.4 (Noise vs Real Text)
      """
      import os, sys, glob, math
      from pathlib import Path
      import numpy as np
      import torch
      import torch.nn as nn
      import torch.nn.functional as F
      
      torch.manual_seed(42)
      device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
      
      # --- КОНФИГУРАЦИЯ ---
      VOCAB_SIZE = 1024
      D_MODEL = 512
      SEQ_LEN = 64
      CHUNK_SIZE = 4
      CORE_DIM = 128
      SLOW_UPDATE_INTERVAL = 4
      NUM_CHUNKS = SEQ_LEN // CHUNK_SIZE
      
      CKPT_PATH = "checkpoints_vortex_hybrid/vortex_26_4_final_step_31000.pt"
      DATA_PATH = r"C:\Users\admin\Desktop\PGHM\PHOA78\data\datasets\fineweb10B_sp1024"
      TRAIN_FILES_MASK = os.path.join(DATA_PATH, "fineweb_train_*.bin")
      
      # --- ЗАГРУЗЧИК РЕАЛЬНЫХ ДАННЫХ ---
      def load_data_shard(file: Path) -> torch.Tensor:
          h = np.fromfile(file, dtype="<i4", count=256)
          t = np.fromfile(file, dtype="<u2", count=int(h[2]), offset=256*4)
          t = np.clip(t, 0, VOCAB_SIZE - 1)
          return torch.from_numpy(t.astype(np.int64, copy=False))
      
      class StatefulTokenLoader:
          def __init__(self, pattern: str, batch_size: int, seq_len: int):
              self.files = [Path(p) for p in sorted(glob.glob(pattern))]
              self.batch_size = batch_size
              self.seq_len = seq_len
              self.file_idx = 0
              if not self.files:
                  print("ВНИМАНИЕ: Файлы датасета не найдены. Режим REAL упадет с ошибкой.")
                  return
              self._load_next_shard()
      
          def _load_next_shard(self):
              tokens = load_data_shard(self.files[self.file_idx])
              num_tokens = len(tokens)
              tokens_per_stream = num_tokens // self.batch_size
              truncated_len = tokens_per_stream * self.batch_size
              self.data = tokens[:truncated_len].view(self.batch_size, tokens_per_stream)
              self.pos = 0
              self.max_pos = tokens_per_stream - (self.seq_len + 1)
      
          def next_batch(self):
              if self.pos >= self.max_pos:
                  self.file_idx = (self.file_idx + 1) % len(self.files)
                  self._load_next_shard()
              chunk = self.data[:, self.pos : self.pos + self.seq_len + 1]
              self.pos += self.seq_len
              x = chunk[:, :-1].to(device, dtype=torch.long)
              y = chunk[:, 1:].to(device, dtype=torch.long)
              return x, y
      
      # --- АРХИТЕКТУРА ---
      class TriuneVortex(nn.Module):
          def __init__(self):
              super().__init__()
              self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
              self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
      
              encoder_layer_front = nn.TransformerEncoderLayer(
                  d_model=D_MODEL, nhead=8, dim_feedforward=D_MODEL * 4,
                  batch_first=True, activation="gelu", norm_first=True,
              )
              encoder_layer_back = nn.TransformerEncoderLayer(
                  d_model=D_MODEL, nhead=8, dim_feedforward=D_MODEL * 4,
                  batch_first=True, activation="gelu", norm_first=True,
              )
              self.transformer_front = nn.TransformerEncoder(encoder_layer_front, num_layers=2)
              self.transformer_back = nn.TransformerEncoder(encoder_layer_back, num_layers=3)
      
              self.cortex_in = nn.Linear(D_MODEL, CORE_DIM)
              self.fast_rnn = nn.GRU(input_size=CORE_DIM + 2, hidden_size=CORE_DIM, batch_first=True)
              self.slow_rnn = nn.GRU(input_size=CORE_DIM, hidden_size=CORE_DIM, batch_first=True)
              self.bridge_fast = nn.Linear(CORE_DIM, D_MODEL)
              self.bridge_slow = nn.Linear(CORE_DIM, D_MODEL)
              self.gate_fast = nn.Parameter(torch.zeros(1))
              self.gate_slow = nn.Parameter(torch.zeros(1))
              self.future_proj = nn.Linear(CORE_DIM * 2, D_MODEL)
              self.motor_monitor = nn.Linear(CORE_DIM * 2, 1)
              self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)
      
          def forward(self, x, targets=None, core_state=None, gate_override=None, mod_override=None, return_internals=False):
              batch_size, seq_len = x.size()
              num_chunks = seq_len // CHUNK_SIZE
      
              positions = torch.arange(0, seq_len, dtype=torch.long, device=x.device).unsqueeze(0)
              emb_x = self.embed(x) + self.pos_embed(positions)
              causal_mask = torch.triu(torch.full((seq_len, seq_len), float("-inf"), device=x.device), diagonal=1)
      
              mid_l2_seq = self.transformer_front(emb_x, mask=causal_mask)
      
              with torch.no_grad():
                  logits_front = self.head(mid_l2_seq)
                  probs_front = F.softmax(logits_front, dim=-1)
                  log_probs_front = F.log_softmax(logits_front, dim=-1)
                  H_norm = (-(probs_front * log_probs_front).sum(-1)) / math.log(VOCAB_SIZE)
                  Top1_front = probs_front.max(-1).values
      
              mid_l2_chunks = mid_l2_seq.view(batch_size, num_chunks, CHUNK_SIZE, D_MODEL)
              l2_context = mid_l2_chunks.mean(dim=2)
              core_input = self.cortex_in(l2_context.detach())
      
              H_chunk = H_norm.view(batch_size, num_chunks, CHUNK_SIZE).mean(dim=2).unsqueeze(-1)
              Top1_chunk = Top1_front.view(batch_size, num_chunks, CHUNK_SIZE).mean(dim=2).unsqueeze(-1)
              mode_signal = torch.cat([H_chunk, Top1_chunk], dim=-1)
      
              fast_rnn_input = torch.cat([core_input, mode_signal], dim=-1)
      
              if core_state is not None:
                  h_fast_hidden, h_slow_hidden = core_state
              else:
                  h_fast_hidden = torch.zeros(1, batch_size, CORE_DIM, device=x.device, dtype=emb_x.dtype)
                  h_slow_hidden = torch.zeros(1, batch_size, CORE_DIM, device=x.device, dtype=emb_x.dtype)
      
              fast_out, h_fast_hidden = self.fast_rnn(fast_rnn_input, h_fast_hidden)
              slow_input = fast_out[:, 0::SLOW_UPDATE_INTERVAL, :]
              slow_out, h_slow_hidden = self.slow_rnn(slow_input, h_slow_hidden)
      
              h_fast_transposed = h_fast_hidden.transpose(0, 1)
              fast_states_chunk = torch.cat([h_fast_transposed, fast_out[:, :-1, :]], dim=1)
      
              S = slow_out
              h_slow_transposed = h_slow_hidden.transpose(0, 1)
              slow_states_chunk = torch.cat([
                  h_slow_transposed,
                  S[:, 0:1, :].repeat(1, 4, 1),
                  S[:, 1:2, :].repeat(1, 4, 1),
                  S[:, 2:3, :].repeat(1, 4, 1),
                  S[:, 3:4, :].repeat(1, 3, 1),
              ], dim=1)
      
              fast_states_seq = fast_states_chunk.repeat_interleave(CHUNK_SIZE, dim=1)
              slow_states_seq = slow_states_chunk.repeat_interleave(CHUNK_SIZE, dim=1)
      
              mod_fast = self.bridge_fast(fast_states_seq) * self.gate_fast
              mod_slow = self.bridge_slow(slow_states_seq) * self.gate_slow
      
              if gate_override is not None:
                  mod_fast = mod_fast * 0
                  mod_slow = mod_slow * 0
      
              if mod_override is not None:
                  mod_fast = mod_override
                  mod_slow = torch.zeros_like(mod_fast)
      
              modulated_mid_seq = mid_l2_seq + mod_fast + mod_slow
      
              final_l2_seq = self.transformer_back(modulated_mid_seq, mask=causal_mask)
              logits_seq = self.head(final_l2_seq)
      
              loss_ce, acc10 = None, None
              if targets is not None:
                  loss_ce = F.cross_entropy(logits_seq.view(-1, VOCAB_SIZE), targets.reshape(-1))
                  with torch.no_grad():
                      _, top10 = torch.topk(logits_seq.view(-1, VOCAB_SIZE), 10, dim=-1)
                      acc10 = (top10 == targets.reshape(-1).unsqueeze(1)).sum().item() / targets.numel() * 100
      
              internals = None
              if return_internals:
                  internals = {
                      "mid_l2_seq": mid_l2_seq.detach(),
                      "mod_fast": mod_fast.detach(),
                      "mod_slow": mod_slow.detach(),
                      "modulated": modulated_mid_seq.detach(),
                  }
      
              next_core_state = (h_fast_hidden.detach(), h_slow_hidden.detach())
              return loss_ce, acc10, internals, next_core_state
      
      # --- ЗАГЛУШКИ ТЕСТИРОВЩИКА ---
      class PositionalBias(nn.Module):
          def __init__(self):
              super().__init__()
              self.bias = nn.Parameter(torch.zeros(1, SEQ_LEN, D_MODEL))
          def forward(self, mid_l2_seq):
              return self.bias.expand(mid_l2_seq.shape[0], -1, -1)
      
      class ChunkBias(nn.Module):
          def __init__(self):
              super().__init__()
              self.bias = nn.Parameter(torch.zeros(1, NUM_CHUNKS, D_MODEL))
          def forward(self, mid_l2_seq):
              return self.bias.repeat_interleave(CHUNK_SIZE, dim=1).expand(mid_l2_seq.shape[0], -1, -1)
      
      # --- ИНСТРУМЕНТЫ ОЦЕНКИ ---
      @torch.no_grad()
      def evaluate(model, mode="noise", loader=None, n_batches=100, batch_size=64, gate_override=None, mod_override_fn=None):
          total_ce = 0.0
          total_acc = 0.0
          core_state = None # Инициализация стейта для сквозного прохода
      
          for _ in range(n_batches):
              if mode == "noise":
                  x = torch.randint(0, VOCAB_SIZE, (batch_size, SEQ_LEN + 1), device=device)
                  inp, tgt = x[:, :-1], x[:, 1:]
                  core_state = None # На шуме стейт сбрасывается (амнезия)
              else:
                  inp, tgt = loader.next_batch()
      
              mod_ov = None
              if mod_override_fn is not None:
                  _, _, internals, _ = model(inp, core_state=core_state, return_internals=True)
                  mod_ov = mod_override_fn(internals["mid_l2_seq"])
      
              loss_ce, acc10, _, core_state = model(inp, targets=tgt, core_state=core_state, gate_override=gate_override, mod_override=mod_ov)
              total_ce += loss_ce.item()
              total_acc += acc10
      
          return total_ce / n_batches, total_acc / n_batches
      
      @torch.no_grad()
      def measure_modulation_stats(model, mode="noise", loader=None, n_batches=50, batch_size=64):
          all_mods = []
          all_mids = []
          core_state = None
      
          for _ in range(n_batches):
              if mode == "noise":
                  inp = torch.randint(0, VOCAB_SIZE, (batch_size, SEQ_LEN), device=device)
                  core_state = None
              else:
                  inp, _ = loader.next_batch()
      
              _, _, internals, core_state = model(inp, core_state=core_state, return_internals=True)
              mod = internals["mod_fast"] + internals["mod_slow"]
              all_mods.append(mod.cpu())
              all_mids.append(internals["mid_l2_seq"].cpu())
      
          mods = torch.cat(all_mods, dim=0)
          mids = torch.cat(all_mids, dim=0)
          mod_mean = mods.mean(dim=0)
          mod_residual = mods - mod_mean.unsqueeze(0)
      
          mean_sq = mod_mean.norm(dim=-1).pow(2).mean().item()
          total_sq = mods.norm(dim=-1).pow(2).mean().item()
          constant_frac = mean_sq / total_sq if total_sq > 0 else 0.0
      
          print(f"    ||mod_total||        = {mods.norm(dim=-1).mean():.4f}")
          print(f"    ||mod_mean|| (const) = {mod_mean.norm(dim=-1).mean():.4f}")
          print(f"    energy: constant/total = {constant_frac:.4f} ({constant_frac*100:.1f}%)")
          
          pos_var = mods.var(dim=0).mean(dim=-1)
          print(f"    position variance: mean={pos_var.mean():.4f}")
      
      def run_tests_for_mode(model, mode, loader):
          print(f"\n" + "="*50)
          print(f" РЕЖИМ ТЕСТИРОВАНИЯ: {mode.upper()}")
          print("="*50)
          
          print("\n[A] Baseline (Полная модель)")
          ce_a, acc_a = evaluate(model, mode=mode, loader=loader)
          print(f"    CE = {ce_a:.4f}, Acc@10 = {acc_a:.2f}%")
      
          print("\n[B] Ablation (Врата = 0, Кортекс отключен)")
          ce_b, acc_b = evaluate(model, mode=mode, loader=loader, gate_override=True)
          print(f"    CE = {ce_b:.4f}, Acc@10 = {acc_b:.2f}% (Delta: {ce_b-ce_a:+.4f})")
      
          print("\n[MODULATION STATS]")
          measure_modulation_stats(model, mode=mode, loader=loader)
      
      def main():
          if not os.path.exists(CKPT_PATH):
              print(f"Файл {CKPT_PATH} не найден.")
              return
      
          sd = torch.load(CKPT_PATH, map_location=device, weights_only=True)
          model = TriuneVortex().to(device)
          model.load_state_dict(sd)
          model.eval()
      
          print(f"Веса загружены: {CKPT_PATH}")
          print(f"gate_fast={sd['gate_fast'].item():.4f}, gate_slow={sd['gate_slow'].item():.4f}")
          
          loader = None
          if glob.glob(TRAIN_FILES_MASK):
              loader = StatefulTokenLoader(TRAIN_FILES_MASK, batch_size=64, seq_len=SEQ_LEN)
          
          # Запуск на белом шуме
          run_tests_for_mode(model, mode="noise", loader=None)
          
          # Запуск на реальных данных
          if loader:
              run_tests_for_mode(model, mode="real", loader=loader)
      
      if __name__ == "__main__":
          main()

      UPD: появилась идея, что это ещё и семантический сдвиг (в зависмости от домена), но это было бы чересчур хорошо, потом протестирую на разного типа текстах.


  1. proxy3d
    26.05.2026 18:27

    Рекомендую обратить внимание на "оживший" не так давно подход PMax. Исторически Predictability Maximization (PMAX) у Шмидхубера и последующих интерпретаций. Это не совсем привычные предсказания, а скорее про два конкурирующих требования:

    1. Predictability (предсказуемость)

    части представления должны быть объяснимы / предсказываемы из других частей

    2. Discriminability (различимость)

    представление не должно схлопываться в константу и должно сохранять структуру различий

    Так как то что описали выше про бутылочное горлышка, как раз центральная проблема PMAX/JEPA класса:

    Если максимизировать только предсказание, то всё схлопнется в константу

    Если максимизировать только различимость, то получаем шум вместо структуры.

    Поэтому PMAX фактически баланс 2 сил. Система должна быть максимально предсказуемой внутри структуры, но максимально различающей между структурами.

    В Jepa одна из проблем, что система находит лазейку и старается все свети к константе. Изначально PMAX (разработанный еще в 90-ых) как раз был про то, что принцип построения представлений, которые одновременно максимально предсказуемы из контекста и максимально информативны. Не знаю, как проще объяснить. Может на видео будет понятней.

    То есть это две конкурирующих потока, один заставляет систему лучше предсказывать, а второй чтобы предсказания отличались.

    Недавно на одном видео попадался разбор, где пытались как раз это объединить в рамках разных архитектурных подходов. Там еще PINN, но ее можно опустить в видео.

    Суть в том, чтобы система не только искала как лучше предсказать (а то будет константа или 0), а одновременно стремилась к разнообразию. Может это может в рамках комментария выше про Мотор подстроился под постоянный сдвиг от кортекса, а не под его "смысл".


    1. Kamil_GR Автор
      26.05.2026 18:27

      Может это может в рамках комментария выше про Мотор подстроился под постоянный сдвиг от кортекса, а не под его "смысл".

      Скорее всего нет, выше я ответил на комментарий. Очень высока вероятность того, что кортекс накладывает маску на структурные шумы трансформера и датасета. Чистый трансформер такого уровня бпб не добивается.

      Сейчас уже обучается следующая версия кода, там расширен кортекс и поменял аддитивное сложение на сигмоиду... По идее бегство кортекса в константу должно быть минимизировано. Смотрю..