Чтобы сделать ускорение ППВМ, или программируемой пользователем вентильной матрицы, и графического процессора одинаково удобным для разработчиков, в Ingonyama разрабатывают решения-ускорители ППВМ для типовых примитивов ZK.

С Blaze  —  библиотекой Rust для ускорения ZK на основе ППВМ компании Xilinx  —  ускорение ППВМ становится доступным для разработчиков ZK.

Что такое Blaze?

Это библиотека Rust с доступом к реализации Ingonyama примитивов MSM, NTT и Poseidon hash на ППВМ без лишних проблем и накладных расходов. С Blaze устраняются сложности чтения/записи/прошивки ППВМ.

Вы можете применять Blaze и с собственными программами ППВМ, определив свою конфигурацию и реализовав клиент.

Сейчас поддерживается карта ППВМ C1100/U55C от Xilinx.

Для кого Blaze?

Для всех, кому в приложении требуются высокопроизводительные примитивы ZK.

Blaze предназначена для разработчиков протоколов и доказательств с нулевым разглашением, для обновления с помощью ускорения ППВМ имеющихся протоколов с целью повышения производительности.

На решение какой проблемы нацелена Blaze?

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

С Blaze же команда разработчиков ПО просто получает конфигурационный файл от команды аппаратных средств и взаимодействует с высокоуровневым API.

Архитектура Blaze

Blaze  —  это библиотека для взаимодействия с ППВМ, представленными на схеме как DriverClient, в которой реализуются низкоуровневые методы взаимодействия с XDMA и другими частями ППВМ. Благодаря Blaze разработчики определяют API-интерфейсы ППВМ в виде объектов JSON, из которых генерируются клиенты для реализации простых высокоуровневых драйверов.

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

Начало работы

Прежде чем переходить к API, рассмотрим базовые понятия и терминологию ППВМ. Чтобы применять Blaze «из коробки», знать эту терминологию необязательно, тем не менее важно понимать упрощаемые здесь протоколы и интерфейсы.

Что такое ППВМ?

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

Высокоуровневый обзор ППВМ

В ППВМ, как правило, имеется три основных компонента:

  1. логические ячейки;
  2. межсоединения;
  3. блоки ввода-вывода.

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

Логические ячейки соединяются межсоединениями, которыми направляются данные между ними, а с помощью ячеек ввода-вывода в ППВМ считываются/записываются данные извне через различные интерфейсы. Все эти компоненты программируются языками описания аппаратуры, такими как Verilog.

Закрепим высокоуровневое понимание, рассмотрев основные применяемые в среде ППВМ компоненты.

IP Cores

IP-ядра  —  это «модули», разработанные и протестированные сторонними компаниями, например AMD, Xilinx или другим участником команды. Они выпускаются в программном и аппаратном виде с тщательно протестированными решениями для типичных задач проектировщиков ППВМ.

AXI

AXI, или Advanced eXtensible Interface,  —  это протокол, применяемый главным образом в ППВМ, интегральных схемах специального назначения и системах на кристалле для обмена данными между различными компонентами: процессорными ядрами, контроллерами памяти, периферийными устройствами. Как часть разработанного компанией ARM семейства протоколов AMBA, то есть прогрессивной архитектуры шины микроконтроллера, AXI используется в ППВМ для эффективного взаимодействия внутренних компонентов.

HBM

HBM, или High Bandwidth Memory,  —  высокопроизводительная технология памяти, тесно интегрированная с ППВМ для обеспечения высокой пропускной способности памяти и низкого энергопотребления, особенно полезна в приложениях, где требуется быстрая обработка больших объемов данных, например MSM.

HBM  —  реальный физический компонент ППВМ, в котором применяется архитектура с трехмерной стековой компоновкой памяти для бо́льшей производительности, чем у традиционной оперативной памяти с удвоенной скоростью передачи данных DDR RAM. В контроллере памяти HBM  —  обычно это физическое IP-ядро  —  для взаимодействия с другими компонентами ППВМ используется AXI.

DMA

DMA, или прямым доступом к памяти, обеспечивается передача данных между внешними устройствами и ППВМ без участия процессора, а также упрощается передача внутренних данных между компонентами ППВМ.

