PicoCalc

Алгоритм LUT8

На сегодня нейросеть в обвязке внутри «МК-61s», запущенная на «ПикоКалке», работает со скоростью примерно 0,43 секунды на токен. Постепенно становится быстрее, но ещё на порядок быстрее не станет,  тут уже нужно что-то принципиально менять.

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

Нейросеть МК-61s на ПикоКале использует алгоритм LUT8 для ускорения вычислений путём замены умножения поиском в таблице и сложением.
Вот такие примеры сейчас способна правильно решить нейросеть после нескольких дней обучения

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

Теперь про LUT8 — идея в том, чтобы заменить умножение поиском по таблице и сложением для восьми активаций сразу.

Для восьми значений мы считаем таблицу сумм из 256 элементов. Если порядок в таблице представить в виде битовой маски, то получится восемь флагов (256 значений от 00000000 до 11111111) — учитывается ли каждая активация в сумме или нет. Причём таблица строится с помощью 255 сложений, на каждом шаге, кроме первого (там всегда ноль), мы используем уже вычисленную маску, отличающуюся от текущей ровно на один бит.

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

Каждый такой байт — готовая маска, которая показывает, входит ли каждая активация в сумму или нет. Для четырёх байт мы четыре раза ходим в таблицу, умножая каждый раз на множитель — ×1, ×2, ×4, ×8. Для «ПикоКалка» это могут быть три умножения (на один умножать не нужно) или три сдвига, не принципиально, то и другое занимает один такт.

В итоге, для восьми элементов в LUT8 у нас нужно будет прочитать четыре маски, четыре раза обратиться к таблице, выполнить три умножения или сдвига и три сложения.

2 комментария

MK-61s и «Спектрум»

С момент как мне подарили «ПикоКалк» хотел поэксперементировать там с цветом. Поскольку у меня теперь основная оболочка для него — прошивка ретроконсоли «МК-61s», то сначала была мысль добавить цвет туда, но я эту идею пока отложил. Вместо этого решил добавить в консоль эмулятор какой-нибудь платформы, где цвет уже имеется.

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

На экране МК-61s показан интерфейс эмулятора ZX Spectrum с игрой, отображающей лабиринт и игровое меню.
Несколько игр из скачанной из интернета подборки, в которые я попробовал поиграть

В качестве основы мы взяли готовый эмулятор, но его пришлось сильно доработать, чтобы достичь хорошей скорости выполнения программ на ядрах «Кортекса-М0+», которые стоят в «ПикоКалке». Сейчас игры просчитываются с той скоростью, с которой они выполнялись бы на настоящем «Спектруме», но с меньшим количеством кадров, в эмуляторе на «ПикоКалке» средняя скорости — 48,8 кадра в секунду.

Это связано со скоростью шины, её пришлось разогнать по максимуму, плюс сделать алгоритм частичной перерисовки экрана, но полную скорость это не дало, впрочем играть довольно комфортно.

Отдельным сюрпризом стало сколько всего модифицировали в «Спектруме» за прошедшие годы — там и игры с расширенной палитрой «ULAPlus», и память до мегабайта, и звуковые процессоры.

На экране показаны скриншоты ZX Spectrum игр: лабиринт, аркада с прыжками через препятствия, платформер с зелеными врагами и счетчик очков.
Игры с расширенной графикой, в детстве о таком я и мечтать не мог

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

Пришлось его разогнать до 200 МГц, это помогло, но выглядело как-то неспортивно. Обычно в таких случаях я читаю спецификацию на «железо», в надежде, что на борту устройства найдётся что-то аппаратное, что поможет победить проблему.

В случае «ПикоКалка» такое устройство нашлось — оказалось, что у него есть аналог южного моста — микроконтроллер «STM32F103C8T6» с процессором «Кортекс-М3», обслуживающий периферию. Он обменивается данными с центральным процессором через довольно медленную шину I²C, но соединён с ним ещё и через UART, где скорость на порядок выше.

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

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

Ещё у меня была идея ускорить таким образом инференс модели для программирования на «МК-61», но оказалось, что скорость шины съедает весь выигрыш. Зато звук удалось синтезировать без задержке даже без разгона центрального процессора.

4 комментария

LUT и SWAR

Ночные эксперименты с нейросеткой на «ПикоКалке» дали, кстати, неплохой результат — теперь на токен тратится чуть больше секунды. Тут есть вклад оборудования, например, разгона центрального процессора до 270 МГц (выше плата зависает), но есть и чисто программные моменты. Я испытал несколько способов перемножить несколько чисел — в вычислениях любой нейросетки такого много.

