nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Нейросетевой процессор на Воронежской ПЛИС!

Всё я какие-то устаревшие решения пытаюсь применять для этого ВидеоИзмерителя Параметров Сближения. 16-битный одноядерный процессор, ассемблер, связанные списки (обычные, не блокчейн) - никто сейчас так не делает! То, что неделю назад поразвлекался с теорией оптимального обнаружения, БИХ-фильтрами и иже с ними - не сильно лучше, этим методам лет 60 как минимум :)

Пора уже вспомнить, что на дворе 2021 год - и сделать обнаружение точек на основе нейросети!
IMG20210401204530.jpg

Причём взять не какой-то там дряхлый персептрон, а самую нынче модную технологию - свёрточные нейросети (Convolutional Neural Networks, CNN), а если точнее - трёхслойную структуру, где первый слой отвечает за распознавание горизонтальных признаков, второй слой - за вертикальные признаки, а третий слой непосредственно обнаруживает пятна, т.е когда один из нейронов "вспыхивает", это означает: в этом месте у нас пятно, в смысле, мишень!

Если делать это "в лоб" на компьютере, то даже довольно мощные персоналки могут задуматься всерьёз и надолго. Но ПЛИС - совсем другое дело, и такая сеть может быть выполнена на Воронежской ПЛИС 5576ХС4Т, и работать в реальном времени (25 кадров в секунду 1024х720) при тактовой частоте 25 МГц :)


Первое серьёзное упрощение - все математические операции в нейросети не требуют запредельной точности, наподобие 32-битных или 64-битных Float (или того хуже, 80-битная Extended, как в сопроцессорах x86) - хватит 8, в крайнем случае 16 бит, обычных целочисленных значений. Главное, чтобы вычисления были "насыщаемые" - переполнения, превращающие 0xFF в тыкву 0x00, нам не нужны!

Что интересно, веса можно задавать с ещё меньшей точностью, и наверняка получить то, что надо. Вон, та же nvidia для своих "тензорных вычислений" поддерживает форматы данных INT8 и даже INT4 (!) Ибо нефиг.

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

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

Осуществить такую длинную свёртку "в лоб" - очень неприятная задача, это нужно делать 64 сложения и 64 умножения за один такт. И это хорошо ещё, что мы додумались разделить свёртку на горизонтальную и вертикальную составляющие, иначе было бы 4096 умножения и сложения за такт - тут никакой распоследний Циклон или Zynq нам не поможет. 64 сложения и 64 умножения на них - в принципе задача решаемая, там количество аппаратных умножителей исчисляется десятками-сотнями. Главное, не завалить тайминги, впихнув все операции "комбинаторно" одну за другой, а поставить регистры время от времени. После этого обработка "от начала до конца", разумеется, займёт куда больше 1 такта, но отдельные части нашего арифметического комбайна будут работать "в конвейере", каждая готовая уже на следующем такте получить новые данные, обработать их и переслать дальше.

Но 5576ХС4Т - довольно старенькая ПЛИС, в ней НЕТ аппаратных умножителей. У серии 5578 уже есть, а конкретно 5578ТС104 - вообще монстр. Но мне их напряжения не нравятся, 1,2 вольта ядро, 2,5 вольта периферия, не стыкуется с 1,8 и 3,3 вольтами фотоприёмной матрицы и не получается "раскачать" приёмопередатчик МКО.

Другой давно известный способ существенно снизить вычислительные затраты на свёртку - это быстрое преобразование Фурье. Вместо того, чтобы в каждой точке производить эти десятки умножений и сложений, достаточно будет ОДНОГО умножения, по сути умножить спектр сигнала на коэффициент передачи фильтра. Само же преобразование Фурье по каждой оси требует около Nlog2N операций, что для N=1024 - около 10 000. То есть, когда ядро для свёртки имеет ширину более 20, делать её через преобразование Фурье уже имеет смысл (нам нужно сделать два преобразования Фурье, прямое и обратное, поэтому 2Nlog2N операций, а для свёртки "в лоб" NM, где M - размер ядра).