Для высокопроизводительной передачи данных между ППВМ и хост-системой по интерфейсу PCIe, то есть Peripheral Component Interconnect Express, в Xilinx специально разработали IP-ядро XDMA, или Xillinx DMA.

HBI

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

Представляем Blaze

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

С Blaze упрощается и определение разработчиками клиентов для конкретных приложений или использование клиентов Ingonyama, которые легко могут задействовать возможности ППВМ-решений Ingonyama для многих важнейших примитивов ZK.

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

Настройка проекта

Какие платформы поддерживаются?

Разработка и тестирование проводятся в основном на установленной локально C1100/U55C от Xilinx.

Текущим выпуском не поддерживаются экземпляры AWS F1, предстоящим  —  будут.

Прошивка устройства

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

Подробности  —  в репозитории.

Warpshell

Warpshell  —  это ОС ППВМ для разработки всех ускорителей.

Вот ее функционал.

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

Прошивка утилитой xbflash2

Подключаем карту BDF, то есть Bus:Device.Function, и запускаем:

sudo lspci -d 10ee:

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

01:00.0 Processing accelerators: Xilinx Corporation Device 5058
01:00.1 Processing accelerators: Xilinx Corporation Device 5059

Обратите внимание на первую функцию каждого устройства в нотации Bus:Device.Function, здесь это 01:00.0.

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

Прошиваем устройство C1100/U55C в образ warpshell:

sudo ./Ingonyama_utils/xbflash2 program --spi --image 
./Ingonyama_utils/warpshell_xilinx_u55n_xdma_gen3x8_v2.mcs
--bar-offset 0x1F06000 -d <BDF>

И видим:

Preparing to program flash on device: 01:00.0
Are you sure you wish to proceed? [Y/n]: y
Successfully opened /dev/xfpga/flash.m256.0
flashing via QSPI driver
Bitstream guard installed on flash @0x1002000

Extracting bitstream from MCS data:
..................
Extracted 18464340 bytes from bitstream @0x1002000
Writing bitstream to flash 0:
..................
Bitstream guard removed from flash
****************************************************
Cold reboot machine to load the new image on device.
****************************************************

Перезагружаемся и оказываемся в экосистеме warpshell.

После перезагрузки снова проверяем lspci -d 10ee: и видим:

01:00.0 Processing accelerators: Xilinx Corporation Device 9038

Использование Blaze

Добавление в имеющийся проект на Rust

С cargo это очень просто:

cargo add ingo-blaze --git
"https://github.com/ingonyama-zk/blaze.git"

Вот что добавляется в Cargo.toml:

ingo-blaze = { git = "https://github.com/ingonyama-zk/blaze.git"}

Установив Blaze, задействуем его.

Создание собственных драйверов

Переходим к API Blaze. Как пример рассмотрим исходный код PoseidonClient для ускорения протоколов ZK, в которых применяется Poseidon. Ознакомьтесь с процессом Filecoins PC2. В PoseidonClient принимаются входные значения размером 256 бит каждое, а возвращается дерево согласно параметрам инициализации, размер входных данных которого  —  до 374 Гб.

Дочитав статью, вы освоите наши драйверы и сможете создавать собственные.

Определение DriverClient

Сделаем первый DriverClient, экземпляр ППВМ со всеми методами для чтения/записи из ППВМ.

В примере ниже создается преднастроенный DriverClient, то есть DriverConfig и драйвер для первого использования, с применением DriverConfig::driver_client_c1100_cfg() от Ingonyama:

ingo_blaze::driver_client::dclient::{DriverClient, DriverConfig};

let dclient = DriverClient::new(
"0", DriverConfig::driver_client_c1100_cfg());

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

DriverConfig

Конфигурация драйвера  —  это объект JSON, адресное пространство памяти для различных компонентов ППВМ.

Обычно при использовании драйверов Ingonyama просто вызывается DriverConfig::driver_client_c1100_cfg().

Если же проектировать собственное оборудование, потребуется своя конфигурация драйвера. Создать пользовательский DriveConfig несложно, сначала определяем <config_file_name>.json и сохраняем здесь:

