Оглавление

Введение

Давайте заранее договоримся, в статье термин емкость означает информационную емкость, разрядность, битность (в двоичной логике), тритность (в троичной) и т.д. Емкость (количество) - это то, сколько всего бит/трит/квадрит выделено на число. Точность (качество) - насколько детально можно представить число в рамках данной емкости.

Любой кто когда либо запускал локальную нейронку, сталкивался с таким понятием как "квантизация". Квантизация - это фактически упрощение нейронки, позволяющее запускать ее на слабом железе. Чем сильнее квантизация - тем меньше весит файл модели, но хуже качество инференса. Изначально модели обучают используя веса с высокой точностью. Но размеры файла модели занимают слишком много места. Поэтому народ активно такие модели квантует, чтобы они "влезли" в ОЗУ видеокарты для локального запуска.

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

Но есть и другие варианты облегчить себе жизнь. Один из них - перейти на нейронки с меньшей точностью. Зачем обучать на FP32, а потом квантовать до INT4 (Q4), когда можно сразу обучать на INT4? Меньше требования к железу, меньше требования к ОЗУ. А значит - дешевле?

А вот и нет

Если всю модель полностью перевести на более слабую емкость (с FP32 на INT4) - модель будет обучатся нестабильно, градиенты могут "застрять" на локальных минимумах. Поэтому самые чувствительные места (градиенты, состояния оптимизатора Adam) оставляют как у обычных моделей в FP32/BF16.

Но, давайте сначала чуток освежим знания.

>> Вас всю жизнь обманывали

Когда мы смотрим на эти красивые "форматы сжатия", кажется что всё это - нативно поддерживаемые форматы современных компьютеров. Такое же ощущение появляется, когда вы видите в своем любимом языке программирования типы byte, int, float, double...

На самом деле всё это - разные интерпретации одной и той же двоичной последовательности, в основе которой - бит, который имеет всего два возможных значения: 0 или 1. Аппаратно поддерживаются не все варианты: где-то поддерживаются операции над байтами, целыми числами и числами с плавающей запятой. А где-то поддержки чисел с плавающей запятой нет вовсе. Все зависит от железа. Но на фундаментальном уровне - везде один лишь бит.

Что значит "поддерживаются"? Это значит что в железе присутствуют аппаратные инструкции которые умеют работать с разными представлениями значения: складывать/умножать/делить значения представленные группами битов, например в double. Аппаратные - значит прямо на чипе/кремнии физически есть транзисторы, объединённые в логические схемы, реализующие операции над определёнными представлениями данных. На самом деле даже значения 0 и 1 не существует - это просто уровень заряда, напряжения, тока, магнитного состояния и т.д.

Когда мы видим новость "выпущен ускоритель AMD MI300 аппаратно поддерживающий вычисления на FP8", это значит что в нем есть аппаратные инструкции работающие с битами объединенными в представление FP8.

Сравнительная таблица форматов и систем счисления. Сюда намеренно внесены чужеродные (для бинарной) системы счисления: трит (три состояния, троичная логика), квадрит (четыре состояния), квинтит (пять состояний), секстит (шесть состояний), септит (семь состояний), октит (восемь состояний), нонит (девять состояний) и децит (десять состояний).

Представление/формат/система счисления

Информационная ёмкость в битах

Кол-во
состояний

Диапазон

Binary / Бит / Двоичная

1

2

0, 1

Ternary / Трит / Троичная

1.585

3

−1, 0, +1

Quaternary / Квадрит / Четвертичная

2

4

−2, −1, +1, +2

INT2

2

4

−2 … +1

Quinary / Квинтит / Пятеричная

2.322

5

−2, −1, 0, +1, +2

Senary / Секстит / Шестеричная

2.585

6

−3, −2, −1, +1, +2, +3

Septenary / Септит / Семеричная

2.807

7

−3 … +3

INT4

4

16

−8 … +7

Octal / Октит / Восьмиричная

3

8

−4, −3, −2, −1, +1, +2, +3, +4

Nonary / Нонит / Девятичная

3.170

9

−4 … +4

Decimal / Децит / Десятичная