Я в своё время придумывал Уравновешенное Троичное Быстрое Преобразование Фурье, хотелось бы стряхнуть с него пыль и применить с пользой. Но для выполнения преобразования Фурье нам нужно запомнить всё изображение целиком, и иметь произвольный доступ к его пикселям. И тут у нас "бутылочным горлышком" мгновенно станет подключаемая статическая память - она сможет на каждом такте делать только одну операцию чтения или записи, ну а "внутрь ПЛИС" целое изображение не влезет. Не в эту ПЛИС, по крайней мере. В 5576ХС4Т имеем 12 килобайт, в третьем Циклоне, который у меня есть - порядка 75 килобайт, а нужен мегабайт как минимум...

Но вспомним, что для корректной работы нейросетей зачастую хватает ОЧЕНЬ ГРУБО ЗАДАННЫХ ВЕСОВ. А если так, то можно попытать счастья в таких ядрах свёртки, которые достаточно хорошо повторяются рекурсивными фильтрами, они же фильтры с бесконечной импульсной характеристикой.

Изыскания показали, что удовлетворительные результаты даёт рекурсивный фильтр первого порядка вида

y[k] = y[k-1] + (x[k-1] - y[k-1]) / 26

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

Его исполнение для фильтрации по горизонтали наиболее простое:

module neuro_IIR_LPF (input clk, input [7:0] D, output [7:0] Q);

parameter level = 2;

reg [7 + level : 0] Qfull = 1'b0;

wire [7:0] subRes;
wire subCout;

lpm_add_sub Sub (	.dataa (D),
			.datab (Qfull[7 + level : level]),
			.result(subRes),
			.cout (subCout));
defparam
	Sub.lpm_direction = "SUB",
	Sub.lpm_hint = "ONE_INPUT_IS_CONSTANT=NO,CIN_USED=NO",
	Sub.lpm_representation = "UNSIGNED",
	Sub.lpm_type = "LPM_ADD_SUB",
	Sub.lpm_width = 8;
	
reg [7:0] rSubRes = 1'b0;
reg rSubCout = 1'b0;

always @(posedge clk) begin
	rSubRes <= subRes;
	rSubCout <= ~subCout;
	Qfull <= Qfull + {{level{rSubCout}}, rSubRes};
end

assign Q = Qfull[7 + level : level];

endmodule


Результат применения первого слоя нейронов "в железе" изображён на рисунке:
LPF_H_6_fixed.png

Для реализации второго слоя нейронов, работающих по вертикали, требуется блок внутренней памяти размером 1024 байт. Концептуально всё просто: мы запоминаем выходное значение для каждого столбца изображения, и обновляем их одно за другим по мере поступления пикселей. Но реализация представляет некоторые трудности, связанные с поведением двухпортовой памяти, когда используется один и тот же адрес одновременно для чтения и записи. Возможна задержка выдачи результаты для записи назад в память, и нужно правильно организовать работу двух счётчиков, отсчитывающих соответствующие адреса. Все подробности расскажем отдельным постом, а пока просто покажем результаты работы двух слоёв нейронов:
blurred.png

Наконец, каждый нейрон из третьего слоя берёт 9 значений с предыдущего слоя, как бы "центральный пиксель" и 8 его соседей. Этот нейрон "зажигается", если центральный пиксель оказался строго ярче всех своих соседей. Покажем, где именно были обнаружены точки:
blurred.png

Неплохой результат!
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Ковыряемся с сантехникой

    Наконец-то закрыл сколько-нибудь пристойно трубы, подводящие к смесителю, в квартире в Москве: А в воскресенье побывал на даче, там очередная…

  • Мартовское велосипедное

    Продолжаю кататься на работу и с работы на велосипеде, а также в РКК Энергию и на дачу. Хотя на две недели случился перерыв, очередная поломка,…

  • Обнаружение на новом GPU - первые 16 мс

    Закончилась симуляция. UFLO и OFLO ни разу не возникли, что не может не радовать. За это время мы дошли до строки 0x10F = 271. Поглядим дамп памяти:…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 5 comments