{
...
"ctrl_baseaddr": "0x00000000",
"ctrl_cms_baseaddr": "0x04000000",
"ctrl_qspi_baseaddr": "0x04040000",
...
}

Затем в DriverConfig добавляем метод для парсинга параметров и генерирования объекта, создаем драйвер:

let dclient = DriverClient::new(
"0", DriverConfig::driver_client_custom_cfg());

Определение примитива

Задействуем созданный экземпляр DriverClient.

Примитив  —  это обертка DriverClient, которой реализуется API для драйвера.

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

Возьмем драйвер Poseidon hash:

// PoseidonClient — это примитив
let poseidon = PoseidonClient::new(Hash::Poseidon, dclient)

В PoseidonClient принимается определенный выше DriverClient, это dclient, реализуется вся пользовательская логика Poseidon hash, а для взаимодействия с ППВМ используется DriverClient. Пример  —  функция reset в PoseidonClient:

fn reset(&self) -> Result<()> {
self.dclient.set_dfx_decoupling(1)?;
self.dclient.set_dfx_decoupling(0)?;
sleep(Duration::from_millis(100));
Ok(())
}

В ней из DriverClient вызывается set_dfx_decoupling.

Загрузка программ и инициализация

В каждой версии драйвера имеется файл .bin  —  программа, запускаемая на ППВМ.

До применения драйвера указываем ППВМ запускаемую программу, загружаем этот файл .bin.

Вот пример загрузки программы:

// считываем файл «.bin» из пути
let buf = read_binary_file(&bin_file)?;
// устанавливаем ППВМ в корректное состояние для загрузки двоичного файла
poseidon.driver_client.setup_before_load_binary()?;
// загружаем двоичный файл
let ret = poseidon.driver_client.load_binary(buf.as_slice());

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

Загрузив программу, инициализируем ППВМ. У разных драйверов свои особенности этого этапа. Так, драйвер PoseidonDriver отличается от load_binary загрузкой CSV-файла, в котором содержатся все инструкции программы, и настройкой ППВМ на генерирование конкретного типа дерева:

let params = PoseidonInitializeParameters { 
tree_height, // сколько ярусов в дереве
tree_mode: TreeMode::TreeC, // тип дерева
instruction_path, // путь к CSV-файлу
};

poseidon.initialize(params);

Заглянем в метод initialize примитива PoseidonClient:

fn initialize(&self, param: PoseidonInitializeParameters) -> Result<()> { 
self.reset()?; // сбрасываем брандмауэр и прочее

self.set_initialize_mode(true)?; // входим в режим инициализации

// загружаем инструкции из csv
self.load_instructions(&param.instruction_path)
.map_err(|_| DriverClientError::LoadFailed {
path: param.instruction_path,
})?;

// выходим из режима инициализации
self.set_initialize_mode(false)?;

// задаем важные параметры генерируемого // дерева
self.set_merkle_tree_height(param.tree_height)?;
self.set_tree_start_layer_for_tree(param.tree_mode)?;
// инициализируем подсистему управления картами,
// например для отслеживания температуры карты
self.dclient.initialize_cms()?;

self.dclient.set_dma_firewall_prescale(0xFFFF)?; Ok(())
}

Переходим к Poseidon hash.

Чтение/запись в ППВМ

В PoseidonClient реализуются методы записи:

fn set_data(&self, input: &[u8]) -> Result<()> { 
self.dclient .dma_write(self.dclient.cfg.dma_baseaddr, DMA_RW::OFFSET, input)?;
Ok(())
}

и чтения:

self.dclient.dma_read_into(
self.dclient.cfg.dma_baseaddr,
DMA_RW::OFFSET,
result_buffer // указатель буфера для считывания данных
);

В PoseidonClient единовременно считывается или записывается до 1 Тбайт. Однако считывание таких объемов данных не всегда оптимально или необходимо в приложении, размеры для чтения и записи определяются особенностями проектирования драйвера.

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

Способ чтения/записи ППВМ определяется множеством деталей, таких как размеры данных, временна́я задержка, доступная на компьютере оперативная память и т. д.