3.322

10

0 ... 9

INT8

8

256

−128 … +127

FP8 E4M3

8

256

≈ −448 … +448

FP8 E5M2

8

256

≈ −57 344 … +57 344

INT16

16

65 536

−32 768 … +32 767

BF16

16

65 536

≈ −3.39×10³⁸ … +3.39×10³⁸

FP16

16

65 536

≈ −65 504 … +65 504

INT32

32

4.29×10⁹

−2.147×10⁹ … +2.147×10⁹

FP32

32

4.29×10⁹

≈ −3.403×10³⁸ … +3.403×10³⁸

INT64

64

1.84×10¹⁹

−9.22×10¹⁸ … +9.22×10¹⁸

FP64

64

1.84×10¹⁹

≈ −1.798×10³⁰⁸ … +1.798×10³⁰⁸

INT128

128

3.40×10³⁸

−2¹²⁷ … +2¹²⁷−1

FP128

128

3.40×10³⁸

≈ −1.190×10⁴⁹³² … +1.190×10⁴⁹³²

Квадрит и INT2 это одно и то же?

Казалось бы, у них всё совпадает: емкость, количество состояний. Но нет.

INT2 — это конкретный способ интерпретации двух битов как целого числа. Но это именно интерпретация битов имеющих любые комбинации 00, 01, 10, 11!