Быстрее всего оказался обычный INT8 — прямое целочисленное умножение. Веса у меня четырёхбитные, чтобы сетка поместилась в память: в одном байте хранятся два веса. Для расчёта они распаковываются и по отдельности умножаются на восьмибитные активации.

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

Пока с этим возился, познакомился с несколькими интересными методами считать такие суммы, о двух таких алгоритмах хочу рассказать. В моём «ПикоКалке» стоит RP2040 с целочисленным умножителем, но нет FPU — аппаратного блока вычислений с плавающей точкой.

В каждом случае будем считать скалярное произведение двух векторов: веса — 3, 6, 1, 4 и активации — 2, 3, 5, 7.

LUT

Первый называется LUT («Look-Up Table»), я его вчера уже упоминал. Он очень простой для понимания.

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

3 = 1 + 2  6 = 2 + 4  1 = 1  4 = 4

Теперь нам надо получить скалярное произведение вектора весов и вектора активаций:

(1 + 2) × 2 + (2 + 4) × 3 + (1) × 5 + (4) × 7

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

1 × (2 + 5) + 2 × (2 + 3) + 4 × (3 + 7)

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

Чтобы дать наборам адреса, назначаем позициям активаций флажки 1, 2, 4, 8. Адрес набора, или маска, — сумма флажков выбранных позиций. Например, младшие биты весов 3, 6, 1, 4 дают выбор 1, 0, 1, 0: берём первую и третью активации, то есть 2 и 5. Адрес этого набора — 1 + 4 = 5, поэтому LUT[5] = 2 + 5 = 7.

Для разряда ×2 выбираются первая и вторая активации: адрес 1 + 2 = 3, сумма LUT[3] = 2 + 3 = 5. Для разряда ×4 — вторая и четвёртая: адрес 2 + 8 = 10, сумма LUT[10] = 3 + 7 = 10. Нужное скалярное произведение таким образом находится по таблице:

1 × LUT[5] + 2 × LUT[3] + 4 × LUT[10] = 7 + 10 + 40 = 57

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

Максимальная ширина входа слоя у меня — 384 активации. Делим их на четвёрки, для каждой храним 16 сумм. Одна сумма в моём варианте LUT_FLOAT32 занимает четыре байта, получается: (384 / 4) × 16 × 4 = 6144 байта. Столько внутренней SRAM занимает весь набор таблиц LUT.

SWAR

Второй метод называется SWAR («SIMD Within A Register»), где SIMD — «Single Instruction, Multiple Data».

Тут мы считаем два произведения одной командой. Проще всего проиллюстрировать принцип в привычной нам десятичной системе. Пусть нам надо подсчитать следующее скалярное произведение:

3 × 2 + 6 × 3

Для этого мы применяем такой любопытный приём:

(6 × 100 + 3) × (2 × 100 + 3) или 603 × 203 = 122409

Если взять средние две цифры, это будет сумма произведений:

3 × 2 + 6 × 3 = 24

Почему это работает? Станет понятно, если по-другому расставить скобки:

12 × 10000 + (6 × 3 + 3 × 2) × 100 + 9

Остаются вопросы — как мы получаем среднюю часть числа и где тут экономия, ведь в раскрытой формуле куча умножений?

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

В настоящем расчёте вместо основания 100 используется 65536: два числа упаковываются в 32-битное слово с двумя 16-битными полями. Команда умножения сохраняет младшие 32 бита произведения, поэтому член с множителем 65536² выпадает сам собой — для него просто нет места. В нашем примере остаётся 24 × 65536 + 9. Сдвиг вправо на 16 бит убирает младшее поле и оставляет 24.

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

2 комментария

MK-61s: обвязка на PicoCalc

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

Решил реализовать заведомо непрактичную, но давно засевшую у меня в голове идею — запустить внутри ретроконсоли «МК-61s» какую-нибудь нейросеть. Наверное месяц крутил эту мысль в голове, но ничего достойного реализации мне в голову не приходило, пока вчера ночью не осенило — надо сделать обвязку («харнесс») для программирования на языке «Электроника МК-61»!

Для этого я выбрал русскоязычную модель «Flourish-3M», у которой около трёх миллионов параметров и попробовал дообучить её на Nvidia B200 синтаксису «МК-61». 

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

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

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

На видеокарте NVIDIA B200 запущено три процесса Python, использующих почти всю память GPU.
Процесс обучения нейросети; большая часть памяти занята «Квеном», который помогал корректировать обучение

Чтобы ускорить матричные вычисления, пришлось использовать предвычисленные таблицы — LUT и оба ядра процессора (все остальные процессы в этот момент замирают), разогнанные с 125 до 250 МГц.

Несмотря на все усилия, скорость генерации невелика  — около 4,67 секунды на токен. Математического сопроцессора нет, всё считается ядрами центрального процессора.