Пример потока, которым данные порциями записываются в ППВМ:

// Второй поток: получаем и обрабатываем указатели буфера 
while let Ok(buffer_ptr) = rx.recv() {
poseidon.set_data(buffer_ptr.lock().unwrap().get_mut
().as_ mut_slice());
}
});

Отладка

Чтобы разработчики получали сведения о состоянии ППВМ, мы внедрили инструменты отладки.

Логирование

Каждым драйвером реализуется метод log_api_values для фиксации всех текущих состояний определенных для драйвера адресов:

pub fn log_api_values(&self) {
log::debug!("=== api values ===");
for api_val in INGO_POSEIDON_ADDR::iter() {
self.dclient
.ctrl_read_u32(self.dclient.cfg.ctrl_baseaddr, api_val)
.unwrap();
}
log::debug!("=== api values ===");
}

Вот вывод драйвера Poseidon:

ADDR_HIF2CPU_C_IMAGE_ID value: 1
ADDR_HIF2CPU_C_IMAGE_PARAMTERS value: 64
ADDR_CPU2HIF_C_MERKLE_TREE_HEIGHT value: 4
ADDR_CPU2HIF_C_MERKLE_TREE_START_LAYER value: 0
ADDR_CPU2HIF_C_INITIALIZATION_MODE value: 0
ADDR_HIF2CPU_C_NOF_ELEMENTS_PENDING_ON_DMA_FIFO value: 720
ADDR_HIF2CPU_C_NOF_RESULTS_PENDING_ON_DMA_FIFO value: 502
ADDR_HIF2CPU_C_MAX_RECORDED_PENDING_RESULTS value: 502
ADDR_HIF2CPU_C_NOF_CLOCKS_SPENT_ON_CURRENT_TASK_LO value: 7766236
ADDR_HIF2CPU_C_NOF_CLOCKS_SPENT_ON_CURRENT_TASK_HI value: 0
ADDR_HIF2CPU_C_LAST_HASH_ID_SENT_TO_RING value: 446
ADDR_HIF2CPU_C_LAST_ELEMENT_ID_SENT_TO_RING value: 1
ADDR_HIF2CPU_C_LAST_HASH_ID_SENT_TO_HOST value: 440
ADDR_HIF2CPU_C_LAST_LAYER_IDX_SENT_TO_HOST value: 0
ADDR_HIF2CPU_C_RING_NODE_ALMOST_FULL value: 0
ADDR_HIF2CPU_C_NOF_CLOCKS_PASSED_FROM_LAST_RING_TRANSMIT_LO value: 1020417
ADDR_HIF2CPU_C_NOF_CLOCKS_PASSED_FROM_LAST_RING_TRANSMIT_HI value: 0
ADDR_HIF2CPU_C_PROGRAM_MEMORY_INITIALIZATION_COUNTER value: 8224

С помощью ADDR_HIF2CPU_C_PROGRAM_MEMORY_INITIALIZATION_COUNTER, например, проверяется корректность инициализации программы:

ADDR_HIF2CPU_C_NOF_ELEMENTS_PENDING_ON_DMA_FIFO value: 720
ADDR_HIF2CPU_C_NOF_RESULTS_PENDING_ON_DMA_FIFO value: 502
ADDR_HIF2CPU_C_MAX_RECORDED_PENDING_RESULTS value: 502

По этим значениям оценивается корректность записи данных в DMA и их обработки.

Контроль температуры и энергопотребления

С помощью CMS контролируется температура карты:

let (temp_inst, temp_avg, temp_max) =
poseidon_temp.dclient.monitor_temperature().unwrap();

Не забываем включить CMS во время инициализации: self.dclient.initialize_cms()?;.

Заключение

Blaze нацелен на интегрирование ППВМ в самые разные проекты ZK.

Ссылка на Github проекта.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Ingonyama: Introducing Blaze: ZK Acceleration for FPGA

Предыдущая статьяПереход на PgCat — прокси-сервер Postgres следующего поколения
Следующая статьяКак использовать GPT-3 для поиска и рекомендаций текстового контента