Основные интерпретации бинарного INT2:

  • беззнаковое (UINT2): 0, +1, +2, +3

  • дополнительный код (two's complement): 0, +1, -2, -1

  • обратный код (one's complement): +0, +1, −1, −0

  • прямой код (sign-magnitude): +0, +1, −0, −1

Квадрит же сам по себе не определяет, какие именно значения соответствуют его четырём состояниям. Это могут быть 0, 1, 2, 3| −2, −1, +1, +2| A, B, C, D или вообще четыре произвольных символа.

Иными словами, INT2 - это интерпретация четырёх состояний бинарной системы как целого числа, а квадрит — информационная единица, имеющая четыре различимых состояния.

Значения диапазона в этой таблице - условные: в том же "квадрите" значения могут быть какими угодно:

  • −2, −1, 0, +1

  • −2, −1, +1, +2

  • 0, 1, 2, 3

  • A, B, C, D

  • 𓂀\hspace{0.2em}, 𓃠\hspace{0.2em}, 𓆣\hspace{0.2em}, \hspace{0.2em}𓇼

Даже информационная емкость в данном случае интерпретируется через бинарное представление: Q = \log_2(N), где:

  • Q - информационная ёмкость в битах

  • N - количество состояний

Например "трит", в бинарной системе счисления имеет информационную емкость Q=log_2​(3)≈1.585 бит.

Пример того, как 32 бита можно интерпретировать в разных форматах. Видно что в FP32 может влезть два FP16/BF16, либо четыре INT8, либо восемь INT4.
Пример того, как 32 бита можно интерпретировать в разных форматах. Видно что в FP32 может влезть два FP16/BF16, либо четыре INT8, либо восемь INT4.
Для справки: что такое экспонента, мантисса...
  • Экспонента определяет диапазон значений

  • Мантисса определяет точность представления

Пример для экспоненты:

Экспонента

Максимальное значение

4 бита

~256

5 бит

~65 504

8 бит

~3.4 × 10^38

Пример для мантиссы:

Мантисса

Число

3 бита

0.125

7 бит

0.1234

23 бита

0.12345679

Получается, на физическом уровне, уровень заряда (и другие виды реализации логики) интерпретируется как 0 или 1. Затем последовательности этих значений интерпретируются в разное представление: uint8, int32, float32, float64 и т.д. Потом и эти представления объединяются и интерпретируются как структуры данных: массивы, объекты. И всё это в итоге объединяется и интерпретируется как: текст, изображение, звук.

То есть, люди придумали уровни абстракции и правила интерпретации физических состояний, а затем построили поверх них всё то многообразие, которое мы называем данными, типами, форматами. Проще говоря - от интерпретации зависит всё. Купил ардиунку, не прочитал документацию, подал 220В на контакты - спалил ардуинку. Печаль.

>> Градиент

Давайте немного поясним что это за "градиент" был в самом начале статьи, и какое отношение он имеет к FP32, FP16, BF16.

Нейронка состоит из слоёв, где каждый слой - это массив чисел, весА. Как раз веса состоят из чисел с плавающей запятой. В нашем случае - из чисел с точностью FP32.

Из чего состоит GPT-2 можно глянуть в этой статье.

Конечно вес - это не всегда сам нейрон. Обычно нейрон это вес + смещение (bias). Когда на вход нейрона подают входящий сигнал, сначала сигнал умножается на вес, а потом добавляется смещение, применяется функция активации... Вес - это направление и важность сигнала (усилить или ослабить), смещение - корректировка порога срабатывания нейрона.

Схематичный нейрон, схематично "обрабатывает" сигнал.
Схематичный нейрон, схематично "обрабатывает" сигнал.

В некоторых нейронках (LLaMA, Gemma, Mistral, OLMo) смещение намеренно отключено в линейных слоях. В тритовой нейронке его вообще нет, но об этом позже.

Обучение нейронки происходит так: ей на вход подают последовательность токенов, и после "прохода" через слои сеть пытается предсказать следующий токен. Результат сравнивается с правильным ответом (следующим токеном), вычисляя связь между ними. Чем хуже связь - тем больше ошибка. Чтобы уменьшить эту ошибку, веса нейронки нужно подкорректировать (увеличить или уменьшить).

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

Оптимизатор - это алгоритм подкручивающий веса модели, исходя из значений градиента. Самые распространенные: SGD (Stochastic Gradient Descent), Adam, AdamW.

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

>> Проблема FP32

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

Модели росли не по дням, а по часам. Но внезапно индустрия ML (Machine learning) при обучении нейронок уперлась в пропускную способность памяти видеокарт (bottleneck). Высокая емкость из спасителя превратилась в тяжелую ношу для видеокарт: при обучении моделей в миллиарды параметров, видеокартам пришлось перегонять через себя огромные объемы данных между памятью и вычислительными ядрами. При этом, вычислительные ядра стали простаивать в ожидании информации.

Что бы облегчить видеокартам жизнь, ML-инженеры придумали схему: оригинальные веса в FP32 копировались в веса в формате FP16, на этой копии производились умножения, а результат (градиент) применялся к оригинальным мастер-весам. Назвали это - "смешанной точностью" (mixed precision). То есть нейронку в FP32 мы обучаем во временном пониженном формате.

Стадии обучения:

  1. Мастер-веса хранятся в FP32 - это оригинал.

  2. Копия: из мастера создаётся рабочая копия весов в пониженном формате (FP16 / BF16 / FP8 / MXFP4).

  3. Forward: тензорные ядра перемножают квантованные тензоры-копии; суммы накапливаются в FP32 (предсказание и loss).

  4. Backward: градиенты считаются на тех же квантованных тензорах; накопление в FP32.

  5. Оптимизатор: готовые градиенты применяются к мастер-весам.

  6. см. пункт 2.

Но маленький диапазон значений FP16 приводил к двум проблемам: маленькие значения приводили к обнулению градиента, большие значения улетали в бесконечность. Костыли вроде Loss Scaling (искусственное раздувание градиента) боролись с обнулением градиентов. Но вторая проблема с переполнением градиентов оставалась.

>> BF16

Тогда инженеры Google пошли на хитрость и создали формат BF16 (Brain Float 16), устраняющий проблему с переполнением градиентов. Они оставили такую же емкость в 16 бит, но увеличили экспоненту до 8 бит как у FP32: урезали точность округления, но оставили такой же диапазон значений как в FP32.

Схематичное изображение формата. Лучи - все возможные значения.
Схематичное изображение формата. Лучи - все возможные значения.

Сравнение максимального числа (бинарный вид и значение):

  • FP32

    • 0 | 11111110 | 11111111111111111111111

    • \approx \mathbf{3.4\times 10}^{\mathbf{38}}

  • BF16

    • 0 | 11111110 | 1111111

    • \approx \mathbf{3.4\times 10}^{\mathbf{38}}

  • FP16

    • 0 | 11110 | 1111111111

    • 65 504

И что же мы видим? FP16 хуже по диапазону возможных значений чем FP32 и BF16 в:

\frac{3.40282 \times 10^{38}}{65504} \approx 5.195 \times 10^{33} раза. Т.е. в 5 дециллионов раз.

Подсчет разницы диапазонов через максимальное значение - рабочий, потому что FP32 FP16 и BF16 - знаковые и симметричные. А вот посчитать разницу диапазонов между например INT32 и UINT16 через максимальное значение было бы некорректным. Надо делить количество всех состояний.

То есть FP16 и BF16 имеет одинаковое количество значений (2¹⁶ = 65 536). Но у BF16 диапазон значений почти как у FP32 и он размазан с худшей точностью чем у FP32.

FP32:

  • 1,0000001192092896

  • 1,0000002384185791

  • 1,0000003576278687

BF16:

  • 1,0078125

  • 1,0156250

  • 1,0234375

>> FP8

Следующим шагом стал переход на FP8 (256 комбинаций), который разделили на "подформаты":

  • E4M3 - используется для прямого прохода (активации и веса)

  • E5M2 - для обратного прохода (расчет градиентов)

E4M3 - буквально экспонента 4 бита, мантисса 3 бита.

Правильнее было бы назвать S1E4M3 - где S это бит знака.

Оказалось, что для разных процессов можно использовать разную емкость. При прямом проходе (forward) активации в E4M3 требуют меньше данных для передачи и ускоряют вычисления. При обратном проходе (backward pass, backpropagation) операции в E5M2 выполняются в разы быстрее.

E4M3 (4 бита экспоненты + 3 бита мантиссы): здесь активации лежат в узком диапазоне, поэтому широкий диапазон не нужен, но важна точность (меньше экспоненты, больше мантиссы).

E5M2 (5 бит экспоненты + 2 бита мантиссы): градиенты имеют огромный разброс, поэтому критичен широкий диапазон значений, а точность можно принести в жертву (больше экспоненты, меньше мантиссы).

>> FP4? MXFP4!

Так, стоп, а где FP6?

"На бумаге" это идеальный компромисс: он точнее чем FP4, и экономичнее чем FP8. Но в реальной жизни он "неудобен" для железа ориентированного на степени двойки. Существует так называемое "выравнивание байтов": способ размещения данных в оперативной памяти компьютера, при котором адрес начала переменной в памяти должен быть кратен её размеру.

Помните в ассемблере непонятная директива align? Или в паскале для передачи байта по сети например, писали TMyRecord = packed record, потому что без этого структура раздувалась?

Выравнивание байтов необходимо для ускорения работы с данными: грубо говоря процессор считывает данные из памяти не по одному байту, а "словами" (блоками по 4, 8, 16 и т.д. байт за раз).

Пример:

  • Если 4-байтовое число выровнено (например, лежит по адресу 0x04), процессор прочитает его за 1 цикл

  • Если число не выровнено (например, лежит по адресу 0x03), оно окажется на стыке двух блоков памяти. Процессору придется сделать 2 цикла чтения, а затем аппаратно "склеивать" эти байты

  • Если число занимает два блока памяти: процессору опять придется сделать 2 цикла чтения. Это наш случай.

Вот и получается, что 6-битное число не укладывается в сетку памяти: оно меньше байта, поэтому не может иметь собственного адреса, и его пакуют впритык с соседями - по 4 штуки в 3 байта. Из-за этого часть чисел оказывается на стыке двух байтов: что бы прочитать одно, приходится считывать сразу два, сдвигать и вырезать нужные биты.

Конечно есть ускоритель от AMD Instinct MI350 у которого FP6 поддерживается на аппаратном уровне (вынужденная конкуренция с архитектурой NVIDIA Blackwell). Вернее формат MXFP6 из открытого стандарта Microscaling. Но о блочном формате - дальше.

Итак, у FP4 всего 16 комбинаций. В 16 комбинаций запихнуть достаточный для обучения диапазон и точность невозможно - битов физически не хватает на сколько то значимую экспоненту и мантиссу. То есть реализовать каждый вес в формате FP4 бесполезно.

Поэтому инженеры пошли на хитрость и объединили веса FP4 (S1E2M1 - знак 1 бит, экспонента 2 бита, мантисса 1 бит) в блок по 32 элемента и добавили одну общую 8 битную экспоненту (E8M0 - экспонента 8 бит) для всех весов в блоке. Эта экспонента является рычагом для изменения диапазона значений для каждого элемента в блоке. В простонародье - "зум". Так появился блочный формат MXFP4.

Блок MXFP4. Развернул представление, что бы влезло на экран.
Блок MXFP4. Развернул представление, что бы влезло на экран.

Зачем каждому FP4 своя экспонента, если есть общая аж на 8 бит? Мини-экспонента в FP4 не дает округлить значение каждого веса во время обучения нейронки.

Главный выигрыш в обучении, помимо уменьшения объемов занимаемой памяти заключается в том, что в MXFP4 дешевле арифметика (перемножаются простые целые 4-битные числа), а масштаб применяется один раз после накопления всей суммы - одним сдвигом экспоненты.

>> Бит vs Трит

Мы уже рассматривали FP4 как рабочий вариант для построения полноценной нейронки, правда пришлось пойти на трюк, объединяя такой формат в блок из 32 штук, и вынос экспоненты.

Так может взять и сразу перепрыгнуть на Бит? Сделать однобитовую нейронку?

Давайте подумаем, что такое нейрон? Нейрон это обучаемый элемент имеющий внутреннее состояние, с помощью которого он может видоизменять проходящий через себя сигнал. Обучаемый - значит способный менять внутреннее состояние. Полноценный нейрон может делать три вещи: усиливать сигнал, ослаблять сигнал, и "молчать".

Главная фундаментальная проблема битового нейрона - у него физически не может быть трех вариантов внутреннего состояния, поэтому он либо только усиливает и ослабляет [-1,+1], либо только "молчит" и усиливает [0,1].

В контексте нейросети из таких нейронов - это приводит к перекосу при обучении:

  • [-1,+1] Слабые и нерелевантные признаки постоянно вносят свой вклад и фонят шумом, поэтому сеть не может стать избирательной, и ее обобщающая способность падает

  • [0, +1] Сеть умеет только наращивать сигнал, из-за чего ей недоступны немонотонные функции (например, "исключающее ИЛИ")

И тут, весь в белом пальто на сцену выходит трит - тот самый, полноценный нейрон. У него есть три состояния [−1, 0, +1], позволяющие построить обучаемую нейронку. Да конечно два бита (условный формат FP2) [-1, 0, +1, +2] например тоже подходит для полноценной нейронки, но в качестве минимально возможного нейрона - трит, вне конкуренции.

Схематичный пример прохода сигнала через нейроны. Серый цвет - нейрон неактивный (имеет значение 0).
Схематичный пример прохода сигнала через нейроны. Серый цвет - нейрон неактивный (имеет значение 0).

На картинке видно, что в тритовой/тернарной нейронке много неактивных нейронов выделенных серым цветом (т.е. имеющих состояние 0 - "молчу"), это называется "разряженность" (sparse). Разреженность дает полезные вещи:

  • Подавление шума: слабые признаки отключаются, и не вносят вклад в сигнал

  • Экономия на вычислениях: неактивные нейроны не участвуют в вычислениях

  • Обобщение: сеть вынуждена строить "правильные" пути движения сигнала, что снижает переобучение

Глядя на серые неактивные нейроны в тритовой нейронке, кажется, что сеть работает неэффективно: столько нейронов неактивно, столько простаивает! Но... в выключенном состоянии они тоже "выполняют работу" - не позволяют тратить вычислительные ресурсы на неэффективные связи между нейронами.

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

Стоит упомянуть, еще на заре компьютерной эпохи в СССР был реализован полноценный компьютер на троичной логике "Сетунь", под руководством Николая Петровича Брусенцова (1925–2014). Он же - главный конструктор. В 1956–1958 годах был разработан троичный ферритодиодный элемент, и на его основе - первая в мире вычислительная машина на троичной логике.

"Сетунь" использовала троичную логику с симметричным кодом [-1, 0, 1]. Компьютер оперировал Трайтами (6 тритов), что эквивалентно 9,5 двоичным битам.

Тактовая частота 200 кГц, 4500 операций в секунду:

  • Сложение: 180 мкс.

  • Умножение: 325 мкс.

  • Передача управления: 100 мкс.

Интересная статья: «Я Сетунь 70». Эмулятор троичного компьютера на Rust

>> B1.58

Microsoft решила реализовать нейронку в тритах, и назвала ее BitNet 1.58. Но если до этого обучение велось на пониженных форматах, а целью была полноценная нейронка на FP32 (BF16). То в этот раз наоборот: обучение ведется в мастер-весах BF16, а тернарные веса получаются путем квантования мастер-весов в триты (на каждом forward-проходе).

После обучения, можно спокойно выкинуть BF16 мастер-веса, и использовать тернарную нейронку для инференса.

Страничка на huggingface, веса в gguf, движок для инференса bitnet.cpp

В обучении используется трюк STE (Straight-Through Estimator). Суть трюка: градиент, который Backward принёс от loss к триту, пропускается сквозь квантователь к оптимизатору без изменений. Благодаря этому мастер-веса плавно дрейфуют под потоком градиентов, накапливая микроизменения, а сами триты меняются дискретно - "переключаются" (из нуля в ±1) в тот момент, когда мастер пересекает порог (среднее значение модулей весов слоя).

Все понятно?
Он - все понял
Он - все понял

Проще говоря, мастер-веса при обучении плавно меняют значения (что то типа увеличить вес нейрона на +0.0001), а трит - он дискретный. У него всего три состояния между которыми он может переключаться. Как понять в какой момент нужно переключать трит? Для этого и вводится некий "порог" значения нейрона в мастер-весах, после достижения которого и переключается трит. При этом порог - не фиксированный, он вычисляется отдельно для каждого слоя и равен среднему значению модулей всех мастер-весов этого слоя.

Но стОит отметить: нет никакого точечного изменения каждого трита. Блок мастер-весов при каждом forward проходе переводится в тритовый и полностью перезаписывает старый тритовый блок:

БЛОК МАСТЕР-ВЕСОВ (BF16)
┌───────────────────────────────────┐
│  0.03  │ -0.41  │  0.27  │ -0.09  │
│  0.18  │  0.55  │ -0.33  │  0.11  │
│ -0.22  │  0.07  │ -0.48  │  0.39  │
└───────────────────────────────────┘
                  │
                  │ вычисляем порог
                  │ среднее модулей всех весов
                  │ (0.03+0.41+0.27+...+0.39) / 12 ≈ 0.26
                  │
                  ▼
            КВАНТИЗАЦИЯ
        ┌───────────────────┐
        │  вес > +0.26 → +1 │
        │  вес < -0.26 → -1 │
        │  иначе       →  0 │
        └───────────────────┘
                  │
                  ▼
БЛОК ТЕРНАРНЫХ ВЕСОВ (триты)
┌───────────────────────────────────┐
│   0    │  -1    │  +1    │   0    │
│   0    │  +1    │  -1    │   0    │
│  -1    │   0    │  -1    │  +1    │
└───────────────────────────────────┘

Конечно в результате не все слои нейронки полностью тернарные. Нельзя например перевести в триты входящий слой, связывающий ID токенов с его числовым описанием (вектором). А так же последний слой, переводящий внутреннее состояние модели в вероятность следующего слова.

>> Ternary LLM

Ну штош, давайте попробуем сделать максимально минимальную нейронку на тритах. В статье Обучение LLM с нуля на c# и экспорт в GGUF. OpenCL против CUDA мы уже реализовали нейронку, которая после экспорта в формат GGUF стала занимать на диске 422Кб.

Возьмем ее за основу, но переделаем на архитектуру Llama, и оставим обучение на CPU. Llama - дешевле при обучении чем GPT-2.

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

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

...Как собственно это же делает сам Bitnet1.58. Других устаканившихся рабочих рецептов полноценного обучения сразу на тритах, на данный момент нет, ну или я о них не знаю.

Архитектура нейронки:

  • BPETokenizer токенизатор.
    Превращает текст в токены (индексы).

  • MultiPlaneEmbedding слой (с динамическим квантованием весов / QAT).
    Превращает токены в векторы. Модель не может работать просто с индексами, потому что индекс - это просто номер. Динамическое квантование (QAT) нужно для того чтобы модель сразу училась работать с тернарными весами. Во время обучения модель уже видит квантованные веса и приспосабливается к ним.

  • TernaryLlamaBlock блок (RMSNorm, GQA Self-attention + KV cache, SwiGLU FFN, Residual connections).
    Понимает контекст и обрабатывает векторы.

    • RMSNorm приводит векторы к нормальному масштабу.

    • GQA (Grouped-Query Attention) - экономная "голова внимания": несколько Q-голов используют общие K и V.

    • KV кэш ускоряет инференс, что бы при генерации каждого токена не пересчитывать предыдущие токены заново.

    • SwiGLU FFN усиливает важные признаки и отбрасывает лишние.

    • Residual connections для стабилизации обучения.

  • Final RMSNorm
    Приводит финальный вектор в порядок.

  • MultiPlaneLinear
    Превращает вектор в вероятности следующих токенов - логиты.

Как мы уже знаем, один бит - всего два значения. Но если объединить несколько битов - будет намного более ёмкая единица информации: byte, short, int, float и т.д. Что нам мешает увеличить емкость тритовой нейронки таким же макаром? Правильно, ничего. Поэтому был придуман параметр planes: количество тритов на нейрон, "плоскость", как слои пирога. Больше значение - более ёмкие тритовые слои у нейронки.

Самая минимальная нейронка на точно таком же корпусе как в LLMGPT2, но уже на Llama архитектуре позволяет сделать такую конфигурацию (106496 параметров):

  Embedding dim = 64
  FFN hidden dim = 64
  Num heads = 1
  Num KV heads = 1
  Num layers = 1
  Max seq len = 512
  BPE vocab size = 16000
  Trit planes = 1

Обучение удалось довести до Loss=0,26.
При этом размер мастер-модели (веса + токенизатор): 420 КБ.
А размер тернарной нейронки: 28,9 КБ (из них 3,71 КБ - данные токенизатора).


И, барабанная дробь, самая экстремальная конфигурация (49152 параметров):

  Embedding dim = 32
  FFN hidden dim = 32
  Num heads = 1
  Num KV heads = 1
  Num layers = 1
  Max seq len = 512
  BPE vocab size = 16000
  Trit planes = 1

Обучение удалось довести до Loss=0,42, ниже - не получается.
При этом размер мастер-модели (веса + токенизатор): 196 КБ
А размер тернарной нейронки: 17,1 КБ (из них 3,71 КБ - всё так же данные токенизатора).
Моделька может хуже отвечать в интерактивном чате.

Та самая модель размером 17,1 КБ
Та самая модель размером 17,1 КБ

>> Итого

Щепотка терпения, горсть нервных клеток, ложка промптов

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

Данный проект - попытка понять насколько малую (но полноценную в плане архитектуры) модель можно сделать на модных нынче тритах. Обучение по прежнему ресурсозатратное, поэтому и был выбран самый "щадящий" корпус, что бы каждый мог попробовать обучить модель на своем железе.

Для тех кто хочет проверить сразу:
Исходный код: https://github.com/virex-84/LLamaTritLLM
Релиз (отдельно два варианта моделей): https://github.com/virex-84/LLamaTritLLM/releases/tag/v1.0

А на сегодня всё...

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Продолжать статьи на тему НЕпрактического применения ИИ?
81.82%Да, отлично, продолжай в том же духе9
0%Надоел ИИ слоп0
27.27%Мне, пожалуйста, три гамбургера, пять чизбургеров, картошку фри, три хот-дога… и диетическую колу, я слежу за фигурой3
Проголосовали 11 пользователей. Воздержались 2 пользователя.