Чтобы сделать ускорение ППВМ, или программируемой пользователем вентильной матрицы, и графического процессора одинаково удобным для разработчиков, в 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 «из коробки», знать эту терминологию необязательно, тем не менее важно понимать упрощаемые здесь протоколы и интерфейсы.
Что такое ППВМ?
ППВМ, или программируемые пользователем вентильные матрицы, — это полупроводники. В отличие, например, от интегральных схем специального назначения, с такими ППВМ «с чистого листа» разработчик максимально близок к созданию специализированного оборудования — фактически ничего производить не требуется. ППВМ «из коробки» вообще ничем не заняты, но программируются на выполнение чего угодно. У них также практически нет состояний: при выключении они сбрасываются.
В ППВМ, как правило, имеется три основных компонента:
- логические ячейки;
- межсоединения;
- блоки ввода-вывода.
Если вкратце, логические ячейки — это конечные автоматы для определения программы. Они строятся из таблиц истинности (своего рода оперативной памяти для функций комбинаторной логики), триггеров для хранения информации о состоянии и мультиплексоров для направления логики между элементами блока и внешними ресурсами.
Логические ячейки соединяются межсоединениями, которыми направляются данные между ними, а с помощью ячеек ввода-вывода в ППВМ считываются/записываются данные извне через различные интерфейсы. Все эти компоненты программируются языками описания аппаратуры, такими как 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 — это ОС ППВМ для разработки всех ускорителей.
Вот ее функционал.
- Брандмауэр — защита ППВМ от пользовательского ввода, чреватого переходом ППВМ в неопределенное состояние и жесткой перезагрузкой. С ним удобнее работать, экономится много времени разработки.
- Конфигурация времени выполнения — ППВМ способны загружать программы во время выполнения с возможностью переключения функциональности. Программы компилируются в бинарный код, загружаются в ППВМ.
Прошивка утилитой 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(¶m.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.
Читайте также:
- Как создать API-шлюз в Rust посредством библиотеки Hyper
- Фича-флаги времени компиляции в Rust: зачем, как и когда используются
- Асинхронный Rust: проблемы и способы их решения
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ingonyama: Introducing Blaze: ZK Acceleration for FPGA