На экране консоли отображено число 0,374666 — результат вычисления синуса угла 22 градуса программой, написанной ИИ Flourish-3M специально для калькулятора MK-61.
Игру эта сеть не напишет, ей бы с синусом справиться!

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

Я, наверное, потренирую её ещё немного и сотру, даже в репозиторий класть не буду — интересно было повозиться, но никакого смысла такой результат сохранять нет.

11 комментариев

«MK-61s»: ещё приложения

Может показаться, что я занялся другими проектами и совсем забыл про ретроконсоль «МК-61s». Это совершенно не так.

Моё молчание отчасти объясняется тем, что почти все токены я сейчас трачу на попытку сделать нормальную арифметику для «ПикоКалка» — хочется, чтобы сишный тип double вёл себя там, как в стандартном Си и при этом все операции с ним выполнялись быстро, а это непросто.

Тем не менее и другими вещами я тоже занимаюсь, в частности в коллекции «МК-61s» появились ещё три приложения, созданных с разными целями.

На экране калькулятора МК-61 отображаются ячейки таблицы с числовыми значениями, формулами и итоговой суммой.
Приложение для работы с таблицами на экране «ПикоКалка»

Первое — это приложение для работы с таблицами. Попытка ответить на вопрос: «А для чего я мог бы использовать эту ретроконсоль в реальной жизни?». Я участвую в проекте создания этой ретроконсоли для собственного развлечения, но иногда в команде возникает вопрос: а могло бы это пригодиться кому-то для чего-то полезного, помимо ностальгии?

На мой взгляд ответ получился удачным, на «ПикоКалке» приложением даже приятно пользоваться — из-за полноценной клавиатуры и большого экрана.

Формулы записываются в привычной для «МК-61» обратной польской записи. Например, чтобы перемножить количество из A2 и цену из B2, нужно ввести A2 В↑ B2 ×. На картинке два предмета по 125 дают 250. Меняешь количество или цену — зависимые ячейки пересчитываются. Формулу можно скопировать или протянуть вниз, относительные ссылки при этом сдвигаются.

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

Экран консоли МК-61с с интерфейсом игры «Гонка»
Игра «Погоня», написанная с применением расширенных команд калькулятора

Следующая программа — игра «Погоня», написанная на языке калькулятора «МК-61» с расширенными командами.

Я давно хотел понять нельзя ли сделать на калькуляторе динамическую игру хоть с какой-то изометрией. Не скажу, что получилось очень здорово, но играть можно. Управление традиционное для калькулятора — переключение режимов «Г», «ГРД» и «Р».

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

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

Экран консоли МК-61с с интерфейсом игры «Лодраннер»
Игра «Лодраннер», опять-таки написанная с применением расширенных команд калькулятора

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

У «МК-61s» есть скриптовый язык, который называется «M61», у него простой и относительно бедный синтаксис, но его плюс в том, что он как клей может скреплять между собой несколько разных программ. Логику опроса клавиатуры я вывел именно туда — ввёл новую конструкцию, запускающую указанные подпрограммы, при нажатии и отпускании клавиш. Эти подпрограммы меняют содержимое регистров калькулятора, таким образом сообщая программе что нажато на клавиатуре.

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

От преследователя можно уйти, прожигая пол слева или справа. Герой через такую дырку падает на этаж ниже, а враг застревает. Через четыре секунды пол восстанавливается, и пойманный враг возвращается к месту старта. Бесконечно дырявить всё вокруг нельзя: одновременно открыт только один прожиг, лестницы и нижний этаж защищены.

1 комментарий

MK-61s внутри PicoCalc

Клавиатура MK-61s с экраном, отображающим меню и координаты X, Y, Z, T, X1, и надпись "Шаг 0/10 шагов".
Рендер интерфейса «МК-61s» на экране «ПикоКалка», в жизни выглядит так же, но смартфон не фокусируется нормально на экране

На работе на прошедший в сентябре день рождения подарили «ПикоКалк» — набор для сборки кибердеки с «Распберри» внутри. Заказали из Китая заранее, но ехал долго, поэтому вручили только в эту пятницу.

Вчера адаптировал под него интерфейс «МК-61s». У меня давно ощущение, что экран у текущего железа маловат, хотелось развернуться, посмотреть как она может выглядеть с большим экраном и полноразменой клавиатурой, поэтому я загорелся идеей адаптации.

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

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

Сейчас «МК-61s» чувствует себя внутри довольно естественно, только надо решить что делать с играми — шахматами, «Элизой» и другими, они на таком экране смотрятся очень уж мелко. Надо либо как-то научиться их хорошо масштабировать, либо каким-то образом поменять шрифт, ещё не решил.

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

2 комментария