Тераграф Cloud. Архитектура программного и аппаратного обеспечения

Московский государственный технический университет им. Н.Э.Баумана, 16 октября - 10 декабря 2023

Организаторы

Алексей Попов,
МГТУ им. Н.Э.Баумана
Станислав Ибрагимов
МГТУ им. Н.Э.Баумана
Егор Дубровин
МГТУ им. Н.Э.Баумана
Ли Цзяцзянь
МГТУ им. Н.Э.Баумана
Максим Калитвенцев,
МГТУ им. Н.Э.Баумана
Михаил Гейне
МГТУ им. Н.Э.Баумана
Гор Парамазян
МГТУ им. Н.Э.Баумана
Петр Шумнов
МГТУ им. Н.Э.Баумана
Тимофей Курохтин
МГТУ им. Н.Э.Баумана
Анастасия Попова
МГК им. П.И.Чайковского
Алекандра Попова
Высшая школа музыки
г.Детмольд; RWTH,
г.Аахен,Германия
Константин Горбунов
НИИ Системной биологии
и медицины
Роспотребнадзора

Аннотация

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

В ходе практикума студенты знакомяться с архитектурой и принципами работы вычислительного комплекса Тераграф, выполняют практические задания по программированию гетерогенных ядер обработки графов, знакомятся с библиотеками для обработки и визуализации графов. Доступ к вычислительному комплексу осуществляется с использование облачной платформы Тераграф Cloud, обеспечивающей одновременный доступ многих пользователей к гетерогенным ядрам обработки, входящим в состав микропроцессора Леонард Эйлер. В разделах 2 и 3 приводится структура вычислительного комплекса Тераграф, микропроцессора Леонард Эйлер и структура гетерогенного ядра обработки графов, а также особенности их программирования. Приводятся примеры инициализации графов в памяти ядра обработки графов, алгоритмов обработки и создания структур для визуализации. Практикум завершается командным проектом генерации музыкальных композиций, построенных на основе обхода графа знаний (графа ДеБрюйна).

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

Практикум состоит из трех этапов:

  • Исследование принципов функционирования вычислительного комплекса Тераграф

  • Практикум по программированию гетерогенного вычислительного узла

  • Командный практикум по генерации музыкальных композиций на основе графов знаний

Вступительная презентация



1. Графы знаний

1.1. Актуальность создания эффективных программных и аппаратных средств обработки графов

Граф G(V,E) – множество вершин V, на элементах которого определены двуместные отношения смежности (ребра) – (vi, vj) E, где vi, vj V (обратите внимание на наличие скобок в первом выражении и их отсутствие во втором). Тогда пара вершин, находящихся в отношении смежности, рассматривается как ребро ek = (vi, vj), ek E. Вершина vi смежна вершине vj тогда и только тогда, когда существует ребро ek, инцидентное vi, такое, что vj инцидентно ему. Аналогично ребру ek смежно ребро el тогда и только тогда, когда существует вершина vi, инцидентная ребру ek, такая, что el инцидентно этой вершине.

Существует несколько видов графов, отличающихся свойствами предикатов инцидентности – неориентированные, ориентированные, гипер- и ультраграфы, метаграфы.

Рисунок 1 — Графы знаний

Рисунок 1 — Графы знаний

Графы знаний являются способом представления модели знаний в виде графовой структуры. Технологии представления и обработки знаний в виде графов приобрели большое значение во многих областях, в которых другие методы показали низкую эффективность. Благодаря способности сохранять информацию о различных объектах и явлениях и учитывать связи между ними, графы знаний могут использоваться при анализе больших данных в биоинформатике [1], в персонифицированной медицине, системах безопасности городов [2][3][4][5][6], в компьютерных сетях, финансовом секторе, при контроле сложного промышленного производства, для анализа информации социальных сетей и во многих других областях.

Существенное влияние на эффективность применения аппаратных средств в задачах обработки графов оказывает адекватность их применения в рамках парадигмы рассматриваемых вычислений. Так, ряд задач обработки графов основан на статических графах, изменение которых либо не предусматривается вообще, либо происходит за пределами графового вычислителя. Для такого класса задач обработки графов характерным является этап передачи графа из исходного места хранения в оперативное хранилище графового вычислителя или же потоковая обработка. Подобная обработка позволяет применять классические варианты построения вычислительных систем, в которых передача данных происходит большими или непрерывными пакетами, а останов ритмичной обработки не предусматривается спецификой алгоритмов. Для решения данного класса задач хорошо зарекомендовали себя графические ускорители GPU [7] и матрично-конвейерные структуры на ПЛИС [8]. Второй вариант постановки задач обработки графов отличается тем, что информация графа должна меняться как под воздействием внутренней обработки (например, результата поиска кратчайшего пути или центральных вершин), так и под воздействием внешних факторов (запросов на изменение информации графов). В этом случае граф должен находится непосредственно в оперативной памяти (памяти процессора общего назначения или специального устройства обработки графов). Такой вариант предполагает непрерывность процессов обработки и изменения, что приводит к необходимости применения иных архитектурных принципов. Вычислительные средства, эффективно воплощающие подобную функциональность, опираются на оптимизацию алгоритмов доступа к структурам данных и графам в памяти, на повышение эффективности подсистемы памяти, на увеличение степени параллельности при обработке каждой нити вычислений.

Приведенные выше различия статических и динамических задач обработки графов приводят к тому, что несмотря на большое количество и разнообразие средств вычислительной техники, потребность в высокопроизводительных ЭВМ для решения задач обработки графов знаний, чрезвычайно высока. При решении подобных задач дальнейшее увеличение скорости обработки на основе универсальных микропроцессорных систем трудно достижимо. Даже благодаря высокому уровню параллелизма, глубокой конвейеризации и большим тактовым частотам современные микропроцессоры и графические ускорители не способны эффективно решать проблемы обработки больших графов. Сказываются такие фундаментальные проблемы, как: зависимости по данным [9][10]; необходимость распределения вычислительной нагрузки при обработке нерегулярных графов; наличия конфликтов при доступе к памяти большого количества обрабатывающих ядер [11].

В МГТУ им. Н.Э.Баумана в настоящее время создается вычислительный комплекс, предназначенный для обработки графов и обладающий передовыми техническими характеристиками: аппаратная реализация набора команд дискретной математики, гетерогенная архитектура, хранение и обработка до 1 триллиона вершин графа. В ходе практикума все участникам будет предоставлен доступ к одной карте комплекса Тераграф.

1.2. Применения графов в задачах аналитики данных и искусственном интеллекте

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

Рисунок 2 — Аналитическая система на основе графов знаний

Рисунок 2 — Аналитическая система на основе графов знаний

Уже сейчас оказывается недостаточным просто хранить огромные массивы фактов и извлекать их по запросу. Необходимо иметь систему, способную анализировать причинно-следственные связи между событиями, оценивать достоверность и полноту сведений, выявлять и хранить контекстную информацию. Именно графы позволяют получать ответы на те вопросы, которые интересуют пользователя такой системы. Например, насколько вероятно развитие дорожной обстановки по неблагоприятному сценарию и как его избежать, имеют ли место незаконные финансовые операции, кто в них задействован и какие схожие сценарии возможны? При этом подход к формированию ответов на такие вопросы должен принципиально отличаться от простого поиска ситуаций похожих на те, что система видела раньше (как это делается сейчас в нейронных сетях). Аналитическая система будущего должна не просто искать сходства и различия, но уметь логически рассуждать на основе графов знаний. В этом смысле, все известные системе факты и правила будут использоваться для принятия решения. Использование логического вывода на основе графов способно избавить аналитическую систему от ошибок, связанных с игнорированием контекста и здравого смысла, и, главное, делает результат объяснимым.

Рисунок 3 — Примеры графов знаний

Рисунок 3 — Примеры графов знаний

Важность повышения эффективности алгоритмов на графах и совершенствования вычислительных средств для их реализации привели к появлению такого направления как Graph Data Science. Это подразумевает выделение в отельную научную область интересов всего, что связано с аналитикой данных с использованием графов. В набор средств анализа входят такие алгоритмы, как: обнаружение сообществ в графах, центральность, поиск подобия и изоморфизм, поиска кратчайших путей и максимальных потоков, и ряд других. Приведем некоторые примеры применения графовых алгоритмов для решения важных практических задач.

Обнаружение незаконных финансовых операций

Мошенники делят грязные деньги на множество малых частей и смешивают их с законными средствами, и затем превращают их в легальные активы. Для этого используется круговое движение денежных средств, которое скрывает первоначальный источник за длинной цепочкой транзакций. Графы позволяют построить модель движения денег для таких мошеннических схем и своевременно препятствовать им.

Обнаружение финансового мошенничества в реальном времени

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

Контроль мошеннической деятельности в налоговой сфере

Системы налогообложения и выявление неуплаты налогов должны постоянно совершенствоваться, чтобы учитывать новые способы ухода от уплаты налогов и мошеннические схемы. С повышением доступности предпринимательства и автоматизацией бизнес-деятельности у преступников появились дополнительные возможности создания подставных юридических лиц, через которых передаются незаконно полученные денежные средства. Графы позволяют анализировать сложные схемы использования подставных юридических лиц и обнаружить мошенническую схему по структуре и взаимосвязи субъектов, участвующих в бизнес-деятельности. Подозрительные паттерны быстро обнаруживаются и выявляются структурные закономерности, которые позволяют установить единый центр мошеннической деятельности.

Промышленное производство и контроль жизненного цикла оборудования

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

Персонифицированная медицина

Графы хорошо подходят для хранения и визуализации медицинской информации. Данные о состоянии организма пациента являются взаимосвязанными, и могут быть соотнесены с аналогичными данными других пациентов. Компания AstraZeneca провела успешные исследования, в которых граф знаний об организме одного члена “сообщества” использовался при выборе терапии для больного по аналогии с другими подобными случаями.

Биомедицинские исследования

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


2. Структура микропроцессора Леонард Эйлер и вычислительного комплекса Тераграф

Анализ графов существенно отличается от привычной арифметико-логической обработки. Самыми существенными особенностями алгоритмов обработки графов являются:

  • зависимости по данным между последовательными итерациями поиска и анализа информации

  • большее количество операций доступа к памяти по сравнению с количеством арифметико-логических операций.

Поэтому в МГТУ им. Н.Э.Баумана была разработана гетерогенная архитектура вычислительного комплекса Тераграф, учитывающая особенности обработки графов. Отличительными чертами комплекса являются:

  1. Доступ к графам и их обработку осуществляет специализированный микропроцессор lnh64 с набором команд дискретной математики (Discrete mathematics instruction set computer, DISC).

  2. Оперативное хранилище графов (так называемая Локальная память структур, Local Structure Memory, LSM) имеет большой размер (2.5 ГБ на один микропроцессор lnh64) и организована как ассоциативная память.

  3. DISC микропроцессор lnh64 подключен непосредственно к шине памяти малого арифметического процессора riscv64im. Пара процессоров lnh64 и riscv64im и составляет гетерогенное ядро обработки графов (Graph Processing Core, GPC).

  4. Множество гетерогенных ядер обработки графов GPC составляют многоядерный микропроцессор Леонард Эйлер (также обозначается как Structure Processing Unit, SPU).

Рассмотрим структуру комплекса Тераграф более подробно.

2.1. Набор команд дискретной математики

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

Таблица 1 – Соответствие инструкций DISC функциям, кванторам и операциям дискретной математики

Функции, кванторы и операции дискретной математики Инструкции набора команд DISC
Функция хранения кортежа INS
Функция отношения элементов множества NEXT,PREV,NSM,NGR,MIN,MAX
Мощность множества CNT
Функция принадлежности элемента множеству SRCH
Добавление элемента в множество INS
Исключение элемента из множества DEL,DELS
Исключение подмножества из кортежа DELS
Включение подмножества в кортеж INS,LS,GR,LSEQ,GREQ,GRLS
Отношение эквивалентности множеств INS,LS,GR,LSEQ,GREQ,GRLS
Объединение множеств OR
Пересечение множеств AND
Разность множеств NOT

Последняя версия набора команд DISC состоит из 21 высокоуровневой инструкции, перечисленных в таблице 1:

  • Search (SRCH) выполняет поиск значения, связанного с ключом.

  • Insert (INS) вставляет пару ключ-значение в структуру. SPU обновляет значение, если указанный ключ уже находится в структуре.

  • Операция Delete (DEL) выполняет поиск указанного ключа и удаляет его из структуры данных.

  • Последняя версия набора команд была расширена двумя новыми инструкциями (NSM и NGR) для обеспечения требований некоторых алгоритмов. Команды NSM/NGR выполняют поиск соседнего ключа, который меньше (или больше) заданного и возвращает его значение. Операции могут быть использованы для эвристических вычислений, где интерполяция данных используется вместо точных вычислений (например, кластеризация или агрегация).

  • Maximum /minimum (MAX, MIN) ищут первый или последний ключи в структуре данных.

  • Операция Cardinality (CNT) определяет количество ключей, хранящихся в структуре.

  • Команды AND, OR, NOT выполняют объединения, пересечения и дополнения в двух структурах данных.

  • Срезы (LS, GR, LSEQ, GREQ, GRLS) извлекают подмножество одной структуры данных в другую.

  • Переход к следующему или предыдущему (NEXT, PREV) находят соседний (следующий или предыдущий) ключ в структуре данных относительно переданного ключа. В связи с тем, что исходный ключ должен обязательно присутствовать в структуре данных, операции NEXT/PREV отличаются от NSM/NGR.

  • Удаление структуры (DELS) очищает все ресурсы, используемые заданной структурой.

  • Команда Squeeze (SQ) дефрагментирует блоки локальной памяти, используемые структурой.

  • Команда Jump (JT) указывает код ветвления, который должен быть синхронизирован с хост CPE (команда доступна только в режиме МКОД при синхронной обработке данных CPU и SPU в составе вычислительного комплекса).

Вызов команд lnh64 осуществляется передачей из микропроцессора riscv64im операндов и кода операции. Результаты выполнения команд сохраняются в регистрах результата (ключ и значение) и регистре статуса. Дополнительно предусмотрена очередь результатов, содержащая аналогичные данные, расположенные последовательно в порядке завершения инструкций. Другим способом передачи результата являются так называемые регистры mailbox.

Механизм ожидания результатов mailbox предполагает наличие регистров, чтение данных из которых возможно только при поступлении в них действительных значений результатов. В случае, если регистр не содержит результатов (статус регистра установлен аппаратно в состояние “регистр пуст”), чтение из него вызывает ошибку доступа и приостанавливает транзакцию на шине. При записи данных со стороны аппаратного обеспечения стату регистра устанавливается в состояние “регистр содержит данные”. После прочтения данных со стороны вычислительного элемента CPE результат регистра mailbox аннулируется (статус регистра снова сбрасывается в состояние “регистр пуст”).

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

2.2. Системная архитектура вычислительного комплекса Тераграф

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

Рисунок 4 — Вычислительный комплекс Тераграф (вид спереди)

Рисунок 5 — Вычислительный комплекс Тераграф (вид сзади)

Конструктив комплекса состоит из следующих элементов:

  1. Три однотипных узла обработки графов (смотри рисунок 4 и 5).
  2. Консоль управления комплексом.
  3. Сеть связи гетерогенных ядер, построенная на основе высокоскоростных сетевых соединений 100Gb Ethernet, показана оранжевым цветом (смотри рисунок 4 и 5).
  4. Сеть связи узлов обработки графов, показана зеленым цветом.
  5. Два источника бесперебойного питания.
  6. Подсистема обработки графов состоит из трех однотипных карт микропроцессора Леонард Эйлер. Каждый микропроцессор содержит 24 гетерогенных ядра обработки графов.
  7. Подсистема хранения графов, реализованная на основе твердотельных накопителей.
  8. Хост-подсистема, основанная на микропроцессорах общего назначения, оперативной памяти и накопителей на жестких магнитных дисках.
  9. Подсистема сетевого взаимодействия узлов обработки графов.

Рисунок 6 — Узел обработки графов

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

Структурная схема одного узла представлена на рисунке 7.

Рисунок 7 — Структура гетерогенного узла обработки графов

Для объединения подсистем обработки графов в единый комплекс предусмотрены сетевые интерфейсы: на уровне узла применяется подсистема сетевого взаимодействия узлов; на уровне ядер GPC применяется кольцевая сеть связи ядер.

Далее рассмотрим подробнее подробно структуру и состав подсистем гетерогенного узла обработки графов.

2.2.1. Хост-подсистема

Основная вычислительная системы (так называемая хост-подсистема) берет на себя функции управления запуском вычислительных задач, поддержкой сетевых подключений, обработкой и балансировкой нагрузки. В хост-подсистему входят два многоядерных ЦПУ по 26 ядер каждый, оперативная память на 1 Тбайт и дополнительная энергонезависимая память на 8 Тбайт, где хранятся атрибуты вершин и ребер графа, буферизируются поступающие запросы на обработку и визуализацию графов, хранятся временные данные об изменениях в графах. В хост-подсистеме используется процессор с архитектурой x86 для обеспечения сетевого взаимодействия и связи системы с внешним миром. В функции хост-подсистемы входят:

  • на стадии инициализации комплекса: настройка сетевой подсистемы, подсистем хранения и обработки графов;

  • на стадии создания/изменения графов в локальной памяти подсистемы обработки графов: реализация очередей запросов на вставку/изменения, балансировка запросов к DISC системам, выделение и освобождение структур, контроль выполнения операций изменения;

  • на стадии запуска алгоритмов оптимизации: буферизация запросов оптимизации, инициализация процедур обработки, их запуск и контроль исполнения;

  • на стадии визуализации графов: буферизация запросов на визуализацию, настройка процедур формирования представлений графов для пользовательских процессов, запуск формирования представлений и контроль результатов, буферизация и передача представлений или изменений в представлениях пользовательским процессам.

Указанные функции реализованы в Программном ядре хост-подсистемы (host software kernel) – программном обеспечении, взаимодействующим с подсистемой обработки графов через шину PCIe.

2.2.2. Подсистема хранения графов

В подсистему хранения графов входят основная память 30Тбайт, состоящая из четырех NVMe SSD дисков по 7,7 Тбайт каждый. Технология NVMe (Non-Volatile Memory Express) обеспечивает интерфейс связи с увеличенной полосой пропускания, что повышает производительность и эффективность обработки графов. Доступ к подсистеме хранения осуществляется как из хост-подсистемы, так и из Подсистемы обработки графов через механизм прямого доступа к памяти.

2.2.3. Подсистема связи узлов

Данная подсистема представляет собой карту PCIe c двумя сетевыми интерфейсами 100Gb Ethernet, размещенную в каждом узле обработки графов. Подсистема позволяет организовать соединение гетерогенного узла с каждым другим узлом комплекса. Шина PCIe обеспечивает высокопроизводительное взаимодействие хост-подсистемы с процессорами Леонард Эйлер, а также последних с подсистемой хранения графов.

2.2.4. Подсистема связи ядер

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

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

  • Организацию нескольких ядер в логическую группу, взаимодействие в рамках которой аппаратно изолировано от других групп. Информация, передаваемая между устройствами группы, не может быть прочтена или изменена другими устройствами, подключенными к сети.
  • Очереди сообщений TX и RX для каждого ядра обработки графов. Поддерживаются очереди сообщений для передачи и приема для каждого ядра. Информация о загрузке очередей устройств передается в виде служебных пакетов канального уровня.
  • Обмен данными внутри логической группы в режиме Unicast и Multicast поддерживается аппаратным обеспечением сетевого уровня.

2.2.5. Подсистема обработки графов

Подсистема обработки графов каждого узла комплекса состоит из 3-х или 4-х карт (в зависимости от версии комплекса) многоядерных микропроцессоров Леонард Эйлер, каждый из которых в свою очередь включает 3 или 4 группы гетерогенных ядер (так называемых Core Groups, CG). В каждую такую группу входят от 2-х до 6-ти ядер DISC GPC, обладающих следующими характеристиками: объем доступной локальной памяти для хранения графов - до 2.5 Гбайт; разрядность ключей и значений - 64 бита; количество хранимых ключей и значений - до 117 миллионов; количество одновременно хранимых структур в локальной памяти структур - до 7; объем ОЗУ CPE - 64 КБайт. Взаимодействие гетерогенных DISC ядер и хост-подсистемы осуществляется как через блоки H2C и С2H с использованием механизма прямого доступа к памяти.

Таким образом, комплекс «Тераграф» может содержать до 288 гетерогенных ядра DISC GPC, и хранить в оперативном доступе (в локальной памяти подсистемы обработки графов) до 11 миллиардов вершин. Группа ядер Core Group содержит контроллеры памяти, которые обеспечивает взаимодействие между GPC и Локальной памятью структур типа DDR4, а также с Глобальной памятью. Один и тот же блок Глобальной памяти используется всеми гетерогенными ядрами группы для передачи данных внутри группы и обмена данными с хост-подсистемой.

Структурная схема микропроцессора Леонард Эйлер версии 4 представлена на рисунке 8. В качестве единицы передаваемых данных принят блок размером 384Б, который передается между хост-подсистемой и группой ядер CG с помощью механизмов прямого доступа к памяти.

Рисунок 8 — Структура микропроцессора Леонард Эйлер

2.3. Микроархитектура гетерогенного ядра обработки графов

Как было отмечено ранее, обработка графов в системе Тераграф выполняется в гетерогенных ядрах, состоящих из микропроцессоров двух типов: CPE и SPE (см. рисунок 9). При этом CPE является универсальным RISC ядром с арифметическим набором команд, в то время как SPE реализует набором команд дискретной математики. Каждый вычислительный элемент CPE состоит из очереди команд, блока выборки, блока декодирования команд, модуля предсказания переходов, арифметико-логического устройства, устройства доступа в память, интерфейса AXI4MM, блока ветвлений и интерфейса шины ускорителя AXL. Также вычислительный элемент связан шиной памяти с ПЗУ, в которой записан загрузчик, обеспечивающий передачу программных ядер. Для размещения программ и данных, каждый CPE имеет оперативную память размером 64КБ.

Рисунок 9 — Микроархитектура ядра обработки графов

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

Под управлением поступающих из хост-системы команд SPE выполняет хранение ключей и значений в многоуровневой подсистеме памяти, выполняет поиск, изменение и выдачу информации другим устройствам системы. Для ускорения поиска и обработки всего набора команд микропроцессор использует внутренне представление множеств в виде B+ дерева, для которого возможна параллельная обработка нескольких вершин дерева как на промежуточных уровнях, используемых для поиска, так и на нижнем уровне, хранящем непосредственно ключи и значения. В связи с этим любая операция над структурой начинается с поиска информации в B+ дереве, а заканчивается обработкой вершин нижнего уровня.

Последовательность вершин B+ дерева, составляющая путь для аппаратного поиска ключа от корня до листа, называется трассой. Трасса сохраняется во внутренней памяти процессора обработки структур для возможности оперативного доступа. После формирования полной трассы в каталоге становится известным информация вершины нижнего уровня, где хранятся ключи и значения. Далее следует загрузить вершину нижнего уровня из внутренней памяти структур в операционный буфер. Для этого требуется транслировать номер вершин в двоичный код, выборки который служит для обращения во внутреннюю память структур. В качестве кода может использоваться как простой линейный адрес ОЗУ, так и код выборки строки из специального массива. Если вершина отсутствует в памяти структур, это приводит к вычислению ее адреса, загрузке вершины из внешней памяти – локальной памяти структур.

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

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

Другой важной функцией операционного буфера является выполнение операций над несколькими линейками для реализации И-ИЛИ-НЕ операций и команд срезов. Таким образом, операционный буфер выполняет изменение линеек и передает их обратно в память структур, а также передает информацию о результатах обработки устройству управления. Модифицированная трасса, в случае создания новых линеек, при необходимости полной или частичной загрузки новой трассы, должна быть сохранена во внешней памяти структур.

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

2.4. Принципы взаимодействия микропроцессора Леонард Эйлер и хост-подсистемы

Основу взаимодействия подсистем при обработке графов составляет передача сообщений между GPC и хост-подсистемой. Для передачи сообщений для каждого GPC реализованы два аппаратных блока, реализующие механизмы прямого доступа к памяти хоста. Блоки содержат FIFO буферы на 512 64-х битных записей: H2C для передачи от хост-подсистемы к ядру, и C2H для передачи в обратную сторону.

Обработка начинается с того, что скомпилированный программный код CPE, называемое далее “программное ядро” (software kernel) загружается в локальное ОЗУ одного или нескольких CPE (микропроцессора riscv64im). Для этого используется программно доступная конфигурационная Глобальная память размером 64КБ, расположенная в микропроцессоре Леонар Эйлер (смотри рисунок 10). Далее в ядро, подлежащее инициализации, передается сигнал инициализации. В свою очередь, инициализируемый GPC (один или несколько) вместе с сигналом инициализации получают информацию о размере образа sowftware kernel. После этого управление принимает специальный загрузчик, хранимый в ПЗУ CPE. Загрузчик выполняет копирование программного ядра из Глобальной памяти в ОЗУ CPE и передает управление на начальный адрес программы обработки. Предусмотрен режим работы GPC, при котором во время обработки происходит обмен данными и сообщениями. Эти два варианта работы реализуется через буферы и очереди соответственно. На рисунке 10 представлена диаграмма последовательностей первого сценария работы – вызов обработчика с передачей параметров и возвратом значения через очередь сообщений.

Диаграмма последовательностей инициализации вычислительного элемента CPE

Рисунок 10 - Диаграмма последовательностей инициализации вычислительного элемента CPE

Если код программного ядра уже загружен в ОЗУ CPE, хост-подсистема может вызвать любой из содержащихся в нем обработчиков. Для этого в GPC передает оговоренный номер обработчика (handler), после чего автоматически передается сигнал запуска указанного обработчика (сигнал START). В ответ CPE устанавливает состояние BUSY и начинает саму обработку. В ходе обработки ядро может обмениваться сообщениями с хост-подсистемой через очереди (команды записи в очередь и чтения из очереди). По завершении обработки устанавливается состояние IDLE, что приводит к выработке прерывания DONE, которое перехватывается хост-подсистемой и информирует пользовательский код об изменении состояния программного ядра. Далее, пользовательское приложение хост-подсистемы может прочитать результат (если таковой был передан через передачу сообщений), и повторить всю указанную процедуру.

Диаграмма последовательностей запуска обработчика вычислительного элемента CPE

Рисунок 11 - Диаграмма последовательностей запуска обработчика вычислительного элемента CPE

Основным назначением вычислительного ядра CPE является управление ходом работы ядра обработки структур SPE. Принимаемые от Хост-подсистемы данные преобразуются в запросы к ядру SPE lnh64. На рисунке 11 показан пример, когда полученные от хост-подсистемы данные транслируются в запрос вставки ключа и значения в одну из структур по команде lnh_ins(str,key,val). Второй блок данных, полученных с использованием механизма передачи данных через очереди, приводит в выдаче команды поиска по ключу lnh_search(str,key). Результат поиска передается в блок C2H и и использованием механизма прямого доступа к памяти оказывается в памяти хоста. Более подробно, механизм прямого доступа к памяти будет рассмотрен ниже.

2.5. Библиотека lnh64 L0

Библиотека lnh64 L0 (библиотека уровня “ноль”) представляет собой API системного уровня, реализующего базовые функции взаимодействия с драйвером ускорительной карты микропроцессора Леонард Эйлер. Библиотека позволяет:

  • Создание файлового дескриптора для эксклюзивного доступа к символьному устройству GPC /dev/gpc*.
  • Инициализация вычислительного элемента CPE.
  • Запуск обработчика вычислительного элемента CPE.
  • Прием и передачи сообщений для взаимодействия Host и CPE.
  • Взаимодействие CPE и SPE (микропроцессором lnh64 DISC).

Библиотека разделена на две части, представленные в таблице 2.

Таблица 2 - Описание частей библиотеки lnh64 L0

Раздел библиотеки Описание Язык программирования Архитектура, Компилятор Способ отладки
Host Lib Управления хост-подсистемой и взаимодействие с ядрами GPC C/C++, объектная модель x86, g++ Jupyter,VSCode,gdb
SW Kernel Lib Взаимодействие с микропроцессором lnh64 C/C++, процедурная модель riscv64, g++ Вывод сообщений

Функциональные возможности Host Lib:

  • Получение информации от дайвера о количестве и состоянии гетерогенных ядер gpc
  • Открытие указанного пользователем или любого свободного gpc в эксклюзивнымй доступ пользователя.
  • Инициализация буферов пользователького пространства для взаимодействия хост-подсистемы и подсистемы обработки графов (ядер обработки графов GPC).
  • Передача и прием данных и сообщений к/от GPC через очереди mq (метод mq_send() и mq_receive()).

Функциональные возможности SW Kernel Lib:

  • Обмен сообщениями с хост-подсистемой через очереди mq (метод mq_send() и mq_receive()).
  • Установка состояния sw_kernel (IDLE - свободен ,BUSY - занят).
  • Вызов пользовательского прерывания sw_interrupt с передачей 64 бит данных (так называемый payload)
  • Сервиcные функции для преобразования типов float и double в беззнаковый образ (необходимо для хранения данных вещественных типов в качестве ключей lnh64)
  • Реализация вызовов команд DISC в SPE DISC lnh64 (полное название: Процессорный элемент обработки структур данных с набором команд дискретной математики и микроархитектурой lnh64).
  • Передача операндов и кодов операций набора команд дискретной математики DISC в микропроцессор lnh64.
  • Контроль результатов исполнения DISC команд.

2.5.1. Структура базового приложения “hello world”

Для использования функций библиотек необходимо также реализовать основные приложения для host и sw_kernel частей. Рассмотрим примеры таких приложений, выполняющих базовые операции. Ниже приведен пример кода программы хост-подсистемы, выполняющей инициализацию и измерение тактовой частоты GPC (hello world). Код доступен по следующему адресу: Леонард Эйлер, пример 1

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include "host_main.h"

#define BUF_SIZE 117440512*sizeof(unsigned long long)

#define handle_error(msg) \
           do { perror(msg); exit(EXIT_FAILURE); } while (0)


int main(int argc, char** argv)
{

	unsigned int err = 0;
	unsigned long long data;
	double LNH_CLOCKS_PER_SEC;
	clock_t start, stop;
	gpc *gpc64_inst;

	if (argc<2) {
		printf("Usage: host_main <rawbinary file>\n");
		return -1;
	}

	//Захват ядра gpc и запись sw_kernel
	gpc64_inst = new gpc();
	printf("Open gpc on %s\n",gpc64_inst->gpc_dev_path);
	if (gpc64_inst->load_swk(argv[1])==0) {
		printf("Rawbinary loaded from %s\n",argv[1]);
	}
	else {
		printf("Rawbinary %s file error\n",argv[1]);
		return -1;
	}
	//Обработкчик для чтения версии sw_kernel
	gpc64_inst->start(__event__(get_version));
	printf("sw_kernel version: 0x%0llx\n", gpc64_inst->mq_receive());

	//Запуск обработчика для измерения тактовой частоты gpc
	gpc64_inst->start(__event__(frequency_measurement));
	gpc64_inst->sync();
	sleep(1);
	gpc64_inst->sync();
	LNH_CLOCKS_PER_SEC = (double)gpc64_inst->mq_receive();
	printf("Leonhard clock frequency (LNH_CF) %f MHz\n", LNH_CLOCKS_PER_SEC/1000000.0);

	//Обработкчик для посылки эхо-пакетов
	gpc64_inst->start(__event__(echo_mq));
	//Создание исходного массив
	unsigned long long *buf_out=(unsigned long long *)malloc(BUF_SIZE);
	unsigned long long *buf_in=(unsigned long long *)malloc(BUF_SIZE);
	for (int i=0;i<(BUF_SIZE>>3);i++) {
		buf_out[i]=(rand()<<32)|rand();
	}
	//Запуск потоков приема-передачи
	auto send_thread = gpc64_inst->mq_send(BUF_SIZE,(char*)buf_out);
	auto receive_thread =  gpc64_inst->mq_receive(BUF_SIZE,(char*)buf_in);
	send_thread->join();
	receive_thread->join();
	for (int i=0;i<(BUF_SIZE>>3);i++) {
		if (buf_out[i]!=buf_in[i]) {
			printf("Error: buf_out[%d]=0x%016llx - buf_in[%d]=0x%016llx\n",i,buf_out[i],i,buf_in[i]);
			err++;
		}
	}
	if (!err) {printf("Test done\n");}

	// gpc64_inst->finish(); //newer finished

	//Освобождение ресурсов
	free(gpc64_inst);
	return 0;
}

Для представленного листинга должен быть также создан и скомпилирован ответный код для микропроцессора riscv64im, который будет работать в составе гетерогенного ядра обработки графов. В коде должна быть реализована логика установки состояния ядра: одно из двух состояний READY или BUSY. Также разработчиком должы быть реализованы обработчики вызываемых из хост-подсистемы функций get_version(), get_lnh_status_high(), get_lnh_status_low(), frequency_measurement().

Номер обработчика может быть задан явным образом в Xост и sw_kernel частях, однако удобнее использовать механизм автоматической нумерации обработчиков на основе макросов С. Для этого мы будем использовать файл gpc_handkers.h, который должен быть включен как в проект хоста, так и в проект sw_kernel:

/*
 * gpc_handlers.h
 *
 * host and sw_kernel library
 *
 * Macro instantiation for handlers
 *
 */
#ifndef DEF_HANDLERS_H_
#define DEF_HANDLERS_H_
#define DECLARE_EVENT_HANDLER(handler) \
            const unsigned int event_ ## handler =__LINE__; \
            void handler ();
#define __event__(handler) event_ ## handler
//  Event handlers declarations by declaration line number 
DECLARE_EVENT_HANDLER(frequency_measurement);
DECLARE_EVENT_HANDLER(get_lnh_status);
DECLARE_EVENT_HANDLER(get_version);
DECLARE_EVENT_HANDLER(echo_mq);
#endif

Таким образом, условное имя обработчика ставится в однозначное соответствие номеру строки, в которой он объявлен в файле include/gpc_handlers.h.

В результате получим следующий код основного модуля sw_kernel

#include <stdlib.h>
#include "lnh64.h"
#include "gpc_io_swk.h"
#include "gpc_handlers.h"

#define SW_VERSION 0x20232109
#define __fast_recall__

extern lnh lnh_core;
volatile unsigned int event_source;

int main(void) {
    /////////////////////////////////////////////////////////
    //                  Main Event Loop
    /////////////////////////////////////////////////////////
    //Leonhard driver structure should be initialised
    lnh_init();
    for (;;) {
        //Wait for event
        event_source = wait_event();
        switch(event_source) {
            /////////////////////////////////////////////
            //  Measure GPN operation frequency
            /////////////////////////////////////////////
            case __event__(frequency_measurement) : frequency_measurement(); break;
            case __event__(get_lnh_status) : get_lnh_status(); break;
            case __event__(get_version): get_version(); break;
            case __event__(echo_mq): echo_mq(); break;

        }
        set_gpc_state(READY);
    }
}

    
//-------------------------------------------------------------
//      Глобальные переменные
//-------------------------------------------------------------
    
        unsigned int LNH_key;
        unsigned int LNH_value;
        unsigned int LNH_status;
        uint64_t TSC_start,TSC_stop;
        int i,j;
        unsigned int err=0;

//-------------------------------------------------------------
//      Измерение тактовой частоты GPN
//-------------------------------------------------------------
 
void frequency_measurement() {
    
        sync();          //синхронизация с host
        TSC_start = TSC; //сохранение счетчика тактов
        sync();          //синхронизация с host
        mq_send_flush(TSC-TSC_start); //вычисление количества тактов и перезача в host

}


//-------------------------------------------------------------
//      Получить версию микрокода 
//-------------------------------------------------------------
 
void get_version() {
    
        mq_send_flush(SW_VERSION); //передача в очередь c2h номера версии программы

}
   

//-------------------------------------------------------------
//      Получить регистр статуса LOW Leonhard 
//-------------------------------------------------------------
 
void get_lnh_status() {
    
        mq_send_flush(LNH_STATE); //передача в очередь c2h номера аппаратной версии SPE lnh64
}

//-------------------------------------------------------------
//      Передача эхо пакетов через очереди сообщений
//-------------------------------------------------------------
 
void echo_mq() {
        while(1) {
                unsigned long long data = mq_receive(); //получить сообщение
                mq_send_flush(data); //передать сообщение
        }
}

2.5.2. Обмен данными между GPC и хост-подсистемой через аппаратные очереди

Взаимодействие обработчика sw_kernel и хост-подсистемы осуществляется через апааратные очереди сообщений c2h и h2c с помощью команд передачи и приема (mq_send() и mq_receive()). Передача сообщений на аппаратном уровне выполняется с использованием механизма прямого доступа к памяти, что существенно ускоряет обмен больших блоков данных. Отметим, что данные могут ритмично передаваться только в том случае, если принимающая сторона выполняет их чтение. В противном случае внутренние буферы будут переполнены, и передача временно прекратится. В хост подсистеме реализованы многопоточные асинхронные методы передачи и приема сообщений, блокирующие доступ других потоков к приему и передаче. Это позволяет запускать множество потоков одновременно, и при этом не нарушать последовательность их запуска. Рассмотрим методы класса gpc, описанного в библиотеке lnh64 L0:

Таблица 3 - Методы класса gpc для передачи и приема сообщений хост-подсистемой

Метод класса Назначение
void mq_send(unsigned long long data) Синхронная передача 8 байт (базовый размер операнда lnh64) в gpc.
std::thread* mq_send(unsigned int bufsize,char *buf) Асинхронная передача блока данных из буфера buf, bufsize байт. Возвращает указатель на объект потока thread, в котором выполняется запись. Несколько запущенных потоков выполняются последовательно в порядке запуска.
unsigned long long mq_receive() Синхронное получение 8 байт из gpc. Возвращает данные типа unsigned long long
std::thread* mq_receive(unsigned int bufsize,char *buf) Асинхронный прием блока данных из gpc в буфер buf, bufsize байт. Возвращает указатель на объект потока thread, в котором выполняется чтение. Несколько запущенных потоков выполняются последовательно в порядке запуска.

Потоки mq_send() и mq_receive() могут и должны выполняться параллельно, что приводит к необходимости синхронизации вычислений в хост-подсистеме и gpc. В связи с этимна обоих сторонах реализован метод sync(), осуществляющий процедуру рукопожатия двух сторон.

На стороне хост-подсистемы:

//====================================================
// Синхронизация с gpc (рукопожатие)
//====================================================
void gpc::sync()
{
    mq_send(0xdeadbeafdeadbeaf);
    while (mq_receive()!=0xbeafdeadbeafdead);
}

Пример ответного кода sw_kernel:

//====================================================
// Синхронизация с хостом (рукопожатие)
//====================================================
void sync() 
{
    while (mq_receive()!=0xdeadbeafdeadbeaf);
    mq_send_flush(0xbeafdeadbeafdead);
}

Со стороны sw_kernel, работающего в gpc, также реализованы функции приема и передачи сообщений. Отличие от хост подсистемы состоит в том, что в sw_kernel принята процедурная модель разработки, существенно сокращающая объем кода. Ядро CPE riscv64im не реализует многозадачность, многонитевость или многопоточность, в связи с чем программист дожен представить программу в последовательном виде. В случае, если требуется реализовать прием и передачу сообщений одновременно, необходимо попеременное чтение данных, используя чтение в буфер (0 и более байт), и в зависимости от потребностей алгоритма выполнить отправку необходимой информации.

Для реализации всех необходимых разработчику действий в библиотеке sw_kernel-lib реализованы следующие функции:

Таблица 4 - Функции для передачи и приема сообщений программным ядром sw_kernel

Функция Назначение
void mq_send(unsigned long long data); Передача 8 байт (базовый размер операнда lnh64) в хост-подсистему
void mq_send_flush(unsigned long long data); Ожидание завершения потока передачи (любого из ранее описанных методов mq_send)
void mq_send(unsigned int bufsize,char *buf); Передача блока данных из буфера buf, bufsize байт в хост-подсистему
unsigned long long mq_receive(); Получение 8 байт из хост-подсистемы
unsigned int mq_receive(unsigned int bufsize,char *buf); Получение блока данных из хост-подсистемы в буфер buf, не более bufsize байт (от 0 до bufsize)

На аппаратном уровне gpc передача реализована в двух вариантах:

  1. Пакетная передача. Данный способ передачи обладает наибольшей эффективностью, так как за цикл прямой передачи в память хост-подсистемы осуществляется передача большого пакета данных и поле данных в пакете pcie используется в максимальной степени. Такой вариант передачи осуществляется послеовательной записью данных по адресу FIFO буфера очереди c2h:
    for (offs=0;offs<bufsize;offs+=8) {
     C2H = *((volatile unsigned long long*)(buf+offs));
    }
    

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

  2. Незамедлительная отправка сообщений. Способ предусматривает передачу всех накопленных в выходной FIFO очереди данных, сколько бы их там не находилось. Минимальный размер составляет 8 байт, максимальный размер ограничен размером FIFO очереди (для текущей версии составляет 4KБайт-16 байт). В этом случает программист, помимо записи данных в C2H (адрес FIFO буфера), в следующей команде посылает флаг в регистр C2H_FLUSH:
void mq_send(unsigned int bufsize,char *buf) {
    unsigned int offs;
    for (offs=0;offs<bufsize;offs+=8) {
        C2H = *((volatile unsigned long long*)(buf+offs));
    }
    C2H_FLUSH = 1;
}

2.6. Взаимодействие CPE(riscv64im) и SPE(lnh64)

Микропроцессор lnh64 с набором команд дискретной математики (Discrete Mathematics Instruction Set Computer) является ассоциативным процессором, т.е. устройством, выполняющим операции обработки над данными, хранящимися в ассоциативной памяти (так называемой Локальной памяти структур). В качестве таковой выступает адресная память DDR4, причем для каждого ядра lnh64 доступны 2.5 ГБ адресного пространства в ней. Для организации ассоциативного способа доступа к адресному устройству микропроцессор lnh64 организует на аппаратном уровне структуру B+дерева. Причем 512МБ занимает древовидая структура от верхнего и до предпоследнего уровня, 2048МБ занимает последний уровень дерева, на котором и хранятся 64х разрядные ключи и значения. Каждый микропроцессор lnh64 может хранить и обрабатывать до 117 миллионов ключей и значений.

Исходя из этого, обработка множеств или графов представляется в DISC наборе команд, как работа со структурами ключей и значений (key-value). Однако, как было показано ранее при описании набора команд DISC, в отличие от общепринятых key-value хранилищ, доступны такие операции как ближайший больший (NGR), ближайший меньший (NSM), команды объединения множеств (OR) и ряд других. Это и позволяет использовать lnh64 в качестве устройства, хранящего большие множества (для графов это множества вершин и ребер).

2.6.1. Описание регистров микропроцессорного ядра SPE с набором команд дискретной математики DISC

Доступ к микропроцессору lnh64 (Structure Processing Element) осуществляется чтением и записью в пространство памяти микропроцессора riscv64im (Computing Processing Element) в диапазоне 0x300000 - 0x301000. Карта памяти представлена в файле gpc_swk.h:

Микропроцессор lnh64 получает на вход команды 6 различных форматов. Так, для команды вставки INS задействуются регистр кода операции, регистр ключа операнда и регистр значения операнда. Результатом выполнения команды является статус ее исполнения, который записывается в регистр статуса. Для команды поиска SRCH задействуются регистры ключа операнда и регистр кода операции, а результаты записываются в регистры ключа результата, значения результата и регистр статуса.

Перечень программно-доступных регистров и их смещения в адресном пространстве относительно базового адреса (0x300000) указан в таблице 5:

Таблица 5 - Программно доступные регистры lnh64

Регистр      Смещение Режим Начальное значение  Назначение
KEY2LNH 0x0000 W 0x00000000 Регистр содержит ключ операнда команд DISC
LNH2KEY 0x0000 R 0x00000000 Регистр содержит ключ результата команды DISC
VAL2LNH 0x0008 W 0x00000000 Регистр содержит значение операнда команд DISC
LNH2VAL 0x0008 R 0x00000000 Регистр содержит значение результата команд DISC
CMD2LNH 0x0010 W 0x00000000 Регистр содержит код операции DISC
LNH_STATE 0x0010 R 0x09110611 Регистр содержит статус микропроцессора lnh64
CARDINALITY 0x0018 R 0x00000000 Количество ключей в структуре , указанной в поле R регистра CMD2LNH
LNH_CNTL 0x0020 W 0x00000000 Регистр управления
LNH2KEYQ 0x0028 R 0x00000000 Очередь ключей результатов команд DISC
LNH2VALQ 0x0030 R 0x00000000 Очередь значений результатов команд DISC
LNH_STATEQ 0x0038 R 0x00000000 Очередь статуса результатов команд DISC
TSC 0x0040 R 0x00000000 Регистр счетчика тактов
СSC 0x0048 R 0x00000000 Регистр счетчика тактов исполнения команд
DBG_A 0x0050 R 0x00000000 Регистр отладки A
DBG_B 0x0058 R 0x00000000 Регистр отладки B
DBG_C 0x0060 R 0x00000000 Регистр отладки C
DBG_D 0x0068 R 0x00000000 Регистр отладки D
MR0 0x0080 R 0x00000000 Регистр 0 mailbox с ожиданием результата
MR1 0x0088 R 0x00000000 Регистр 1 mailbox с ожиданием результата
MR2 0x0090 R 0x00000000 Регистр 2 mailbox с ожиданием результата
MR3 0x0098 R 0x00000000 Регистр 3 mailbox с ожиданием результата
MR4 0x00A0 R 0x00000000 Регистр 4 mailbox с ожиданием результата
MR5 0x00A8 R 0x00000000 Регистр 5 mailbox с ожиданием результата
MR6 0x00B0 R 0x00000000 Регистр 6 mailbox с ожиданием результата
MR7 0x00B8 R 0x00000000 Регистр 7 mailbox с ожиданием результата

Регистр управления содержит биты для управления ресурсами lnh64. Биты 0..31 устанавливаются в необходимое состояние записью соответствующего значения в регистр LNH_CNTL. Биты 32..63 при записи логической 1 выдают одиночный импульс сброса ресурса, после чего автоматически устанавливаются в значение 0. Назначение бит для регистра управления представлено в таблице 6.

Таблица 6 -Назначение разрядов регистра управления

Название           Бит Назначение
ALLOW_LNH_FLAG 0 Разрешение работы lnh64
SUSPEND_Q_FLAG 1 Останов выдачи транзакций из очереди запросов на запись в lnh64
LSM_DMA_FLAG 2 Разрешение прямого доступа к LSM
LCM_DMA_FLAG 3 Не используется
ENABLE_TSC_FLAG 4 Разрешение работы счетчика тактов
ENABLE_READY_INT 5 Не используется
RESET_MAILBOX[0] 32 Запуск импульса сброса регистра mailbox[0]
RESET_MAILBOX[1] 33 Запуск импульса сброса регистра mailbox[1]
RESET_MAILBOX[2] 34 Запуск импульса сброса регистра mailbox[2]
RESET_MAILBOX[3] 35 Запуск импульса сброса регистра mailbox[3]
RESET_MAILBOX[4] 36 Запуск импульса сброса регистра mailbox[4]
RESET_MAILBOX[5] 37 Запуск импульса сброса регистра mailbox[5]
RESET_MAILBOX[6] 38 Запуск импульса сброса регистра mailbox[6]
RESET_MAILBOX[7] 39 Запуск импульса сброса регистра mailbox[7]
RESET_SPU 48 Сброс lnh64 в начальное состояние (мягкий сброс)
RESET_ALL_QUEUES 49 Сброс состояния всех очередей
RESET_LNH2AXI_QUEUE 50 Сброс очереди запросов на чтение lnh64
RESET_AXI2LNH_QUEUE 51 Сброс очереди запросов на запись lnh64
RESET_TSC 52 Сброс счетчика тактов
RESET_RISCV 53 Аппаратный сброс lnh64 в начальное состояние

Регистр статуса позволяет отслеживать готовность результатов выполнения операций (готовность, наличие ошибки), состояние очередей, версию аппаратного обеспечения и ряд других параметров. Назначение бит для регистра статуса представлено в таблице 7.

Таблица 7 - Назначение разрядов регистра статуса

Название           Бит Назначение
SPU_READY_FLAG 0 Флаг завершения команды/готовности к приему команды
SPU_ERROR_FLAG 1 Флаг ошибки выполнения команды
SPU_ERROR_Q_FLAG 2 Флаг ошибки выполнения команды в очереди статуса результатов
DDR_Q_OVF_FLAG 3 Флаг переполнения очереди к DDR LSM памяти
DDR_TEST_SUCC_FLAG 4 Результат верификации контролера памяти DDR4 (не использован) = 0
NU 5-8 Не использованы
SPU_ALL_DONE 9 Очередь команд пуста и последняя команда исполнена
AXI2LNH_Q_EMP_FLAG 16 Очередь запросов на запись пуста
AXI2LNH_Q_FULL_FLAG 17 Очередь запросов на запись переполнена
AXI2LNH_Q_AEMP_FLAG 18 Очередь запросов на запись наполовину пуста (содержит <256 значений)
AXI2LNH_Q_AFULL_FLAG 19 Очередь запросов на запись наполовину заполнена (содержит >256 значений)
LNH2AXI_Q_EMP_FLAG 20 Очередь запросов на чтение пуста
LNH2AXI_Q_FULL_FLAG 21 Очередь запросов на чтение переполнена
LNH2AXI_Q_AEMP_FLAG 22 Очередь запросов на чтение наполовину пуста (содержит <256 значений)
LNH2AXI_Q_AFULL_FLAG 23 Очередь запросов на чтение наполовину заполнена (содержит >256 значений)
MBOX_VFLAG 32-40 Биты готовности операндов в регистрах mailbox[0..7], 1 - готовность
LNH_DATA_PARTITION 48-50 Номер партиции DDR данных нижнего уровня B+дерева (0..7)
LNH_INDEX_PARTITION 51-53 Номер партиции DDR индексной части B+дерева (0..7)
LNH_INDEX_REGION 54-55 Номер региона DDR индексной части B+дерева в LNH_INDEX_PARTITION (0..3)

2.6.2. Вызовы и передача операндов команд дискретной математики

Операции чтения и записи регистров lnh64 в DISC в библиотеке SW Kernel Lib выполняются с помощью макросов как при помощи непосредственных параметров (по значению), так и с помощью адреса параметра (по ссылке).

Таблица 8 - Макросы доступа к регистрам lnh64

Макрос Назначение
lnh_wr_reg64_byref(adr, value) Запись регистра 64 бит (Адрес, Данные) по ссылке
lnh_wr_reg64_byval(adr, value) Запись регистра 64 бит (Адрес, Данные) по значению
lnh_rd_reg64_byref(adr, value) Чтение регистра 64 бит (Адрес, Данные) по ссылке
lnh_rd_reg64_byval(adr) Чтение регистра 64 бит (Адрес) => Данные по значению
lnh_wr_reg32_l2l_byref(adr, value) Запись регистра 32 бит (Адрес, Данные) по ссылке
lnh_wr_reg32_byval(adr, value) Запись регистра 32 бит (Адрес, Данные) по значению
lnh_rd_reg32_byref(adr, value) Чтение регистра 32 бит (Адрес, Данные) по ссылке
lnh_rd_reg32_byval(adr) Чтение регистра 32 бит (Адрес) => Данные по значению

Для ускорения запуска команд, когда по сравнению с предыдущей командой меняется только часть операндов, может применяться функция __fast_recall(), передающая только измененные относительно предыдущей команды операнды и код операции.

Типичным примером использования макросов для запуска команды на выполнение и ожидание результатов является команда Вставки (INS) ключа и значения. Для этого, необходимо проверить возможность записи в очередь запросов на запись. При этом, обращение к очереди требуется выполнять только в том случае, если исчерпан лимит на количество последовательных операций записи (256 записей). Только в случае превышения лимита проверяется флаг AXI2LNH_Q_AFULL_FLAG. При освобождении половины от имеющего ся места в очереди посылка транзакций возобновлется.

Микроархитектура lnh64 допускает обращение к одной из семи независимых структур (1..7). Структура с инексом 0 не используется для хранения и зарезервирована.

Далее происходит запись ключей и значений в регистры KEY2LNH и VAL2LNH, а также посылка кода операции в регистр CMD2LNH. При этом указывается параметр str, определяющий номер структуры, в которую должна произойти вставка нового ключа. После записи старшей части регистра CMD2LNH (CMD2LNH_HIGH) происходит запуск команды на исполнение.

Далее выполняется ожидание готовности (проверяется бит SPU_READY_FLAG регистра статуса), после чего выполняется чтение регистра состояния и анализ результата. Статус выполнения команды, а для других команд ключ и значение результата записываются в структуру lnh_core.result.

//====================================================
// Добавление (Структура, Ключ, Значение)
//====================================================

bool lnh_ins_sync(uint64_t str, uint64_t key, uint64_t value)
{
    //проверка готовности устройства
	    lnh_axi2lnh_queue_credits_check;

    //запись исходных данных
	    lnh_wr_reg64_byref(KEY2LNH, &key);
        lnh_wr_reg64_byref(VAL2LNH, &value);
    	lnh_wr_reg64_byval(CMD2LNH, (INS<<lnh_cmd)|str);

    //ожидание готовности очереди команд
		lnh_sync();

    //чтение результата
	    lnh_rd_reg64_byref(LNH_STATE,&lnh_core.result.status);

    //results
	    if ((lnh_core.result.status & (1<<SPU_ERROR_FLAG)) != 0) {
			return false;
		} else {
			return true;
		}
}

Функции для вызова команд DISC организованы в виде шести групп:

Таблица 9 - Функции библиотеки lnh64 L0 для вызова команд DISC lnh64

Группа функций / функция Пояснение
Функции для работы с Leonhard API В группу входят функции чтения и записи регистров Lnh64
        lnh_hw_reset() Аппаратный сброс GPC, удаление всех структур Lnh64, сброс riscv микропроцессора, сброс очередей mq
        lnh_sw_reset() Программное удаление всех структур Lnh64
        lnh_init() Инициализация lnh64, установка указателей на буферы Глобальной памяти
        lnh_rd_reg64(adr) Чтение 64 разрядного регистра lnh64 микропроцессора
        lnh_rd_reg32(adr) Чтение 32 разрядной части регистра lnh64 микропроцессора
        lnh_fast_recall(key)
        lnh_fast_recall(key,value)
Быстрый перезапуск предыдущей команды. Ускорение достигается благодаря передаче только части операндов (только ключ, только младшая часть ключа и т.д.)
Сервисные функции Leonhard API В группу входят функции преобразования типов и ожидания готовности lnh64
        float2uint(value) Представление значения типа float в целочисленном виде (инферсия знака мантиссы) для сохранения в ввиде поля ключа. Команда позволяет исопльзовать целочиссленное безщнаковое сравнение для чисел float
        uint2float(value) Функция, обратная к float2uint, позволяет преобразовать часть ключа, сохраненного в виде unsigned int в тип float
        double2ull(value) Представление значения типа double в целочисленном виде (инферсия знака мантиссы) для сохранения в ввиде поля ключа. Команда позволяет исопльзовать целочиссленное безщнаковое сравнение для чисел double
        ull2double(value) Функция, обратная к double2ull, позволяет преобразовать часть ключа, сохраненного в виде unsigned int в тип double
        lnh_sync() Ожидание готовности результатов выполнения команды в Lnh64. Функция ожидает завершения всех команд очереди команд Lnh64
        lnh_syncm(mbr) Ожидание готовности регистра Mailbox. При запуске команд DISC с записью результатов в регистры mbr сбрасывется флаг достоверности указанных регистров. При помещении результатов в mbrx регистр, флаг устанавливается и разрешается его чтение.
Синхронные функции Leonhard API Функции для вызова команд DISC с ожиданием их завершения и чтением результатов в структуру lnh_core.results
        lnh_ins_sync(str,key,value) Вставка ключа key и значения value в структуру str.
        lnh_del_sync(str,key) Удаление записи с ключом key из структуры str.
        lnh_get_num(str) Получить количество записей в структуре str.
        lnh_del_str_sync(str) Удаление структуры str.
        lnh_sq_sync(str) Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти).
        lnh_or_sync(A,B,R) Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R.
        lnh_and_sync(A,B,R) Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R.
        lnh_not_sync(A,B,R) Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R.
        lnh_lseq_sync(key,A,R) Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют).
        lnh_ls_sync(key,A,R) Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют).
        lnh_greq_sync(key,A,R) Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют).
        lnh_gr_sync(key,A,R) Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют).
        lnh_grls_sync(key_ls,key_gr,A,R) Срез структуры A по условию “больше” ключа key_ls и “меньше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют).
        lnh_search(str,key) Поиск ключа key в структуре str и выдача найденного ключа и значения value.
        lnh_next(str,key) Поиск следующего ключа, следующего за ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах.
        lnh_prev(str,key) Поиск предыдущего ключа, следующего перед ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах.
        lnh_ngr(str,key) Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах.
        lnh_nsm(str,key) Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах.
        lnh_ngr_signed(str,key) Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах.
        lnh_nsm_signed(str,key) Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах.
        lnh_get_first(str) Поиск наименьшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах.
        lnh_get_last(str) Поиск наибольшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах.
        lnh_get_first_signed(str) Поиск наименьшего ключа структуры str. Поиск выполняется в целочисленном знаковом порядке на ключах.
        lnh_get_last_signed(str) Поиск наибольшего ключа структуры str. Поиск выполняется в целочисленном знаковом порядке на ключах.
Синхронные функции Leonhard API с записью в очередь результатов Функции для вызова команд DISC с ожиданием их завершения и автоматической записью результатов в очередь (регистры LNH2KEYQ, LNH2VALQ, LNH_STATEQ)
        lnh_ins_syncq(str,key,value) Вставка ключа key и значения value в структуру str. Запись результата в очередь.
        lnh_del_syncq(str,key) Удаление записи с ключом key из структуры str. Запись результата в очередь.
        lnh_get_numq(str) Получить количество записей в структуре str.Запись результата в очередь.
        lnh_del_str_syncq(str) Удаление структуры str. Запись результата в очередь.
        lnh_sq_syncq(str) Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти). Запись результата в очередь.
        lnh_or_syncq(A,B,R) Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в очередь.
        lnh_and_syncq(A,B,R) Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в очередь.
        lnh_not_syncq(A,B,R) Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R. Запись результата в очередь.
        lnh_lseq_syncq(key,A,R) Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют). Запись результата в очередь.
        lnh_ls_syncq(key,A,R) Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют). Запись результата в очередь.
        lnh_greq_syncq(key,A,R) Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют). Запись результата в очередь.
        lnh_gr_syncq(key,A,R) Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют). Запись результата в очередь.
        lnh_grls_syncq(key_ls,key_gr,A,R) Срез структуры A по условию “больше” ключа key_ls и “меньше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют). Запись результата в очередь.
        lnh_searchq(str,key) Поиск ключа key в структуре str и выдача найденного ключа и значения value. Запись результата в очередь.
        lnh_nextq(str,key) Поиск следующего ключа, следующего за ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь.
        lnh_prevq(str,key) Поиск предыдущего ключа, следующего перед ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь.
        lnh_ngrq(str,key) Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь.
        lnh_nsmq(str,key) Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь.
        lnh_get_firstq(str) Поиск наименьшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь.
        lnh_get_lastq(str) Поиск наибольшего ключа структуры str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в очередь.
        lnh_get_q() Чтение результата из очереди
Асинхронные функции Leonhard API с записью в асинхронный Mailbox Вызов команд DISC без ожидания их завершения и с записью результатов в регистры mbr. На стороне sw_kernel возможно чтение с ожиданием готовности mbr регистров функцией lnh_syncm(int mbr)
        lnh_ins_syncm(st_mreg,str,key,value) Вставка ключа key и значения value в структуру str. Запись результата в регистр st_mreg.
        lnh_del_syncm(st_mreg,str,key) Удаление записи с ключом key из структуры str. Запись результата в регистр st_mreg.
        lnh_get_numm(str) Получить количество записей в структуре str. Запись результата в регистр mrf.
        lnh_del_str_syncm(st_mreg,str) Удаление структуры str. Запись результата в регистр st_mreg.
        lnh_sq_syncm(st_mreg,str) Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти). Запись результата в регистр st_mreg.
        lnh_or_syncm(st_mreg,A,B,R) Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в регистр st_mreg.
        lnh_and_syncm(st_mreg,A,B,R) Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R. Запись результата в регистр st_mreg.
        lnh_not_syncm(st_mreg,A,B,R) Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R. Запись результата в регистр st_mreg.
        lnh_lseq_syncm(st_mreg,key,A,R) Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют). Запись результата в регистр st_mreg.
        lnh_ls_syncm(st_mreg,key,A,R) Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют). Запись результата в регистр st_mreg.
        lnh_greq_syncm(st_mreg,key,A,R) Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют). Запись результата в регистр st_mreg.
        lnh_gr_syncm(st_mreg,key,A,R) Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют). Запись результата в регистр st_mreg.
        lnh_grls_syncm(st_mreg,key_ls,key_gr,A,R) Срез структуры A по условию “меньше” ключfа key_ls и “больше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют). Запись результата в регистр st_mreg.
        lnh_searchm(key_mreg,val_mreg,st_mreg,str,key) Поиск ключа key в структуре str и выдача найденного ключа и значения value. Запись результата в регистр st_mreg.
        lnh_nextm(key_mreg,val_mreg,st_mregstr,key) Поиск следующего ключа, следующего за ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg.
        lnh_prevm(key_mreg,val_mreg,st_mregstr,key) Поиск предыдущего ключа, следующего перед ключом key в структуре str. Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg.
        lnh_ngrm(key_mreg,val_mreg,st_mregstr,key) Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg.
        lnh_nsmm(key_mreg,val_mreg,st_mregstr,key) Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном беззнаковом порядке на ключах. Запись результата в регистр st_mreg.
        lnh_get_firstm(key_mreg,val_mreg,st_mregstr) Поиск следующего ключа, большего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах. Запись результата в регистр st_mreg.
        lnh_get_lastm(key_mreg,val_mreg,st_mregstr) Поиск следующего ключа, меньшего значению key в структуре str (ключ key может отсутствовать в структуре). Поиск выполняется в целочисленном знаковом порядке на ключах. Запись результата в регистр st_mreg.
        lnh_get_m(mreg) Чтение регистра Mailbox Запись результата в регистр st_mreg.
Асинхронные функции Leonhard API без записи результатов Функции для вызова команд DISC без ожидания их завершения. Записью результатов выполнения команд не производится
        lnh_ins_async(str,key,value) Вставка ключа key и значения value в структуру str.
        lnh_del_async(str,key) Удаление записи с ключом key из структуры str.
        lnh_del_str_async(str) Удаление структуры str.
        lnh_sq_async(str) Сжатие структуры в памяти lnh64 (сокращение занимаемого объема памяти).
        lnh_or_async(A,B,R) Объединение множеств ключей структуры A и B. Помещение результирующей структуры в R.
        lnh_and_async(A,B,R) Пересечение множеств ключей структуры A и B. Помещение результирующей структуры в R.
        lnh_not_async(A,B,R) Дополнение множеств ключей структуры A ключами структуры B. Помещение результирующей структуры в R.
        lnh_lseq_async(key,A,R) Срез структуры A по условию “меньше или равно” ключа key. Помещение результирующей структуры в R (все ключи, не соответствующие условию lseq в структуре R отсуствуют).
        lnh_ls_async(key,A,R) Срез структуры A по условию “меньше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию ls в структуре R отсуствуют).
        lnh_greq_async(key,A,R) Срез структуры A по условию “больше или равно” ключа key. Помещение результирующей структуры в R.(все ключи, не соответствующие условию greq в структуре R отсуствуют).
        lnh_gr_async(key,A,R) Срез структуры A по условию “больше” ключа key. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr в структуре R отсуствуют).
        lnh_grls_async(key_ls,key_gr,A,R) Срез структуры A по условию “больше” ключа key_ls и “меньше” ключа key_gr. Помещение результирующей структуры в R. (все ключи, не соответствующие условию gr и ls в структуре R отсуствуют).

Полный перечень функций вызовов команд DISC вы можете посмотреть в файле lnh64.h.


//==================================
// Функции для работы с Leonhard API
//==================================

void                            lnh_hw_reset();
void                            lnh_sw_reset();
void                            lnh_init();
uint64_t                        lnh_rd_reg64(int adr);
uint32_t                        lnh_rd_reg32(int adr);
void                            lnh_fast_recall(uint32_t key);
void                            lnh_fast_recall(uint32_t key, uint32_t value);
void                            lnh_fast_recall(uint64_t key);
void                            lnh_fast_recall(uint64_t key, uint64_t value);

//================================
// Сервисные функции  Leonhard API
//================================

uint32_t                        float2uint(float value);
float                           uint2float(uint32_t value);
uint64_t                        double2ull(double value);
double                          ull2double(uint64_t value);
void                            lnh_sync();
void                            lnh_syncm(int mbr);

//================================
// Синхронные функции Leonhard API
//================================

bool                            lnh_ins_sync(uint64_t str, uint64_t key, uint64_t value);
bool                            lnh_del_sync(uint64_t str, uint64_t key);
uint32_t                        lnh_get_num(uint64_t str);
bool                            lnh_del_str_sync(uint64_t str);
bool                            lnh_sq_sync(uint64_t str);
bool                            lnh_or_sync(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_and_sync(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_not_sync(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_lseq_sync(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_ls_sync(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_greq_sync(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_gr_sync(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_grls_sync(uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);
bool                            lnh_search(uint64_t str, uint64_t key);
bool                            lnh_next(uint64_t str, uint64_t key);
bool                            lnh_prev(uint64_t str, uint64_t key);
bool                            lnh_nsm(uint64_t str, uint64_t key);
bool                            lnh_ngr(uint64_t str, uint64_t key);
bool                            lnh_nsm_signed(uint64_t str, long long int key);
bool                            lnh_ngr_signed(uint64_t str, long long int key);
bool                            lnh_get_first(uint64_t str);
bool                            lnh_get_last(uint64_t str);
bool                            lnh_get_first_signed(uint64_t str);
bool                            lnh_get_last_signed(uint64_t str);


//================================================================
// Синхронные функции Leonhard API с записью в очередь результатов
//================================================================

bool                            lnh_ins_syncq(uint64_t str, uint64_t key, uint64_t value);
bool                            lnh_del_syncq(uint64_t str, uint64_t key);
uint32_t                        lnh_get_numq(uint64_t str);
bool                            lnh_del_str_syncq(uint64_t str);
bool                            lnh_sq_syncq(uint64_t str);
bool                            lnh_or_syncq(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_and_syncq(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_not_syncq(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_lseq_syncq(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_ls_syncq(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_greq_syncq(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_gr_syncq(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_grls_syncq(uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);
bool                            lnh_searchq(uint64_t str, uint64_t key);
bool                            lnh_nextq(uint64_t str, uint64_t key);
bool                            lnh_prevq(uint64_t str, uint64_t key);
bool                            lnh_nsmq(uint64_t str, uint64_t key);
bool                            lnh_ngrq(uint64_t str, uint64_t key);
bool                            lnh_get_firstq(uint64_t str);
bool                            lnh_get_lastq(uint64_t str);
bool                            lnh_get_q();


//=================================================================
// Асинхронные функции Leonhard API с записью в асинхронный Mailbox
//=================================================================

bool                            lnh_ins_syncm(int st_mreg, uint64_t str, uint64_t key, uint64_t value);
bool                            lnh_del_syncm(int st_mreg, uint64_t str, uint64_t key);
uint32_t                        lnh_get_numm(uint64_t str);
bool                            lnh_del_str_syncm(int st_mreg, uint64_t str);
bool                            lnh_sq_syncm(int st_mreg, uint64_t str);
bool                            lnh_or_syncm(int st_mreg, uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_and_syncm(int st_mreg, uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_not_syncm(int st_mreg, uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_lseq_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_ls_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_greq_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_gr_syncm(int st_mreg, uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_grls_syncm(int st_mreg, uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);
bool                            lnh_searchm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool                            lnh_nextm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool                            lnh_prevm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool                            lnh_nsmm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool                            lnh_ngrm(int key_mreg, int val_mreg, int st_mreg, uint64_t str, uint64_t key);
bool                            lnh_get_firstm(int key_mreg, int val_mreg, int st_mreg, uint64_t str);
bool                            lnh_get_lastm(int key_mreg, int val_mreg, int st_mreg, uint64_t str);
uint64_t                        lnh_get_m(int mreg);


//========================================================
// Асинхронные функции Leonhard API без записи результатов
//========================================================
bool                            lnh_ins_async(uint64_t str, uint64_t key, uint64_t value);
bool                            lnh_del_async(uint64_t str, uint64_t key);
bool                            lnh_del_str_async(uint64_t str);
bool                            lnh_sq_async(uint64_t str);
bool                            lnh_or_async(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_and_async(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_not_async(uint64_t A, uint64_t B, uint64_t R);
bool                            lnh_lseq_async(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_ls_async(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_greq_async(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_gr_async(uint64_t key, uint64_t A, uint64_t R);
bool                            lnh_grls_async(uint64_t key_ls, uint64_t key_gr, uint64_t A, uint64_t R);

2.6.3. Представление структур данных в виде ключей и значений

SPE lnh64 использует беззнаковое сравнение 64 битных ключей для формирования упорядоченной структуры B+дерева. Это позволяет выполнять большинство операций набора команд DISC за O(log8n) операций доступа к памяти.

Таким образом, чтобы реализовать хранение информации в SPE неободимо представить информацию в виде беззнаковых целых чисел.

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

В библиотеке lnh64 l0 реализованы шаблоны для работы со структурами составных ключей. Обязательным требованием к ним является общий размер, который должен быть равен 64 разрядам как для ключа, так и для значения.

Ниже представлен пример объявления структур композитного ключа и композитного значения:

 //Структура данных
 #define 		A 	1 	//Структура A

 //Структура A - ключ
	/* 
	 * key[63..32] -  Поле 0 - Идентификатор (id)
	 * key[31..0]  -  Поле 1 - Порядковый номер (index)
	 */
 STRUCT(A_key)
 {
        uint32_t	index:32;	//Поле 0: 
        uint32_t	id   :32; 	//Поле 1: 
 };

//Структура A - значение
	/* 
	 * value[63..32] -  Поле 0 - Атрибут 0 
	 * value[31..24] -  Поле 1 - Атрибут 1 
	 * value[23..8]  -  Поле 2 - Атрибут 2 
	 * value[7..0]   -  Поле 3 - Атрибут 3
	 */
 STRUCT(A_value)
 {
 		// Поля объявляются в обратной последовательности (старший байт расположен по старшему адресу)
        uint8_t      atr3   :8;  //Поле 3: Атрибут 3
        uint16_t     atr2   :16; //Поле 2: Атрибут 2
        uint8_t      atr1   :8;  //Поле 1: Атрибут 1
        uint32_t     atr0   :32; //Поле 0: Атрибут 0
 };

2.7. Программная модель микропроцессора Леонард Эйлер

Программная модель микропроцессора Леонард Эйлер представляет собой совокупность программно-доступных ресурсов в следующих адресных пространствах:

  • Адресное пространство хост-подсистемы: 64 битное пространство, в которое отображены ресурсы подсистемы обработки графов (за исключением ресурсов SPE), ресурсы хост-подсистемы (ОЗУ, дисковые устройства), хост-подсистемы, подсистема хранения графов, подсистемы сетевого взаимодействия узлов обработки графов.
  • Адресное пространство ядер обработки графов: 64 битное пространство доступа к ресурсам SPE.

Таким образом, распределение программно-доступных ресурсов по двум адресным пространствам соответствует их назначению: программа, функционирущая в хост-подсистеме (включая программный драйвер) выполняет управление системными процессами в узле; программы в гетерогенных ядрах обработки графов выполняет управление процессорным элементом обработки структур SPE.

2.7.1. Ресурсы микропроцессора Леонард Эйлер в адресном пространстве хост-подсистемы

Микропроцессор Леонард Эйлер реализован в виде карты расширения, подключаемой к узлу через шин PCIe v3 x16. Все программно-доступные ресурсы отобразаются в адресное пространство через регистры базового адреса. В текущей версии используются регистры BAR0 (64 битный доступ), BAR2 (64 битный доступ) и BAR4 (32 битный доступ) В таблицах 10-14 перечислены программно доступные ресурсы.

Таблица 10 - Программно доступные регистры Леонард Эйлер для хост-подсистемы (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)

Название                Смещение     Режим      Бит   Описание
1. BAR0       В пространство отображены регистры гетерогенных ядер GPC
Регион GPC# 0x#00..3F     Регион регистров GPC номер # (всего 24 региона) (64 байта)
CONTROL 0x#00     Регистр управления GPC
STATUS 0x#08     Регистр статуса GPC#
RV_CONFIG 0x#10 W   Регистр номера обработчика. При записи вырабатывается сигнал ap_start, читаемый со сторны GPC#
HANDLER_PAYLOAD 0x#18 R   Регистр данных пользовательского прерывания. Устанавливается программно программным кодом GPC#
C2H_PIDX_ADDR 0x#20 W   Регистр адреса поля Card2Host PIDX (продьюсер индекс) для уведомлении хоста об индексе последнего записанного слова
C2H_PIDX 0x#28 W   Регистр Host2Card CIDX (консьюмер индекс) для уведомлении карты об индексе последнего записанного слова
LNH_NFO_ADR 0x#30 W   Регистр адреса слова состояния lnh64
LNH_NFO_DATA 0x#30 R   Регистр слова состояния lnh64
SYS_CONTROL 0x600      
SYS_STATUS 0x608      
2.BAR2       В пространство отображены блоки программно-доступной памяти
GPC#_MSG_REGION 0x#000.. FFF R/W   Память для передачи блоков данных между Хост-подсистемой и GPC# (4КБайт)
BIN_MEMORY 0x20000.. 30000 R/W   Память конфигурирования GPC#. Память используется для передачи в GPC# программных ядер sw_kernel
3.BAR4       В пространство отображена таблица векторов MSI-X
MSIX_TABLE 0x000.. FFF R/W   Таблица векторов MSI-X: 49 векторов

Таблица 11 - Назначение бит регистра управления гетерогенного ядра (SC = Self Clear, COR = Clear on Read)

Название                Режим      Бит   Описание
ap_start R/W/COR 0 Бит запуска хэндлера. Устанавливается программно или автоматически по записи в регистр GPC_RV_CONFIG
auto_restart R/W 1 Бит автоматического повторного перезапуска хэндлера. При auto_restart=1 после завершения работы хэндлера происходит его повторный запуск
mq_enable R/W 2 Бит разрешения работы очередей MQ для GPC#
done_ie R/W 3 Бит разрешения прерывания по переходу в состояние IDLE
user _ie R/W 4 Бит разрешения пользовательского прерывания от GPC#
gpc_reset W/SC 5 Бит сброса GPC#. При этом также происходит сброс ядра lnh64
mq_h2c_reset W/SC 6 Бит сброса очереди Host2Card ядра GPC#
mq_c2h_reset W/SC 7 Бит сброса очереди Card2Host ядра GPC#
data_partition R/W 10..8 Номер партиции данных для ядра lnh64
index_partition R/W 13..11 Номер индексной партиции для ядра lnh64
index_region R/W 15..14 Номер индексного региона для ядра lnh64

Таблица 12 - Назначение бит регистра статуса гетерогенного ядра (COR = Clear on Read)

Название                Режим      Бит   Описание
ap_idle R 0 Бит состояния GPC#. 0 - BUSY/Занят; 1 - IDLE/Свободен
ap_done R/COR 1 Бит выдачи сигнала DONE, устанавливается при переходе из состояние BUSY в IDLE

Таблица 13 - Назначение бит регистра управления микропроцессора (SC = Self Clear)

Название                Режим      Бит   Описание
Global reset W/SC 0 Бит сброса микропроцессора (подлежат сбросу все GPC, включая ядра risv64im и lnh64)
H2CQ_reset W/SC 1 Бит сброса очереди Host2Card
C2HQ_reset W/SC 2 Бит сброса очереди Card2Host
MSIX_ie W 3 Бит глобального разрешения прерываний MSI-X

Таблица 14 - Назначение бит регистра статуса микропроцессора

Название                Режим      Бит   Описание
System Ready R 0 Микропроцессор инициализирован и готов к работа
H2C Queue Empty R 1 Очередь Host2Card пуста
H2C Queue AEmpty R 2 Очередь Host2Card почти пуста
H2C Queue Full R 3 Очередь Host2Card заполнена
H2C Queue AFull R 4 Очередь Host2Card почти заполнена
C2H Queue Empty R 5 Очередь Card2Host пуста
C2H Queue AEmpty R 6 Очередь Card2Host почти пуста
C2H Queue Full R 7 Очередь Card2Host заполнена
C2H Queue AFull R 8 Очередь Card2Host почти заполнена
System revision R 63..32 Версия микропроцессора Леонард Эйлер

2.7.2. Ресурсы микропроцессора Леонард Эйлер в адресном пространстве CPE

Микропроцессорное ядро CPE riscv64im обладает 64х разрядным адресным пространством, в котором отображены следующие ресурсы (смотри таблицу 15):

Таблица 15 - Программно доступные регистры Леонард Эйлер для CPE (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)

Название                  Смещение           Режим          Бит   Описание
BOOTLOADER 0x00000000 R   ПЗУ, в котором записан программный загрузчик (256 байт)
RAM 0x100000 R/W   Оперативная память 64 КБайт
AXI4 0x200000 R/W   Пространство локальной шины AXI4
GPC_START AXI4+0x00     Регистр управления GPC
ap_start   R/W/COR 0 Бит запуска хэнлера. Устанавливается программно или автоматически по записи в регистр GPC_RV_CONFIG
GPC_STATUS AXI4+0x08     Регистр статуса GPC
ap_idle   W 0 Бит состояния GPC. 0 - BUSY/Занят; 1 - IDLE/Свободен
RV_CONFIG AXI4+0x10 R   Регистр номера обработчика. Данные валидны только при GPC_START.ap_start=1
HANDLER_PAYLOAD AXI4+0x18 W   Регистр данных пользовательского прерывания. Прерывание вырабатывается автоматически при каждой записи регистра.
MSG_REGION AXI4+0x1000.. 1FFF R/W   Память для передачи блоков данных между Хост-подсистемой и GPC# (4КБайт)
BIN_MEMORY AXI4+0x10000.. 20000 R/W   Память конфигурирования GPC. Память используется для передачи в GPC программных ядер sw_kernel
LNH64 0x300000     Пространство локальной шины AXL для подключения ядра lnh64
AXIS 0x400000 R/W   Пространство локальной шины AXI STREAM для связи с очередью C2H и DMA с хост-подсистемой
C2H AXIS+0x00 W   Регистр данных очереди Card2Host. Запись данных в очередь C2H происходит при каждой транзакции записи. Немедленная отправка не гарантируется.
C2H_FLUSH AXIS+0x08 W   Регистр запроса на ускоренную отправку данных в хост систему.

3. Практакум №1. Разработка и отладка программ в вычислительном комплексе Тераграф

Практикум посвящен освоению принципов работы вычислительного комплекса Тераграф и получению практических навыков решения задач обработки множеств на основе гетерогенной вычислительной структуры. В ходе практикума необходимо ознакомиться с типовой структурой двух взаимодействующих программ: хост-подсистемы и программного ядра sw_kernel. Для выполнения практикума предоставляется доступ к облачной платформе devlab.bmstu.ru с установленными ускорительными картами микропроцессора Леонард Эйлер и настроенными средствами сборки проектов.

3.1. Пример взаимодествия устройств: система определения ролей.

Рассмотрим следующие примеры кода подсистемы и программного ядра, которые мы будем использовать в практикуме. Система определения ролей пользователя сохраняет в памяти микропроцессора DISC тестовый набор ролей пользователей (1К пользователей с 1K ролями каждого). Далее система отвечает на запросы вида: какие роли пользователя были использованы начиная с момента времени time. Рассматриваемый пример выполняет следующие действия:

  • Хост подсистема инициализирует gpc программным ядром sw_kernel.rawbinary.
    gpc64_inst = new gpc();
	log<<"Открывается доступ к "<<gpc64_inst->gpc_dev_path<<endl;
	if (gpc64_inst->load_swk(argv[1])==0) {
		log<<"Программное ядро загружено из файла "<<argv[1]<<endl;
	}
	else {
		log<<"Ошибка загрузки sw_kernel файла << argv[1]"<<endl;
		return -1;
	}
  • Если программное ядро успешно загружено, хост подсистема запускает в gpc обработчик update, выполняющий прием и запись ключей и значений в SPE (ins_async). Код обработчика, функцуионирующего в sw_kernel представлен ниже:
//-------------------------------------------------------------
//      Вставка ключа и значения в структуру
//-------------------------------------------------------------

void update() {

        while(1){
                users::key key=users::key::from_int(mq_receive());
                if (key==-1ull) break;
                users::val val=users::val::from_int(mq_receive());
                // Поля структуры могут записываться явно следующим образом 
                //      auto new_key = users::key{.rec_idx=1,.user=2};
                //      auto new_val = users::val{.role=3,.lst_time=0}
                // Копирование полей в переменные можно выполнить следующим образом:
                //      auto user = key.user;
                //      auto [lst_time,role] = val;
                USERS.ins_async(key,val); //Вставка в таблицу с типизацией uint64_t
        } 
}
  • Далее хост-подсистема инициализирует поток сообщений к программному ядру. Для этого могут быть использованы два способа:
  1. Последовательная пересылка ключей и значений unsigned long long короткими сообщениями.
     for (uint32_t user=0;user<TEST_USER_COUNT;user++) {
         for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) {
             gpc64_inst->mq_send(users::key{.idx=idx,.user=user}); //запись о роли #idx
             gpc64_inst->mq_send(users::val{.role=idx,.time=time_t(0)}); //роль и время доступа
         }
     }
    
  2. Заполнение буфера данных и передача его драйверу (блочная передача). Данный способ обеспечивает большую пропускную способность передачи, так как реализуется через механизм прямого доступа к памяти. Передача данных из буфера выполняется в асинхронном режиме (процесс запускается по команде mq_send). Для ожидания момента завершения передачи метод mq_send возвращает указатель на поток передачи. Далее, если требуется ожидание завершения процесса передачи, необходимо использовать синхронизирующую команду join (send_buf_th->join()). Пример кода блочной передачи приведен ниже:
     unsigned long long *buf = (unsigned long long*)malloc(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2);
     for (uint32_t user=0,offs=0;user<TEST_USER_COUNT;user++) {
         for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) {
             buf[offs]=users::key{.idx=idx,.user=user};
             buf[offs+1]=users::val{.role=idx,.time=time_t(idx*3600)};
         }
     }
     auto send_buf_th = gpc64_inst->mq_send(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2,(char*)buf);
     send_buf_th->join();
     free(buf);
    
  • По завершению передачи посылается терминальный символ (0xffffffffffffffff):
	//Терминальный символ
	gpc64_inst->mq_send(-1ull);
  • В ответ на терминальный символ sw_kernel завершает обработчик update, и код хост-подсистемы запускает обработчик запросов поиска select.
  • Система готова к приему запросов пользователя. Формат таблицы, представленной в SPE микропроцессоре следующий common.sh
	//Запись для формирования ключей (* - наиболее значимые биты поля)
	STRUCT(key)
	{
	    uint32_t	idx	    :idx_bits;	//Поле 0:
	    uint32_t	user    :32; 		//Поле 1*
	};

	//Запись для формирования значений
	STRUCT(val)
	{
	    uint32_t	role	:32;		//Поле 0:
	    time_t		time    :32; 		//Поле 1*
	};

Поле ключа состоит из полей: user (поле идентификатора пользователя, старшая часть ключа) и idx (поле индекса записи о роли пользователя, младшая часть ключа). Поле значения состоит из полей: role (поле идентификатора роли) и time (поле времени последнего доступа).

  • Запрос состоит в выборе тех ролей пользователя из таблицы users, которые были использованы позднее момента времени time, заданного в запросе. Например:
select role from users where user=5 and time>100000;
  • Программный код хост-системы использует регулярные выражения (regex) для выделения полей в запросе select.

  • Поля запроса user и time передаются в sw_kernel:

        gpc64_inst->mq_send(stoi(match_query1[4])); //пользователь
        gpc64_inst->mq_send(stoi(match_query1[6])); //время доступа
  • Микропроцессор DISC выбирает из ассоциативной памяти все роли пользователя user и определяет те из них, которые соответствуют условию запроса (например, time>100000). Найденные роли передаются в хост-подсистему.

  • В итоге, хост подсистема выдает сообщение о результатах поиска в поток cout.

Полный код приложения для хост-подсистемы показан ниже:


#define TEST_USER_COUNT 1000
#define TEST_ROLE_COUNT 1000

int main(int argc, char** argv)
{
	ofstream log("lab2.log"); //поток вывода сообщений
	unsigned long long offs=0ull;
	gpc *gpc64_inst; //указатель на класс gpc
	regex select_regex_query("select +(.*?) +from +(.*?) +where +(.*?)=(.*?) +and +(.*?)>(.*);", //запрос
            std::regex_constants::ECMAScript | std::regex_constants::icase);

	//Инициализация gpc
	if (argc<2) {
		log<<"Использование: host_main <путь к файлу rawbinary>"<<endl;
		return -1;
	}

	//Захват ядра gpc и запись sw_kernel
	gpc64_inst = new gpc();
	log<<"Открывается доступ к "<<gpc64_inst->gpc_dev_path<<endl;
	if (gpc64_inst->load_swk(argv[1])==0) {
		log<<"Программное ядро загружено из файла "<<argv[1]<<endl;
	}
	else {
		log<<"Ошибка загрузки sw_kernel файла << argv[1]"<<endl;
		return -1;
	}

	//Инициализация таблицы для вложенного запроса
	gpc64_inst->start(__event__(update)); //обработчик вставки 

	//1-й вариант: пересылка коротких сообщений
	for (uint32_t user=0;user<TEST_USER_COUNT;user++) {
		for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) {
			gpc64_inst->mq_send(users::key{.idx=idx,.user=user}); //запись о роли #idx
			gpc64_inst->mq_send(users::val{.role=idx,.time=time_t(0)}); //роль и время доступа
		}
	}

	//2-й вариант: блочная передача
	unsigned long long *buf = (unsigned long long*)malloc(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2);
	for (uint32_t user=0,offs=0;user<TEST_USER_COUNT;user++) {
		for (uint32_t idx=0;idx<TEST_ROLE_COUNT;idx++,offs+=2) {
			buf[offs]=users::key{.idx=idx,.user=user};
			buf[offs+1]=users::val{.role=idx,.time=time_t(idx*3600)};
		}
	}
	auto send_buf_th = gpc64_inst->mq_send(sizeof(unsigned long long)*TEST_USER_COUNT*TEST_ROLE_COUNT*2,(char*)buf);
	send_buf_th->join();
	free(buf);
	//Терминальный символ
	gpc64_inst->mq_send(-1ull);

	gpc64_inst->start(__event__(select)); //обработчик запроса поиска 
	while(1) {
		string query1;
		//разбор полей запроса
		smatch match_query1;
		getline(cin, query1);
		log<<"Введен запрос: "<<query1<<endl;
		if (!query1.compare("exit")) {
			gpc64_inst->mq_send(-1ull);
			break;
		}
		if (regex_match (query1, match_query1, select_regex_query) && 
			match_query1[3]=="user" && 
			match_query1[5] == "time") {
			//match_query1[1] - возвращаемое поле запроса
			//match_query1[2] - номер структуры запроса 
			//match_query1[3] - поле поиска 1
			//match_query1[4] - значение поля поиска 1
			//match_query1[5] - поле поиска 2
			//match_query1[6] - значение поля поиска 2
			log << "Запрос принят в обработку." << endl;
			log << "Поиск ролей пользователя " << match_query1[4] << "и time > " << time_t(stoi(match_query1[6])) << endl;
			gpc64_inst->mq_send(stoi(match_query1[4])); //пользователь
			gpc64_inst->mq_send(stoi(match_query1[6])); //время доступа
			while (1) {
				uint64_t result = gpc64_inst->mq_receive();
				if (result!=-1ull) {
					cout << "Роль: " << users::val::from_int(result).role << " - ";
					cout << "Время доступа: " << users::val::from_int(result).time << endl;
				} else {
					break;
				}
			}
		} else {
      		log << "Ошибка в запросе!" << endl;
		}
	}
	log << "Выход!" << endl;
	return 0;
}

Код соответствующего кода sw_kernel представлен ниже.

extern lnh lnh_core;
volatile unsigned int event_source;

int main(void) {
    /////////////////////////////////////////////////////////
    //                  Main Event Loop
    /////////////////////////////////////////////////////////
    //Leonhard driver structure should be initialised
    lnh_init();
    for (;;) {
        //Wait for event
        event_source = wait_event();
        set_gpc_state(BUSY);
        switch(event_source) {
            /////////////////////////////////////////////
            //  Measure GPN operation frequency
            /////////////////////////////////////////////
            case __event__(update) : update(); break;
            case __event__(select) : select(); break;
        }
        set_gpc_state(IDLE);
    }
}
    
//-------------------------------------------------------------
//      Вставка ключа и значения в структуру
//-------------------------------------------------------------
 
void update() {

        while(1){
                users::key key=users::key::from_int(mq_receive());
                if (key==-1ull) break;
                users::val val=users::val::from_int(mq_receive());
                // Поля структуры могут записываться явно следующим образом 
                //      auto new_key = users::key{.rec_idx=1,.user=2};
                //      auto new_val = users::val{.role=3,.lst_time=0}
                // Копирование полей в переменные можно выполнить следующим образом:
                //      auto user = key.user;
                //      auto [lst_time,role] = val;
                USERS.ins_async(key,val); //Вставка в таблицу с типизацией uint64_t
        } 
}


//-------------------------------------------------------------
//      Передать все роли пользователя и время доступа 
//-------------------------------------------------------------
 
void select() {
        while(1){
                uint32_t quser = mq_receive();
                if (quser==-1) break;
                uint32_t qtime = mq_receive();
                //Найдем все роли пользователя и последнее время доступа:
                // Результаты поиска могут быть доступны следующим образом:
                //      auto user = USERS.search(users::key{.idx=1,.user=2}).key().user;
                //      auto role = USERS.search(users::key{.idx=3,.user=4}).value().role;

                //Вариант 1 - обход записей пользователя явным образом
                auto crole = USERS.nsm(users::key{.idx=users::idx_min,.user=quser});
                while (crole && crole.key().user==quser) {
                        if (crole.value().time>qtime) mq_send(crole.value());  
                        crole = USERS.nsm(crole.key());
                } 

                //Вариант 2 - использование итератора
                for (users::val val : role_range(USERS,quser)) {
                        if (val.time>qtime) mq_send(val);
                }
                mq_send_flush(-1ull);
        } 
}

3.2. Подключение к облачной платформе Тераграф Cloud

Для подключение к облачной платформе необходимо получить у организаторов практикума логин и начальный пароль.

Далее необходимо выполнить подключение по протоколу ssh и сменить разовый пароль. Выполнить это можно с помощью терминальных программ, поддерживающих протокол SSH:

  • ОС Windows - gitbash, putty.

  • В ОС Linux и MacOS - ssh клиент доступен в терминальном режиме в консоли.

Подключение в консоли выполняется при помощи следующей команды:

ssh username@195.19.32.95

где: username - имя пользователя, выдается организаторами практикума каждому участнику.

Обратите внимание, что при троекратном введении неверного пароля аккаунт пользователя будет заблокирован на 2 часа. Рекомендуется прописать ключ пользователя для доступа к серверу с помощью команды ssh-copy-id username@195.19.32.95, после чего вход на сервер будет осуществляться без ввода пароля.

На сервере установлены все необходимые библиотеки, средства для сборки и отладки проекта:

  • набор средств сборки riscv toolchain и экспорт исполняемых файлов в PATH

  • набор библиотек picolib и экспорт в C_INCLUDE_PATH

  • библиотека gpc64io на языке python для для работы с микропроцессором Леонард Эйлер.

Облачная платформа доступна по ссылке:

Облачная платформа devlab.bmstu.ru

Рисунок 12 - Облачная платформа devlab.bmstu.ru

Облачная платформа построена на основе открытого программного решения JupyterHub и VSCode, и предоставляет следующие функциональные возможности:

  • Для работы с приложениями, написанными на языке Python (для 2-й части практикума), или для работы в консоли ssh может быть использована среда JupyterHub.

Среда разработки и отладки программ JupyterHub

Рисунок 13 - Среда разработки и отладки программ JupyterHub

  • Для разработки и отладки программ, написанных на языке C/C++, а также других языках (Python, Go и пр.) может быть испольхована среда VSCode.

Среда разработки и отладки программ VSCode

Рисунок 14 - Среда разработки и отладки программ VSCode

3.3. Утилита для мониторинга состояния гетерогенных ядер обработки графов

Для мониторинга состояния ядер модет быть использована утлита lnh_nfo, установленная в системе devlab. Вызов системы выполняется следующим образом:

lnh_nfo -t/j [-l0,6,..]

Например, при вызове без параметров будет выдана информация обо всех ядрах gpc в виде таблицы:

user@dl580:~$ lnh_nfo
Утилита для мониторинга состояния гетерогенных ядер обработки графов.
Использование:  lnh_nfo -t/j [-l0,6,..]
-t - вывод в виде таблицы (по умолчанию); -j - вывод в виде json; -l - список ядер gpc (номера через запятую)
Условные обозначения:  <ключи> - количество ключей в структурах 1..7;  <атрибуты> - состояние b+поддеревьев 0..7;
<O> - флаг переполнения поддерева; <E> - флаг пустого поддерева; <S> - номер структуры, занимающей поддерево;
<busy> - ядро выполняет обработчик; <rdy> - ядро ожидает запуска обработчика; * - символическое устройство открыто
╔══════╤══════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╗
║ ядро │ параметр │     #0      │     #1      │     #2      │     #3      │     #4      │     #5      │     #6      │     #7      ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║ *  0 │ ключи    │           - │     1000000 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ busy │ атрибуты │ O=0 E=1 S=1 │ O=0 E=0 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║    1 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ rdy  │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║    2 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║    3 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║    6 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║    7 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║   12 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║   13 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ busy │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║   18 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║   19 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║   20 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║   21 │ ключи    │           - │           0 │           0 │           0 │           0 │           0 │           0 │           0 ║
║ init │ атрибуты │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╚══════╧══════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╝
  • Параметр -t/j позволяет выбрать формат вывода информации: -t - табибличный вариант; -j - json формат.
  • Необязательный параметр -l позволяет выбрать номера ядер, включаемые в вывод. Например -l0,1,2,3 сформирует вывод только для gpc0..gpc3
  • Для каждого ядра gpc выводится состояние физического и логического представления структур SPE (микропроцессора с набором команд дискретной математики).
  • В строке <ключи> выдается информация о логических структурах SPE (как было сказано ранее, доступно 7 структур с номерами 1..7). При решении прикладных задач, программист должен самостоятельно определить в sw_kernel номера структур и использовать их. Например, с помощью команды `lnh_ins_async(1,key,val)` будет выполнена вставка в логическую структуру 1), после чего в таблице можно наблюдать изменение поля <ключи> в столбце #1.
  • В строке <атрибуты> представлено текущее состояние физических структур (B+ поддеревьев в локальной памяти SPE). Всего доступно 8 поддеревьев (0..7), на которые могут быть распределены логические структуры. Номер логической структуры, занимающей B+ поддерево указано в атрибуте S (при этом S=0 означает, что поддерево не занято). Атрибут O указывает на факт полного заполнения поддерева (O=1 - дерево заполнено). Флаг E указывает на то, что поддерево не содержит уключей (E=1 - поддерево пусто).
  • В столбце <ядро> указывается состояние гетерогенного ядра обработки графов. Сивол `*` означает, что символическое устройство ядра открыто. Ниже указывается текущее состояние, в котором находится CPE: - начальное состояние CPE после включения питания или ресета; - CPE выполняет обработчик; - CPE ожидает запуска обработчика;

3.4. Запуск демо проекта 1

Пример демонстрирует основные механизмы инициализации гетерогенных ядер gpc и взаимодействие хост-подсистем с Graph Processor Core, используются аппаратные очереди.

Для установки требуется рекурсивно клонировать репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab1/lab1.git
cd lab1

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

Для сборки проекта следует выполнить команду:

make

Результатом выполнения команды станет файлы host_main, sw_kernel_main.rawbinary в директориях ./host/ и ./sw_kernel/.

Для запуска проекта

Параметры запуска проекта:

host_main <путь к="" файлу="" sw_kernel="">

Например:

host/host_main sw-kernel/sw_kernel.rawbinary

Результат работы теста:

Open gpc on /dev/gpc1
Rawbinary loaded from ../sw-kernel/sw_kernel.rawbinary
sw_kernel version: 0x20232109
Leonhard clock frequency (LNH_CF) 190.091429 MHz
Test done

Очистка проекта выполняется следующим образом:

make clean

3.5. Запуск демо проекта 2

Программа демонстрирует принципы взаимодействия устройств в системе и примеры хранения и обработки множеств в микропроцессоре Lnh64.

Для установки требуется рекурсивно клонировать репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab2/lab2.git
cd lab2

Для сборки проекта следует выполнить команду:

make

Результатом выполнения команды станет файлы host_main, sw_kernel_main.rawbinary в директориях ./host/ и ./sw_kernel/.

Для запуска проекта

Параметры запуска проекта:

host_main <путь к="" файлу="" sw_kernel="">

Например:

host/host_main sw-kernel/sw_kernel.rawbinary

Далее необходимо ввести запрос (например: select role from users where user=5 and time>7200;) или exit для выхода.

Результат работы теста:

...
Роль: 10 - Время доступа: 36000
Роль: 9 - Время доступа: 32400
Роль: 8 - Время доступа: 28800
Роль: 7 - Время доступа: 25200
Роль: 6 - Время доступа: 21600
Роль: 5 - Время доступа: 18000
Роль: 4 - Время доступа: 14400
Роль: 3 - Время доступа: 10800

Одновременно с выводом результата в поток stdout, приложение выводит лог работы в файл lab2.log. Просмотр лога возможен по команде:

tail -f lab2.log

Очистка проекта выполняется следующим образом:

make clean

3.6. Запуск демо проекта 3

Пример демонстрирует основные механизмы взаимодействия микропроцессоров CPE (resv64) и SPE (lnh64), и выполняет тестирование корректности команд DISC.

Для установки требуется рекурсивно клонировать репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab3.git
cd lab3

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

Для сборки проекта следует выполнить команду:

make

Результатом выполнения команды станет файлы host_main, sw_kernel_main.rawbinary в директориях ./host/ и ./sw_kernel/.

Для запуска проекта

Параметры запуска проекта:

host_main <путь к="" файлу="" sw_kernel="">

Например:

host/host_main sw-kernel/sw_kernel.rawbinary

На ядрах gpc начнется процесс тестирования:

Утилита для мониторинга состояния гетерогенных ядер обработки графов.
Использование:  lnh_nfo -t/j [-l0,6,..]
-t - вывод в виде таблицы (по умолчанию); -j - вывод в виде json; -l - список ядер gpc (номера через запятую)
Условные обозначения:  <ключи> - количество ключей в структурах 1..7;  <атрибуты> - состояние b+поддеревьев 0..7;
<O> - флаг переполнения поддерева; <E> - флаг пустого поддерева; <S> - номер структуры, занимающей поддерево;
<busy> - ядро выполняет обработчик; <rdy> - ядро ожидает запуска обработчика; * - символическое устройство открыто
╔═══════╤══════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╤═════════════╗
║ ядро# │ параметр │     #0      │     #1      │     #2      │     #3      │     #4      │     #5      │     #6      │     #7      ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  *  0 │ ключи    │           - │    48064052 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  *  1 │ ключи    │           - │    47589696 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  *  2 │ ключи    │           - │    47224312 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  *  3 │ ключи    │           - │    46900114 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  *  6 │ ключи    │           - │    46758051 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  *  7 │ ключи    │           - │    46407924 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  * 12 │ ключи    │           - │    45959794 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  * 13 │ ключи    │           - │    45607688 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  * 18 │ ключи    │           - │    45081866 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  * 19 │ ключи    │           - │    44720079 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  * 20 │ ключи    │           - │    44367391 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=1 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╠═══════╪══════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╪═════════════╣
║  * 21 │ ключи    │           - │    44017839 │           0 │           0 │           0 │           0 │           0 │           0 ║
║  busy │ атрибуты │ O=1 E=0 S=1 │ O=1 E=0 S=1 │ O=0 E=0 S=1 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 │ O=0 E=1 S=0 ║
╚═══════╧══════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╧═════════════╝

Результат работы тестов будет выведен на консоль:

Ядро /dev/gpc0. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 1 - тест вставки и поиска. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc3. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 2 - тест вставки в заполненную ассоциативную память. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc13. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 3 - тест команд ближайшего меньшего и большего (nsm и ngr). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc6. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc13. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc13. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc7. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc6. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc12. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc7. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc12. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc13. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc0. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc2. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc2. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc3. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc2. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc1. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc0. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc1. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc2. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc3. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 4 - тест вставки и удаления случайных ключей. Ошибок: 0
Ядро /dev/gpc18. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc21. Эксперимент 5 - тест команды NOT (дополнение). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 6 - тест команды AND (пересечение). Ошибок: 0
Ядро /dev/gpc20. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc19. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 7 - тест команды OR (объединение). Ошибок: 0
Ядро /dev/gpc18. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 8 - тест пересечения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc19. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 9 - тест дополнения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc20. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0
Ядро /dev/gpc21. Эксперимент 10 - тест объединения множеств на случайных ключах. Ошибок: 0

Очистка проекта выполняется следующим образом:

make clean

3.7. Индивидуальные задания

Разработать программу для хост-подсистемы и обработчики программного ядра, выполняющие следующие действия:


Вариант 1

Система ролевого доступа. Сформировать в хост-подсистеме и передать в SPE две структуры: USERS и DEVICES. В структуре USERS для каждого пользователя хранятся доступные для него роли (1К id пользователей и 5К id ролей каждого). В структуре DEVICES для каждого устройства хранятся роли, для которых разрешен доступ к устройству (1K id устройств и 5K id ролей каждого). Пользователь вводит id пользователя и id устройства. В ответ на запрос выдается сообщение “Доступ разрешен” или “Доступ запрещен”.


Вариант 2

Система контроля доступа. Сформировать в хост-подсистеме и передать в SPE структуры: RESOURCE_ENABLED и RESOURCE_AVAILABLE. В структуре RESOURCE_ENABLED хранятся id ресурсов, доступ к которому разрешен (10К устройств) и время последнего доступа к ним. В структуре RESOURCE_AVAILABLE хранятся id ресурсов, имеющихся в системе (100К устройств), доступ к которому разрешен. По запросу пользователя выдать те устройства, которые присутствуют в обеих структурах и доступ к которым был ранее заданного момента времени. Для реализации запроса использовать команду пересечения множеств lnh_and_async(INTERSECT_STRUCT,RESOURCE_ENABLED,RESOURCE_AVAILABLE).


Вариант 3

Система распределенного хранения. Сформировать в хост-подсистеме и передать в SPE структуры: STORAGE0 и STORAGE1. В структуре STORAGE0 хранятся id записей в хранилище 0 (10K записей). В структуре STORAGE1 хранятся id записей в хранилище 1 (20K записей). По запросу пользователя выдать однократно записи, которые присутствуют в STORAGE0 или STORAGE1. Для реализации запроса использовать команду объединения множеств lnh_or_async(UNION_STRUCT,STORAGE0,STORAGE1).


Вариант 4

Система ограничения доступа. Сформировать в хост-подсистеме и передать в SPE структуры: USER_ACCESS и URL_DENY. В структуре USER_ACCESS в поле ключа хранятся id пользователей (32 бит) и crc32 текста запроса посещенной пользователем страницы (1К пользователей и 5К crc32). В поле значения хранится время последнего обащения. В структуре URL_DENY хранятся crc32 текста запроса веб страниц, запрещенных к посещению (1M записей). Пользователь передает id пользователя и url запроса в хост-подсистему, которая формирует crc32 посещенной пользователем страницы и запрашивает разрешение на доступ к ней у SPE. Если пользователь не обращался к ней последние 60 секунд и страница не находится в списке запрещенных страниц, выдать сообщение “Страница доступна”. В противном случае выдать сообщение: “Страница не доступна”. Информация о времени доступа обновляется.


Вариант 5

Система логирования данных. Приложение хост-подсистемы принимает запросы двух типов: INSERT и SELECT. Запросы INSERT приводят к вставке в таблицу EVENTS ключей и значений. В ключе сохраняется время записи данных time и тип события type (доступно 256 типов событий). В поле значений хранятся бинарные данные события bin (64 бит). По запросу SELECT type,time,bin from EVENTS where time > x and time < y выбираются и выдаются в консоль поля type,time,bin, произошедшие в промежуток времени (x,y).


Вариант 6

Гео-информационная система. Приложение хост-подсистемы передает в SPE структуры OBJECT_X и OBJECT_Y. В структуре OBJECT_X в поле ключа хранится координата X объекта (64 бит), а в поле значения идентификатор объекта (64 бит). В структуре OBJECT_Y в поле ключа хранится идентификатор объекта (64 бит), а в поле значения координата Y объекта (64 бит). По запросу пользователя выдать все объекты, попадающие в прямоугольную область с координатами вурхнего левого угла (x1,y1) и нижнего правого угла (x2,y2).


Вариант 7

Конвейерная система. Разработать два приложения и выполнить их запуск с использованием конвейерного соединения (linux pipe) с использованием консолькой команды ‘|’. Первое приложение создает в SPE структуру URLS, ключами которой явлются crc64 запроса web страницы (64 бит), а зачениеми являются id файлов (64 бит). В ответ на запрос пользователя, который вводит URL и ключ доступа, первое приложение посылает crc64 введенного URL в первый SPE. В ответ на это SPE выполняет поиск в структуре URL и посылает найденное значение id файла в хост-подсистему. Далее хост-подсистема получает id файла и отправлет его и ключ доступа в поток вывода stdout. Второе приложение в начале работы инициализирует структуру FILES, ключами которой являются id файлов, а в поле значения хранятся атрибуты (время доступа 32 бит, ключ доступа 32 бит). Получив id файла и ключ доступа второе хост-приложение посылает его во второй SPE, который ищет соответствующую запись по id файла, сравнивает ключ доступа с хранимым в структуре FILES. Если ключ совпадает, то второй SPE передает сообщение с временем последнего доступа. Второе хост приложение выдает полученный результат в поток вывода.


Вариант 8

Очередь сообщений. В SPE формируется структура очередей QUEUES, которая содержит до 2^32 очередей и до 2^32 записей в каждой. Ключ структуры QUEUES содержит номер очереди (32 бит) и номер сообщения (32 бит). Пользователь может обратиться к хост-подсистеме с одной из двух команд: запись 64 бит в очередь с номером id, чтение 64 бит из очереди с номером id. Запрос передается в SPE, который выполняет выборку из хвоста очереди и запись в голову очереди. Если очередь пуста, запись начинается с нулевого сообщения.


Вариант 9

Система хранения. Приложение хост-подсистемы формирует в SPE структуру BLOCKS из 1К боков записей, каждый из котороых содержит 1K последовательных 64-битных записей. Ключ структуры содержит номер блока (32 бит) и номер записи блока (32 бит). Поле значения используется для хранения данных. Пользователь передает в хост-подсистему файл размером до 8КБайт. Хост подсистема передает содержимое файла в SPE, который сохраняет его в поле значений блока по последовательным записям. Далее пользователь запрашивает одержимое блока по его номеру. SPE читает все записи блока и посылает обратно в хост-подсистему, которая записывает полученную информацию в файл. Файлы могут быть сравнены утилитой fc.


Вариант 10

Сетевой коммутатор на 128 портов. Сформировать в хост-подсистеме и передать в SPE таблицу коммутации из 254 ip адресов 195.19.32.1/24 (адреса 195.19.32.1 .. 195.19.32.254). Каждому адресу поставить в соответствие один из 128 интерфейсов (целые числа 0..127). Выполнить тестирование работы коммутатора, посылая из хост-подсистемы ip адреса и сравнивая полученный от GPC номер интерфейса с ожидаемым.


Вариант 11

Цифровой интерполятор. Сформировать в хост-подсистеме и передать в SPE 256 записей key-value со значениями функции f(x)=x^2 в диапазоне значений x от 0 до 1048576 (где x - ключ, f(x) - значение). Выполнить тестирование работы устройства, посылая из хост-подсистемы значение x и получая от sw_kernel значение f(x). Если указанное значение x не сохранено в SPE, выполнить поиск ближайшего (меньшего или большего) значения к точке x и вернуть соответствующий f(x). Сравнить результат с ожидаемым.


Вариант 12

Устройство формирования индексов SQL INTERSECT. Сформировать в хост-подсистеме и передать в SPE 256 записей множества A (случайные числа в диапазоне 0..1024) и 256 записей множества B (случайные числа в диапазоне 0..1024). Сформировать в SPE множество C = A and B. Выполнить тестирование работы SPE, сравнив набор ключей в множестве C с ожидаемым.


Вариант 13

Цифровой интерполятор ЧПЗ. Сформировать в хост-подсистеме и передать в SPE 256 значений x и функции f(x)=sin(x), имеющие тип double (где x - ключ, f(x) - значение). Для представления чисел double в целочисленном диапазоне использовать функции double ull2double(uint64_t) и uint64_t double2ull(double), входящие в библиотеку sw_kernel-lib. Для случайного значения, сформированного в хост-подсистеме выполнить поиск ближайшего большего, и передать его в хост-подсистему. Выполнить тестирование работы SPE, сравнив результат с ожидаемым.


Вариант 14

Ассоциативная память. Сформировать в хост-подсистеме и передать в SPE 256 случайных ключей и значений (по 64 бит). Выполнить поиск случайного значения ключа. Если результат найден, выдать его на консоль. Если результат не найден, то записать искомый ключ и случайное значение в SPE. Выполнить тестирование работы SPE, сравнив результат с ожидаемым.


Вариант 15

Устройство интегрирования. Сформировать в хост-подсистеме и передать в SPE 256 записей с ключами x и значениями f(x)=x^2 в диапазоне значений x от 0 до 1048576. Передать в sw_kernel числа x1 и x2 (x2>x1). В хост-подсистему вернуть сумму значений f(x) на диапазоне (x1,x2). Сравнить результат с ожидаемым.


Вариант 16

Блок пакетной передачи. Сформировать в хост-подсистеме буфер из 1048575 случайных ключей и значений (ключ и значение занимают по 64 бит). Передать буфер в SPE и сохранить четные ключи в структуре 1, а нечетные в структуре 2. Передать обратно в хост систему и выдать в консоль количество ключей в структурах 1 и 2. Выполнить обход полученных структур и результат передать в хост-подсистему. Сравнить результат с ожидаемым.


Вариант 17

Устройство вычисления обратной функции. Сформировать в хост-подсистеме и передать в SPE 256 записей key-value со значениями функции f(x)=x^2 в диапазоне значений x от 0 до 1048576 (где f(x) - ключ, x - значение). Выполнить тестирование работы устройства, посылая из хост-подсистемы значение f(x) и получая от sw_kernel значение x. Если указанного значения f(x) не сохранено в SPE, выполнить поиск ближайшего (меньшего или большего) значения к f(x) и вернуть соответствующий x. Сравнить результат с ожидаемым.


Вариант 18

Устройство формирования индексов SQL UNION. Сформировать в хост-подсистеме и передать в SPE 256 записей множества A (случайные числа в диапазоне 0..1024) и 256 записей множества B (случайные числа в диапазоне 0..1024). Сформировать в SPE множество C = A or B. Выполнить тестирование работы SPE, сравнив набор ключей в множестве C с ожидаемым.


Вариант 19

Коммутатор с QoS. Сформировать в хост-подсистеме и передать в SPE таблицу коммутации из 32 ip адресов 195.19.32.1/24 (адреса 195.19.32.1 .. 195.19.32.32), где для каждого адреса доступны 8 вариантов интерфейсов. Вариант определяется по уровню QoS, принимающему значения от 0 до 7 (в таблице коммутации 256 записей). Выполнить тестирование работы коммутатора, посылая из хост-подсистемы уровень QoS и ip адрес, и сравнивая полученный от GPC номер интерфейса с ожидаемым.


Вариант 20

Устройство формирования индексов SQL EXCEPT. Сформировать в хост-подсистеме и передать в SPE 256 записей множества A (случайные числа в диапазое 0..1024) и 256 записей множества B (случайные числа в диапазоне 0..1024). Сформировать в SPE множество C = A not B. Выполнить тестирование работы SPE, сравнив набор ключей в множестве C с ожидаемым.


Вариант 21

Система сбора сетевой статистики. Сформировать в хост-подсистеме и передать в SPE таблицу из 1024 ip адресов 195.19.32.0/22 (адреса 195.19.32.0 .. 195.19.35.255), где для каждого адреса сформированы четыре 16-ти разрядных счетчика (начальное значение - 0). Далее отправлять из хост-подсистемы номер счетчика и ip адрес. При каждом обращении увеличить соответствующий счетчик на 1. По запросу хост-подсистемы выдать состояние счетчиков для запрошенного ip адреса.


Вариант 22

Устройство управления памятью. Сформировать в SPE таблицу из 1048576 записей свободных страниц (в начальный момент таблица содержит все записи) и вторую таблицу из 1048576 занятых страниц (в начальный момент таблица пуста). При поступлении от хост-подсистемы запроса на выделение страницы удалить запись с минимальным ключом из таблицы свободных страниц и добавить его в таблицу занаятых страниц. Вернуть в хост подсистему номер записанной страницы. При поступлении от хост системы запроса на освобождение страницы произвести обратное действие.


Вариант 23

Устройство хранения темпоральных данных. Сформировать в SPE таблицу хранения темпоральных данных, для которой ключом поиска является текущее время в формате Posix time (количество секунд, прошедшее с 00:00 01.01.1970). От хост-подсистемы запрос на сохранение передается в виде текущего времени (ключ - 32 бит) и некотрого числа (значение - 64 бит). По запросу хост-подсистемы по переданной метке времени выдать число, записанное в ближайшее событии до указанного времени.


Вариант 24

Устройство проверки прав доступа. По запросу от хост-подсистемы, содержащему 64-битный индекс и 64-битный ключ доступа необходимо выполнить поиск на наличие записи с указанным индексом в таблице прав доступа. Если такой индекс имеется, сравнить переданный ключ доступа с сохраненным, и при совпадение ответить хост системе утвердительно (значение 1). Если индекс сохранен, но ключи доступа не совпадают, ответить отрицательно (значение 0). Если индекс не найден, то создать новую запись с полученным индексом и ключом доступа.


Вариант 25

Устройство поиска k ближайших соседей. Сформировать в хост-подсистеме и передать в SPE 256 записей key-value со случайными значениями x и соответствующих им значениях функции f(x)=2 * x^2 - x + 1 в диапазоне значений x от 0 до 1048575 (где x - ключ, f(x) - значение). По запросу хост подсистему выдать ключи x и значения f(x) 32-х ближайших в Эвклидовом пространстве точек x.


Вариант 26

Устройство управления записью в SSD накопитель. Сформировать в SPE таблицу из 1048575 записей с номерами свободных страниц и количеством перезаписей в них (в начальный момент таблица содержит все записи, количество перезаписей равно 0) и вторую таблицу из 1048575 занятых страниц и количеством перезаписей в них (в начальный момент таблица пуста). Для таблиц старшей частью ключа является количество перезаписей, а младшей частью ключа номер страницы. При поступлении от хост-подсистемы запроса на запись удалить из таблицы свободных страниц запись с минимальным количеством перезаписей, увеличить количество перезаписей, и перенести эту информацию в таблицу занятых страниц. Вернуть в хост подсистему номер записанной страницы и ее количество перезаписей. При поступлении от хост системы запроса на освобождение страницы произвести обратное действие, количество перезаписей при этом оставить неизменным.


Вариант 27

Сетевой маршрутизатор на 128 портов. Сформировать в хост-подсистеме и передать в SPE таблицу маршрутизации из 256 произвольно заданных непересекающихся диапазонов ip адресов 195.19.0.0/16 (адреса 195.19.0.0 .. 195.19.255.255). Каждый отрезок задается начальным адресом и маской («/17»..«/31») таким образом, что нет пересечения ip адресов (диапазоны адресов на числовой оси не пересаются). Каждому адресу поставить в соответствие один из 128 интерфесов (целые числа 0..127). При поступлении адреса от хост системы выбрать ближайший меньший (lnh_nsm) диапазон, после чего проверить соответствие ip адреса маске. Если маска соответствует существующему диапазону (адрес попадает в границы диапазона), выдать его хост-подсистеме. Если соответствующего диапазона не существует, выдать 0.


4. Практикум №2. Обработка и визуализация графов в вычислительном комплексе Тераграф

Практикум посвящен освоению принципов представления графов и их обработке с помощью вычислительного комплекса Тераграф. В ходе практикума необходимо ознакомиться с вариантами представления графов в виде объединения структур языка C/C++, изучить и применить на практике примеры решения некоторых задач на графах. По индивидуальному варианту необходимо разработать программу хост-подсистемы и программного ядра sw_kernel, выполняющего обработку и визуализацию графов.

4.1. Конвейер визуализации графов

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

  • 2D графическую сцену - наиболее часто применяемый случай, обладающий приемлемой вычислительной сложностью (порядка O(n2 log n));

  • 3D графическую сцену - такой вариант позволяет выполнять перемещение камеры наблюдения, что увеличивает возможное количество визуализируемых вершин;

  • Иерархическое представление - граф представляется в виде иерархически вложенных подграфов (уровней), что позволяет более наглядно представить тесно связанные компоненты первоначального графа.

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

  • Меньшее количество пересечений ребер: выравнивание вершин и ребер для получения наименьшего количества пересечений ребер делает визуализацию более понятной и менее запутанной.

  • Минимум наложений вершин и рёбер.

  • Распределение вершин и/или рёбер равномерно.

  • Более тесное расположение смежных вершин.

  • Формирование сообществ вершин из сильно связанных групп вершин.

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

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

Процесс визуализации графа

Рисунок 15 - Процесс визуализации графа

Кратко поясним представленный процесс:

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

  • На первом этапе формируется граф визуализации, содержащий для каждой вершины и ребра дополнительные атрибуты, значение которых и требуется определить в ходе этого процесса. Могут быть использованы дополнительные атрибуты, позволяющие выявить свойства вершин и более наглядно визуализировать структуру графа. Частым случаем является определение свойства центральности для вершин и ребер. В конечном итоге, для каждой вершины требуется хранить еще и ее координаты (x,y), цвет (color), радиус окружности для представления на сцене визуализации (size).

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

  • На следующем этапе происходит выделение групп вершин, входящих в так называемые сообществ: связность вершин внутри сообщества превосходит связность за его пределами. Примеры алгоритмов поиска сообществ представлены в примерах Практикума: Пример 5 и Пример 6.

  • Для каждого сообщества определяется область экрана для его размещения (алгоритмы глобального размещения, global placement).

  • Далее выполняется размещение вершин сообщества внутри каждой выделенной области. На данном этапе применяются алгоритмы, позволяющие представить связи небольшой группы вершин. Частым случаем является применение алгоритмов, основанных на имитации сил притяжения и отталкивания. сформированных на основе информации о вершинах и ребрах.

  • На заключительном этапе происходит передача готового графа визуализации в библиотеку. осуществляющую отрисовку сцены визуализации. В практикуме используется библиотека bokeh.

4.2. Представление информационных моделей алгоритма в виде структур данных

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

4.2.1. Представление графа G(V,E) списком смежных вершин

Пусть в алгоритме требуется вести обход графа, например, методом поиска в глубину. Тогда основной операцией будет поиск вершин v ∈ Adj[u], инцидентных указанной, и последующий переход к обработке всех связанных вершин. Но, поскольку степень вершин различна, требуется также хранить количество исходящих ребер count. Поле G.KEY хранит номера вершин u и порядковый номер ребра. Поле данных G.VALUE хранит номер инцидентной вершины v и вес ребра w, как показано в Таблице:

Таблица - Пример представления графа G(V,E) списком смежных вершин (G.KEY[u,i], G.VALUE[v,w])

G.KEY G.VALUE
u,0 count
u,1 v1,w
u,count vcount,w

Заметим, что индексная запись (u,0) - (count) может быть перенесена в любое место диапазона индексов, например в последнее (максимальное) значение индекса: (u,-1) - (count).

Представим указанную структуру композитных ключей в виде структур языка С/C++, используя макросы объединения, описанные в разделе 2.6.3. Представление структур данных в виде ключей и значений.

 //Структуры данных
 #define G 	        1                              //Граф
 //Объявление индексов
 #define ADJ_C_BITS 32                             //количество бит для хранения индекса смежной вершины графа
 const unsigned int IDX_MAX=(1ull<<ADJ_C_BITS)-1;  //максимальная смежность
 #define PTH_IDX  	IDX_MAX                        //номер индексной записи о вершине графа
 
 ////////////////////////////////////////////////////
 // Структура графа тип 1: (G.KEY[u,i], G.VALUE[v,w])
 ////////////////////////////////////////////////////
 
 //регистр ключа для вершины
 	/* Struktura 1 - G - описание графа
 	 * key[63..32] - номер вершины
 	 * key[31..0] -  индекс записи о вершине (0,1..adj_u-1)
 	 */
 STRUCT(Key){ //Data structure for graph operations
          unsigned int                				index:32;	//Поле 1: индекс записи
         unsigned int                				u:32; 		//Поле 0: номер вершины
 } ;
 
 //регистр значения индексной записи для вершины (с индексом PTH_IDX)
 	 /* key[31..0] = PTH_IDX
 	 *  key[63..32] = номер вершины u
 	 */ 
 STRUCT(Attributes){ //Data structure for graph operations
         unsigned int                				count:32;   //Поле 1: количество записей
         unsigned int                				any_atrs:32;//Поле 0: дополнительные атрибуты
 } ;
 
 //регистр значения для записей о смежных вершинах
 	/*
 	 * key[31..0] = 0..PTH_IDX-1
 	 * key[63..32] = номер вершины u
 	 * data[31..0] - w[u,v] вес ребра
 	 * atr[63..48] - номер смежной вершины v
 	 */
 STRUCT(Edge) { //Data structure for graph operations
 		unsigned int				v:32;		//Поле 1: номер смежной вершины				
 		unsigned int				w:32; 		//Поле 0: вес ребра (u,v)
 } ; 

Для упрощения синтаксиса описания структур и обращения к их полям мы используем шаблоны, описанные в файле compose_keys.hxx. Макрос объявления структуры выглядит следующим образом: #define STRUCT(name) struct name: _base_uint64_t<name>. Шаблон структуры _base_uint64_t позволяет описать 64 битное значение как беззнаковое целое стандартного типа uint64_t и разместить его в регистрах процессора, а не в оперативной памяти. Таким образом достигается большее быстродействие.

4.2.2. Представление графа G(V,E) списком инцидентных ребер

Если в алгоритме необходимо выполнить поиск ребер, соединяющих вершины (u,v), граф может быть представлен другим образом. Поле G.KEY в этом случае составляется из номеров вершин u и v, а поле данных G.VALUE хранит вес ребра w.

Таблица - Пример представления графа G(V,E) списком инцидентных ребер (G.KEY[u,v], G.VALUE[w])

G.KEY G.VALUE
u,v w

Соответствующее описание структуры графа на языке С показано ниже:

 //Структуры данных
 #define G 	        1                              //Граф
 
 //////////////////////////////////////////////////
 // Структура графа тип 2: (G.KEY[u,v], G.VALUE[w])
 //////////////////////////////////////////////////
 
 //регистр ключа для вершины
 	/* Struktura 1 - G - описание графа
 	 * key[63..32] - номер вершины u
 	 * key[31..0] -  номер вершины v
 	 */
 STRUCT(Key) { //Data structure for graph operations
         unsigned int                				v:32;	    //Поле 1: номер вершины v
         unsigned int                				u:32; 		//Поле 0: номер вершины u
 };
 
 //регистр значения записи для ребра (u,v) 
 	 /* key[31..0] = дополнительные атрибуты (не использованы)
 	 *  key[63..32] = номер вершины u
 	 */ 
 STRUCT(Attributes) { //Data structure for graph operations
         unsigned int                				w:32;       //Поле 1: вес ребра (u,v)
         unsigned int                				any_attrs:32;//Поле 0: дополнительные атрибуты
 };
 

4.2.3. Представление графа G(V,E) упорядоченным списком инцидентных ребер

Часто требуется хранить граф таким образом, чтобы множество ребер было упорядочено по их весу: минимальный ключ должен принадлежать ребру с наименьшим весом. Так как связность в общем случае не является уникальной и в графе могут присутствовать несколько ребер с одинаковым весом, следует использовать более сложный составной ключ. В старшей части ключа должен храниться вес ребра w, а в младшей будут храниться номера вершин (u,v). Т.е. поле G.KEY=(w,u,v). Поле G.VALUE может оставаться пустым, так как необходимая информация об инцидентности вершин и ребер имеется в составном ключе. Однако, в алгоритме может возникнуть необходимость хранить дополнительные атрибуты ребра (флаги, переменные и пр.).

Таблица - Пример представления графа G(V,E) упорядоченным списком инцидентных ребер (G.KEY[w,u,v], G.VALUE[])

G.KEY G.VALUE
w,u,v дополнительные атрибуты

Соответствующее описание структуры графа на языке С показано ниже:

 //Структуры данных
 #define G 	        1                              //Граф
 
 ///////////////////////////////////////////////////
 // Структура графа тип 3: (G.KEY[w,u,v], G.VALUE[])
 ///////////////////////////////////////////////////
 
 //регистр ключа для вершины
 	/* Struktura 1 - G - описание графа
 	 * key[63..48] -  вес ребра (u,v)
 	 * key[47..24] - номер вершины u
 	 * key[23..0]  - номер вершины v
 	 */
 STRUCT(Key) { //Data structure for graph operations
         unsigned int                				v:24;	    //Поле 2: номер вершины v
         unsigned int                				u:24; 		//Поле 1: номер вершины u
         unsigned int                				w:16;       //Поле 0: вес ребра (u,v)
 };
 
 //регистр значения записи для ребра (u,v) 
 	 /* key[63..0] = дополнительные атрибуты
 	 */ 
 STRUCT(Attributes) { //Data structure for graph operations
         unsigned int                				any_atr1:32;//Поле 1: дополнительные атрибуты
         unsigned int                				any_atr0:32;//Поле 0: дополнительные атрибуты
 };
 

В зависимости от выполняемых в алгоритме действий возможно использование как одного варианта представления, так и одновременно несколько вариантов.

4.3. Использование библиотеки шаблонов для обработки графов

Для реализация алгоритмов обработки графов необходимо представить операции над множествами (в том числе, множествами вершин и ребер графа) в виде набора команд дискретной математики DISC. Все команды обработки структур данных изменяют регистр статуса, по которому можно определить, было ли выполнение команды успешным (Регистр LNH_STATE, бит SPU_ERROR_FLAG). Результаты, влияющие на работу программы, должны быть учтены в общем алгоритме. После завершения основания команд, основанных на поиске (SEARCH, DELETE, MAX, MIN, NEXT, PREV, NSM, NGR) в очередь данных попадают ключ и значение найденных записей (KEY, VALUE), которые могут быть использованы в алгоритме программного ядра CPE riscv32. Для команд И-ИЛИ-НЕ (пересечение,объединение,дополнение) передаются операнды номеров структур (R,A,B). Операнд R указывает на номер структуры, в которой будет сохранен результат. Структуры A и B используются в И-ИЛИ-НЕ операциях и срезах в качестве исходных.

Таблица - Выполнение базовых операций над структурами данных.

Действие Псевдокод вызова команды DISC
Поиск по ключу (KEY,VALUE) = SEARCH(G, Key)
Поиск минимального ключа (KEY,VALUE) = MIN(G, Key)
Поиск максимального ключа (KEY,VALUE) = MAX(G, Key)
Поиск ближайшего меньшего (KEY,VALUE) = NSM(G, Key)
Поиск ближайшего большего (KEY,VALUE) = NGR(G, Key)
Поиск следующего (KEY,VALUE) = NEXT(G, Key)
Поиск предыдущего (KEY,VALUE) = PREV(G, Key)
Добавление INSERT(G, Key, Data)
Удаление DELETE(G, Key)
Объединение структур OR(Result, Source_A, Source_B)
Дополнение структур NOT(Result, Source_A, Source_B)
Пересечение структур AND(Result, Source_A, Source_B)
Срез структуры выше ключа GR(Result, Source_A, Key)
Срез структуры не ниже ключа GREQ(Result, Source_A, Key)
Двойной срез структуры GRLS(Result, Source_A, Key_A, Key_B)

После выполнения команд в структуре lnh_core формируется результат в виде ключа,значения и статуса, которые доступны в коде sw_kernel по именам полей:

  • lnh_core.result.key - поле ключа (64 бит);
  • lnh_core.result.value - поле значения (64 бит);
  • lnh_core.result.status - статус выполнения команды (64 бит).

Для того, чтобы извлечь композитные поля структуры, может быть использован следующий вариант приведения полей lnh_core к шаблону структур:

  • Чтение поля из композитного ключа выполняется с помощью следующего шаблона: get_result_key<ИМЯ_СТРУКТУРЫ>().ИМЯ_ПОЛЯ;. Например:

    weight = get_result_key<Graph::Key>().u;

  • Чтение поля из композитного значения выполняется аналогично с помощью следующего шаблона: get_result_value<ИМЯ_СТРУКТУРЫ>().ИМЯ_ПОЛЯ;. Например:

    var = get_result_value<Graph::Edge>().w;

  • Запись композитных полей в структуру осуществляется с помощью стандартного шаблона инициализации структуры: ИМЯ_СТРУКТУРЫ{.ИМЯ_ПОЛЯ1=ЗНАЧЕНИЕ, .ИМЯ_ПОЛЯ2=ЗНАЧЕНИЕ}, например:

    Graph::Key{.index=BASE_IDX, .u=u}

Для упрощения разработки алгоритмов на графах, а также контроля корректности синтаксических конструкций работы с ядром lnh64 была разработана специализированное программное ядро, расширяющее функциональность библиотеки Lnh64 L0. По данной ссылке доступны дополнительные заголовочные и cpp файлы, в которых собраны шаблоны описаний типовых структур графа и различных сервисных структур (очередей, деревьев и т.д.), необходимых для обработки графов и их визуалиации. Описание структур приведено в файле graph_iterators.hxx

Описание каждой из перечисленных ниже структур состоит из следующих секций:

  • Описание константного значения номера структуры struct_number при хранении в ядре lnh64. Данный параметр передается при инициализации структуры и остается неизменным в дальнейшем, вплоть до удаления информации из памяти lnh64/ Допускаются указывать номера структур, не занятые другой информацией, в диапазоне 1..7.

  • Описание индексов и констант, используемых при описании ключей. Целесообразно использовать индексы в конце диапазона рабочих значений.

  • Описание множества шаблонов ключей и значений.

  • Связывание типов ключей и значений с помощью макросов DEFINE_DEFAULT_KEYVAL(<КЛЮЧ>,<ЗНАЧЕНИЕ>) и DEFINE_KEYVAL(<КЛЮЧ>,<ЗНАЧЕНИЕ>). Макросы служат для создания синтаксических ограничений, которые запрещают программисту использовать иные сочетания. Например, если указан макрос DEFINE_DEFAULT_KEYVAL(Key_type,Value_type), то результатов выполнения команды выборки минимального ключа множеств будет структура типа Key_type.

  • Область объявления итераторов над структурой.

//Объявление номера структуры для хранения графа в lnh64 
#define   G   1   //Граф
...
//Инициализация структуры и запись номера структуры lnh64
constexpr Graph G1(G);
...
// Получить ключ записи с минимальным значением ключа
auto key = G1.get_first().key();
// key имеет тип, указанный в качестве DEFINE_DEFAULT_KEYVAL для ключа 

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

Таблица - Шаблон структуры для представления графа.

Структура/-Шаблон/* Поле Назначение
Graph Представление графа G(V,E) списком смежных вершин
    Key Поле ключа для записи о ребре (используется по умолчанию)
        index Индекс записи (0..index_max-2)
        u Номер вершины в графе
    Path_key Поле ключа для записи о кратчайшем пути и центральности
        index Индекс записи = index_max
        u Номер вершины в графе
    Base_key Поле ключа для записи атрибутов вершины
        index Индекс записи = index_max-1
        u Номер вершины в графе
    Viz_key Поле ключа для записи об атрибутах визуализации
        index Индекс записи = index_max-2
        u Номер вершины в графе
    Edge Поле значения для записи о ребре (используется по умолчанию)
        v Номер смежной вершины
        w Вес ребра
        attr Дополнительные атрибуты ребра
    Shortest_path Поле значения для записи о кратчайшем пути до вершины
        du Кратчайший путь от данной вершины до стартовой вершины
        btwc Центральность вершины
    Attributes Поле значения для записи атрибутов
        pu Номер предшествующей вершины в кратчайшем пути
        eQ Флаг хранения вершины в очереди (используется в алгоритме Дейкстры)
        adj_c Количество ребер вершины
    vAttributes Поле значения для записи атрибутов визуализации
        x Координата x для визуализации
        y Координата y для визуализации
        size Размер окружности для визуализации вершины
        color Цвет окружности для визуализации вершины

Для структуры графа определены следующие синтаксические ограничения:

//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Key, Edge)
//Дополнительная типизация
DEFINE_KEYVAL(Base_key, Attributes)
DEFINE_KEYVAL(Path_key, Shortest_path)
DEFINE_KEYVAL(Viz_key, vAttributes)

Для структуры графа определены итераторы:

  • Итератор вершин графа.

  • Итератор ребер для вершины графа.

Пример использования итератора вершин графа:

//Для каждой вершины графа
for (unsigned int v : virtex_range{G1}) {
 	...
}

Пример использования итератора ребер для вершины графа:

for (auto [adj, wu, attr] : edge_range(G1, v)) {
 	...
}           

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

uint32_t weight_sum=0; //сумма весов ребер
uint32_t edge_count=0; //количество ребер
//Обход всех вершин графа и вычисление среднего веса ребра
for (auto com_u : virtex_range{G1}) {
	//Для каждого ребра определить его вес
	for (auto [com_v, wu, attr] : edge_range(G1, com_u)) {
		weight_sum += wu;
		edge_count++;
	}
}
uint32_t weight_average = weight_sum/edge_count;

В ряде алгоритмов требуется использовать очередь вершин. Для этого в библиотеке создана следующая структура:

Таблица - Шаблон структуры для представления очереди вершин графа.

Структура / Шаблон / Поле Назначение
Queue Структура очереди
    Record Поле ключа для записи очереди
        id Номер вершины графа
        du Кратчайший путь (используется в алгоритме Дейкстры)
    Attributes Поле значения для атрибутов записи
        attr Атрибуты записи

Синтаксические ограничения на типы для очереди следующие:

//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Record, Attributes)

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

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

Приведем все четыре варианта обхода очереди.

В некоторых случаях требуется модифицировать очередь и выбирать записи из хвоста или головы очереди. Для этого можно использовать методы структуры очереди begin или rbegin (головная, первая запись очереди), rend (хвост, последняя вершина очереди), а также функцию удаления элемента из очереди erase. Пример использования итератора обхода очереди в прямом порядке с удалением записи из очереди:

//обход всех вершины графа
while(auto q_it = Q.begin()) { //Выбирается первая запись очереди
        Q.erase(q_it);
		auto [u, du] = *q_it;
		//Получение суммарной длины ребер
		du_sum += du;
  		...
}

Пример использования итератора обхода очереди в обратном порядке с удалением (итератор rbegin):

//обход всех вершины графа
while(auto q_it = Q.rbegin()) { //Выбирается поседняя запись очереди
        Q.erase(q_it);
		auto [u, du] = *q_it;
		//Получение суммарной длины ребер
		du_sum += du;
	  	...
}

Пример использования итератора обхода очереди в прямом порядке без изменения очереди:

//обход всех вершины графа
for(auto [u, du]:Q) { //Выбирается следующая запись очереди
		//Получение суммарной длины ребер
		du_sum += du;
}

Пример использования итератора обхода очереди в обратном порядке без изменения очереди:

//обход всех вершины графа
for(auto [u, du]:reverse{Q}) { //Выбирается следующая запись очереди
		//Получение суммарной длины ребер
		du_sum += du;
		...
}

Для визуализации графов часто требуется объединить вершины в сообщества по свойству модулярности (смотри алгоритм в разделе “4.4.3.1. Выделение сообществ на основе алгоритма MultiLevel”). Следующие структуры используются для построения сообществ. а также для размещения сообществ вершин в прямоугольных областях экрана.

Таблица - Шаблон структуры для представления сообществ.

Структура / Шаблон / Поле Назначение
Community Структура сообществ по модулярности (для визуализации)
    Key Поле ключа для сообщества
        adj Количество ребер сообщества
        id Номер сообщества
    Value Поле зачения для атрибутов сообщества
        first_virtex Номер начальной вершины графа, принадлежащей сообществу
        last_virtex Номер конечной вершины графа, принадлежащей сообществу
        id Номер сообщества

Синтаксические ограничения на типы для очереди следующие:

//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Key, Value)

Для структуры сообществ определены итераторы:

  • Итератор обхода сообществ

  • Итератор обхода вершин графа, входящий в сообщество.

Пример кода для обхода всех сообществ, хранимых в структуре cG1:

for (auto cmty : community_range{cG1}) {
  	...
}

Для обхода всех вершин графа одного сообщества может быть использован следующий код:

for (auto u : community_member_range(cG1, cmty)) {
  	...
}

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

Таблица - Шаблон структуры для представления дерева сообществ.

Структура / Шаблон / Поле Назначение
cTree Структура дерева сообществ по модулярности (для визуализации)
    Key Поле ключа для сообщества
        adj Количество ребер сообщества
        com_id Номер сообщества
    Vcount_key Поле ключа для записи атрибутов сообщества
        index Индекс записи об атрибутах сообщества, index = index_max
        com_id Номер сообщества
    XY_key Поле ключа для записи границ визуализации сообщества
        index Индекс записи о границах поля визуализации, index = index_max-1
        com_id Номер сообщества
    Value Поле зачения для атрибутов сообщества
        left_leaf Номер сообщества левого листа в дереве
        right_leaf Номер сообщества правого листа в дереве
    Vcount_value Поле значения для записи атрибутов сообщества
        v_count Количество вершин графа, входящих в сообщество
        is_leaf Флаг, указывающий на запись дерева типа “лист”
    XY_value Поле значения для записи атрибутов визуализации сообщества
        x0 Координата x левого нижнего угла прямоугольной области визуализации
        y0 Координата y левого нижнего угла прямоугольной области визуализации
        x1 Координата x правого верхнего угла прямоугольной области визуализации
        y1 Координата y правого верхнего угла прямоугольной области визуализации

Для дерева сообществ определена следующая синтаксическая типизация:

//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Key, Value)
//Дополнительная типизация
DEFINE_KEYVAL(Vcount_key, Vcount_value)
DEFINE_KEYVAL(XY_key, XY_value)

Итератор для структуры дерева сообществ позволяет выполнить обход всех сообществ:

for (auto community : ctree_range(cT1)) {
  	...
}

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

Структур mQueue упорядочивает записи по значению модулярности, в то время как в атрибутах записей хранит номера сообществ. Структура iQueue, наоборот, представляет записи по ключам сообществ, а в поле значения хранит модулярность (обратный формат записи).

Таблица - Шаблон структуры для представления очереди демодулярности (изменения модулярности при объединении сообществ u и v).

Структура / Шаблон / Поле Назначение
mQueue Структура очереди демодулярности (для визуализации)
    Modularity Поле ключа для хранения записи о номере сообщества
        index Номер записи (всегда = 1)
        id Индекс записи с значением дельтамодулярности delta_mod
        delta_mod Значение демодулярности
    Modularity_ext Поле ключа дополнительной записи атрибутов
        index Номер записи (всегда = 0)
        id Индекс записи с значением дельтамодулярности delta_mod
        delta_mod Значение демодулярности
    Communities Поле зачения для атрибутов сообщества
        com_u Номер сообщества u
        com_v Номер сообщества v
    Attributes Поле ключа для записи границ визуализации сообщества
        w_u_v Количество ребер, связывающих сообщества u и v

Для очереди модулярности заданы следующие соответствия типов:

//Дополнительная типизация
DEFINE_DEFAULT_KEYVAL(Modularity, Communities)
//Обязательная типизация
DEFINE_KEYVAL(Modularity_ext, Attributes)

Таблица - Шаблон структуры для представления обратной очереди демодулярности.

Структура / Шаблон / Поле Назначение
iQueue Структура обратной очереди демодулярности (для визуализации)
    Communities Поле ключа для атрибутов сообщества (обратная запись)
        com_u Номер сообщества u
        com_v Номер сообщества v
    Modularity Поле значения для хранения записи о номере сообщества
        index Номер записи (всегда = 1)
        id Индекс записи с значением дельтамодулярности delta_mod
        delta_mod Значение демодулярности

Для структуры обратной очереди задано ограничение по умолчанию:

//Обязательная типизация
DEFINE_DEFAULT_KEYVAL(Communities, Modularity)

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

// Инициализация итератора
mqueue_range mqr{mQ, iQ};

//Основной цикл
while (auto mq_it = mqr.rbegin())  {
	//u,v - номера объединяемых сообществ
	//w_u_v - атрибут связности u и v
	auto [com_v, com_u, com_u_index_val, com_u_delta_mod, w_u_v] = *mq_it;
	//Удалить запись о модулярности связи сообществ u<->v
	mqr.erase(mq_it);
  	...
}

Для обратной обратной очереди демодулярности реализован дополнительный итератор, позволяющий выполнить обход всех сообществ, связанных с указанным сообществом v:

for (auto mod_record : iqueue_range(iQ1,v)) {
  	...
}

Далее все примеры будут использовать указанные структуры и их итераторы.

4.4. Примеры реализации алгоритмов на графах

4.4.1. Алгоритм Дейкстры поиска кратчайшего пути

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

Задача формулируется следующим образом. Для взвешенного ориентированного графа G(V,E) без петель и дуг отрицательного веса найти кратчайшие пути от некоторой вершины до всех остальных. Пусть: w[u][v] - вес ребра, соединяющего вершину u и v; Adj[u] — множество вершин, смежных с u; s — исходная вершина; Q — множество вершин, которые осталось рассмотреть для поиска кратчайших путей; d[u] — расстояние от вершины s до вершины u; p[u] — вершина, предшествующая вершине u в кратчайшем пути от s до u.

В начальный момент времени множество Q состоит из всех вершин графа: Q=V. Алгоритм предполагает на каждом шаге поиск в множестве Q вершины u с наименьшим значением d[u], ее удаление из Q, а также вычислению значений d[v] для всех связанных с u вершин, входящих в Q. Если полученная длина пути короче ранее найденной, то она модифицируется, и сохраняется новый маршрут p[v] через вершину u. В итоге, когда будут просмотрены все вершины и Q останется пуст, в p[u] окажется кратчайший маршрут, а в d[u] — его длина. Псевдокод алгоритма представлен ниже:

Дейкстра(s)
	Q = V
	d[s] = 0
	p[s] = 0
	ЦИКЛ ПОКА 
		Для всех u ∈ V, u /= s 
		d[u] = ∞;
	ВСЕ ЦИКЛ ПОКА
	ЦИКЛ ПОКА Q /= ∅
		ПОИСК (u ∈ Q с минимальным значением d[u])
		Q=Q \ u;
		ЦИКЛ ПОКА 
			Для всех вершин v ∈ Adj[u]
			ЕСЛИ ( (v ∈ Q) и (d[v] > d[u] + w[u][v]) ) ТО 
				d[v] = d[u] + w[u][v]; 
				p[v] = u;
			ВСЕ ЕСЛИ
		ВСЕ ЦИКЛ ПОКА
	ВСЕ ЦИКЛ ПОКА

В алгоритме необходимо реализовать структуры данных для хранения графа G и очередь из вершин Q по порядку длины путей d[u] до стартовой вершины. Как говорилось ранее, для вычислительной системы с набором команд дискретной математики требуется представить G и Q в виде структур с 64x битными ключами и значениями.

Примем следующие обозначения: S.KEY(k1,..,kn) является составным ключом поиска в некоторой структуре S, состоящим из полей k1,..,kn; S.VALUE(d1,…,dn) являются данными для структуры S, состоящими из полей d1,…,dn. Доступ к конкретным полям данных может быть показан в псевдокоде алгоритма с помощью модификатора, т.е. как S.VALUE.d1. Например, изменение поля x в составном поле данных указывается как S.VALUE.x=10.

Множество Q используется для поиска вершины с наименьшим показателем d[u]. Т.к. значение d[u] для разных вершин в структуре Q может быть одинаковым, то d[u] не может являться ключом. В качестве ключа может быть выбрана пара значений Q.KEY=(d[u],u). Это обеспечивает не только поиск вершины с минимальным d[u], но и выбор вершины с наименьшим номером из нескольких возможных вариантов. Поле данных структуры Q не используется: Q.VALUE=(0).

Пример поиска кратчайших путей в графе - начальное состояние структур данных

Рисунок 16 - Пример поиска кратчайших путей в графе - начальное состояние структур данных

Исходный граф G применяется в алгоритме для определения списка смежных вершин и другой информации, соответствующей каждой вершине. Ключом поиска в структуре G будет являться уникальный номер вершины в графе G: G.KEY=(u). Данными для структуры G является множество смежных вершин Adj[u], массив длин путей для них w[u], найденное кратчайшее расстояние d[u], маршрут p[u], а также бит принадлежности вершины к множеству Q: т.е. G.VALUE=(Adj[u],w[u],d[u],p[u],uQ). Пояснить использование этих структур можно на примере поиска кратчайших путей для графа, представленного на рисунке:

Пример поиска кратчайших путей в графе - состояние структур данных после выполнения первой итерации

Рисунок 17 - Пример поиска кратчайших путей в графе - состояние структур данных после выполнения первой итерации

На первом шаге алгоритма из структуры Q выбирается минимальный ключ Q.KEY=(0,1) и по нему определяется код u, соответствующий вершине s=1. Для этой вершины в структуре G выбирается строка и определяются поля Adj[u],w[u],d[u],p[u],uQ.

Найденная строка исключается из структуры Q по известному ключу Q.KEY=(d[u],u). Далее, для каждой вершины v из множества Adj[u] по структуре G определяется принадлежность к множеству Q (параметр uQ). Если он равен 1, то проверяется условие d[v]=d[u]+w[u][v], где w[u][v] — один из элементов множества w[u], соответствующий ребру между u и v.

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

Пример поиска кратчайших путей в графе - состояние структур в конце работы алгоритма

Рисунок 18 - Пример поиска кратчайших путей в графе - состояние структур в конце работы алгоритма

Указанные на рисунке структуры могут быть объявлены следующим образом:

struct Graph {
        using virtex_t = uint32_t;
        int struct_number;
        constexpr Graph(int struct_number) : struct_number(struct_number) {}
        static const uint32_t adj_c_bits = 32;
        static const uint32_t idx_max = (1ull << adj_c_bits) - 1;
        static const uint32_t pth_idx = idx_max;
        static const uint32_t base_idx = idx_max - 1;
        static const uint32_t viz_idx = idx_max - 2;

        //регистр ключа для вершины:
        /* STRUCT(u_key) - G - описание графа
         * key[63..32] - номер вершины
         * key[31..0] -  индекс записи о вершине (0,1..adj_u)
         */
        STRUCT(Key) {
                unsigned int index: 32;
                virtex_t     u: 32;
        };
        STRUCT(Path_key) {
                unsigned int index: 32 = pth_idx;
                virtex_t     u: 32;
        };
        STRUCT(Base_key) {
                unsigned int index: 32 = base_idx;
                virtex_t     u: 32;
        };
        //граф визуализации
        STRUCT(Viz_key) {
                unsigned int index: 32 = viz_idx;
                virtex_t     u: 32;
        };

        //регистр значения для записей о смежных вершинах:
        STRUCT(Edge) {
                virtex_t     v: 32;
                short int    w: 16;
                short int    attr: 16;
        };

        //регистр значения индексной записи для вершины (с индексом PTH_IDX):
        STRUCT(Shortest_path) {
                virtex_t     du: 32;
                unsigned int btwc: 32;
        };

        //регистр значения атрибутов для вершины с индексом BASE_IDX: STRUCT(u_attributes)
        STRUCT(Attributes) {
                unsigned int pu: 32;
                bool         eQ: 8;
                int          non: 8 = 0;
                short int    adj_c: 16;
        };

        //регистр значения для записи атрибутов визуализации вершинах
        STRUCT(vAttributes) { //Data structure for graph operations
                unsigned short int                              x: 16;          //Поле 1: координата x [0..64K]
                unsigned short int                              y: 16;          //Поле 2: координата y [0..64K]
                unsigned short int                              size: 8;        //Поле 3: размер [0..255]
                unsigned int                                    color: 24;      //Поле 4: цвет [0x000000..0xFFFFFF]
        };

        //Обязательная типизация
        DEFINE_DEFAULT_KEYVAL(Key, Edge)
        //Дополнительная типизация
        DEFINE_KEYVAL(Base_key, Attributes)
        DEFINE_KEYVAL(Path_key, Shortest_path)
        DEFINE_KEYVAL(Viz_key, vAttributes)

};

Далее рассмотрим код, реализующий алгоритм Дейкстры в программном ядре CPE.

 void dijkstra() {
	//получить начальную вершину графа из MQ
	uint32_t start_virtex = mq_receive(); 

	//получить конечную вершину графа из MQ
	uint32_t stop_virtex = mq_receive(); 

	//Очистить очередь Q 
	lnh_del_str_async(Q); 

	// Вставляем начальную вершину в Q с нулевым кратчайшим путем
	lnh_ins_async(Q,INLINE(q_record,{.u=start_virtex,.du=0}),0);

	//Получите btwc (центральность), чтобы сохранить его снова
	lnh_search(G,INLINE(u_key,{.index=PTH_IDX,.u=start_virtex}));
	btwc = (*(u_index*)&lnh_core.result.value).__struct.btwc;

	// Сохраняем du для запуска virtex
	lnh_ins_async(G,INLINE(u_key,{.index=PTH_IDX,.u=start_virtex}),
		INLINE(u_index,{.du=0,.btwc=btwc}));

	// Перебрать все вершины в очереди Q
	while (lnh_get_first(Q)) { 
    	u = (*(q_record*)&lnh_core.result.key).__struct.u;
    	du = (*(q_record*)&lnh_core.result.key).__struct.du;

    	//Удалит вершину из Q
    	lnh_del_async(Q,lnh_core.result.key); 
    	lnh_search(G,INLINE(u_key,{.index=BASE_IDX, .u=u}));
    	pu = (*(u_attributes*)&lnh_core.result.value).__struct.pu;
    	eQ = (*(u_attributes*)&lnh_core.result.value).__struct.eQ;
    	adj_c = (*(u_attributes*)&lnh_core.result.value).__struct.adj_c;
    	
    	// Очистить флаг eQ
     	lnh_ins_async(G,lnh_core.result.key,
     		INLINE(u_attributes,{.pu=pu, .eQ=false, .non=0, .adj_c=adj_c}));

     	//Для каждой вершины Adj
     	for (i=0;i<adj_c;i++) { 

        	//Получить Adj[i]
     		lnh_search(G,INLINE(u_key,{.index=i,.u=u})); 
          	wu = (*(edge*)&lnh_core.result.value).__struct.w;
            adj = (*(edge*)&lnh_core.result.value).__struct.v;
          
          	//Получить информацию о смежных вершинах
          	lnh_search(G,INLINE(u_key,{.index=BASE_IDX,.u=adj}));
          	eQc=(*(u_attributes*)&lnh_core.result.value).__struct.eQ;
          	count=(*(u_attributes*)&lnh_core.result.value).__struct.adj_c;
          	lnh_search(G,INLINE(u_key,{.index=PTH_IDX,.u=adj}));
          	dv=(*(u_index*)&lnh_core.result.value).__struct.du;
          	btwc=(*(u_index*)&lnh_core.result.value).__struct.btwc;
          	
          	//Если изменился кратчайший путь
          	if (dv>(du+wu)) { 
           		if(eQc) {
            		if (dv!=INF) //если не петля, отправить вершину в Q
                		lnh_del_async(Q,INLINE(q_record,{.u=adj, .du=dv}));
            			lnh_ins_async(Q,INLINE(q_record,{.u=adj, .du=du+wu}),0); 
            		}
           			// Обновляем кратчайший путь
           			lnh_ins_async(G,INLINE(u_key,{.index=PTH_IDX,.u=adj}),
           				INLINE(u_index,{.du=du+wu,.btwc=btwc})); //изменить du
           			lnh_ins_async(G,INLINE(u_key,{.index=BASE_IDX,.u=adj}),
           				INLINE(u_attributes,{.pu=u, .eQ=eQc, .non=0, .adj_c=count})); 
           		}
        }
    }
 
 	// Сохранить кратчайший путь
 	lnh_search(G,INLINE(u_key,{.index=PTH_IDX, .u=stop_virtex}));
 	mq_send((*(u_index*)&lnh_core.result.value).__struct.du);
 }
 void dijkstra() {
	//получить начальную вершину графа из MQ
	uint32_t start_virtex = mq_receive(); 

	//получить конечную вершину графа из MQ
	uint32_t stop_virtex = mq_receive(); 

    //Очистка очереди
    lnh_del_str_async(Q);

    //добавление стартовой вершины с du=0
    lnh_ins_async(Q,q_record{.u=start_virtex,.index=0},0);

    //Получите btwc (центральность), чтобы сохранить его снова 
    lnh_search(G,u_key{.index=PTH_IDX,.u=start_virtex});
    btwc = get_result_value<u_index>().btwc;

    // Сохраняем du для запуска virtex 
    lnh_ins_async(G,u_key{.index=PTH_IDX,.u=start_virtex},u_index{.du=0,.btwc=btwc});

    // Перебрать все вершины в очереди Qа   
    while (lnh_get_first(Q)) {

        u = get_result_key<q_record>().u;
        du = get_result_key<q_record>().index;

        //Удалит вершину из Q
        lnh_del_async(Q,lnh_core.result.key);

        //Получить значения pu, |Adj|, eQ
        lnh_search(G,u_key{.index=BASE_IDX, .u=u});
        pu = get_result_value<u_attributes>().pu;
        eQ = get_result_value<u_attributes>().eQ;
        adj_c = get_result_value<u_attributes>().adj_c; 

        // Очистить флаг eQ 
        lnh_ins_async(G,lnh_core.result.key,u_attributes{.pu=pu, .eQ=false, .non=0, .adj_c=adj_c});

        //Для каждой вершины Adj
        for (i=0;i<adj_c;i++) {

            //Получить Adj[i]
            lnh_search(G,u_key{.index=i,.u=u});
            wu = get_result_value<edge>().w;
            adj = get_result_value<edge>().v;

            //Получить информацию о смежных вершинах
            lnh_search(G,u_key{.index=BASE_IDX,.u=adj});
            eQc=get_result_value<u_attributes>().eQ;
            count=get_result_value<u_attributes>().adj_c;
            lnh_search(G,u_key{.index=PTH_IDX,.u=adj});
            dv=get_result_value<u_index>().du;
            btwc=get_result_value<u_index>().btwc;

            //Если изменился кратчайший путь
            if (dv>(du+wu)) {
                if (eQc) {
                    //Если не петля, отправить вершину в Q
                    if (dv!=INF) {
                        lnh_del_async(Q,q_record{.u=adj, .index=dv});
                    }
                    lnh_ins_async(Q,q_record{.u=adj, .index=du+wu},0);
                }

                // Обновляем кратчайший путь
                lnh_ins_async(G,u_key{.index=PTH_IDX,.u=adj},u_index{.du=du+wu,.btwc=btwc});
                lnh_ins_async(G,u_key{.index=BASE_IDX,.u=adj},u_attributes{.pu=u, .eQ=eQc, .non=0, .adj_c=count});
            }
        }
    }
 
 	// Сохранить кратчайший путь
 	lnh_search(G,INLINE(u_key,{.index=PTH_IDX, .u=stop_virtex}));
 	mq_send(get_result_value<u_index>().du);
 }

–>

void dijkstra_core(unsigned int start_virtex) {
    //Очистка очереди
    Q.del_str_async();
    //добавление стартовой вершины с du=0
    Q.ins_async(Queue::Record{.id = start_virtex, .du = 0}, Queue::Attributes{});
    //Get btwc to store it again
    auto [du, btwc] = G.search(Graph::Path_key{.u = start_virtex}).value();
    //Save du for start virtex
    G.ins_async(Graph::Path_key{.u = start_virtex}, Graph::Shortest_path{.du = 0, .btwc = btwc});
    //обход всех вершины графа
    while(auto q_it = Q.begin()) {
        Q.erase(q_it);
	    auto [u, du] = *q_it;
        //Get pu, |Adj|, eQ
        auto result = G.search(Graph::Base_key{.u = u});
        auto [pu, eQ, non, adj_c] = result.value();
        // Clear eQ
        G.ins_async(result.key(), Graph::Attributes{.pu = pu, .eQ = false, .non = 0, .adj_c = adj_c});
        //For each Adj
        for (auto [adj, wu, attr] : edge_range(G, u)) {
            //Get information about Adj[i]
            auto [adj_pu, eQc, non, count] = G.search(Graph::Base_key{.u = adj}).value();
            auto [dv, btwc] = G.search(Graph::Path_key{.u = adj}).value();
            //Change distance
            if (dv > (du + wu)) {
                if (eQc) {
                    //if not loopback, push to Q
                    if (dv != Graph::inf) {
                        Q.del_async(Queue::Record{.id = adj, .du = dv});
                    }
                    Q.ins_async(Queue::Record{.id = adj, .du = du + wu}, Queue::Attributes{});
                }
                //change du
                G.ins_async(Graph::Path_key{.u = adj}, Graph::Shortest_path{.du = du + wu, .btwc = btwc});
                //change pu
                G.ins_async(Graph::Base_key{.u = adj}, Graph::Attributes{.pu = u, .eQ = eQc, .non = 0, .adj_c = count});
            }
        }
    }
}

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

auto [du, btwc] = G1.search(Graph::Path_key{.u = start_virtex}).value();
G1.ins_async(Graph::Path_key{.u = start_virtex}, Graph::Shortest_path{.du = 0, .btwc = btwc});

4.4.2. Алгоритм поиска центральности

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

где:

σs,t(i) – число кратчайших путей из узла s в узел t через узел i;

σs,t – общее число кратчайших путей между всеми парами s и t.

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

Вы можете ознакомиться с дополнительными материалами по данной тематике в работе И.А. Евина: [“Введение в теорию сложных сетей”].

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

Центральность()
	ЦИКЛ ПОКА 
		Для всех u ∈ V
		btwc[u] = 0;
	ВСЕ ЦИКЛ ПОКА
	ЦИКЛ ПОКА  
		Для всех u ∈ V
		Дейкстра(u);
		ЦИКЛ ПОКА 
			Для всех v ∈ V , v /= u 
			ЕСЛИ (кратчайший путь (u,v) проходит через вершину k) TO
				btwc[k] = btwc[k] + 1;
			ВСЕ ЕСЛИ
		ВСЕ ЦИКЛ	
	ВСЕ ЦИКЛ ПОКА

Указанный алгоритм определения центральности представлен в файле dijkstra.cpp.

//-------------------------------------------------------------
// Центральность
//-------------------------------------------------------------

void btwc () {
    //Iterate u
    for (Graph::virtex_t u : virtex_range{G1}) {
        //Start Dijksra shortest path search
        dijkstra_core(u);
        //Iterate v
        for (Graph::virtex_t v : virtex_range{G1}) {
            //For undirected graphs needs to route 1/2 shortest paths (u<v)
            if (u != v) {
                auto du = G1.search(Graph::Path_key{.u = v}).value().du;
                //If there is a route to u
                if (du != INF) {
                    //Get pu
                    auto pu = G1.search(Graph::Base_key{.u = v}).value().pu;
                    while (pu != u) {
                        //Get btwc
                        auto [du, btwc] = G1.search(Graph::Path_key{.u = pu}).value();
                        //Write btwc, set du by the way
                        G1.ins_async(Graph::Path_key{.u = pu}, Graph::Shortest_path{.du = du, .btwc = btwc + 1});
                        //Route shortest path
                        pu = G1.search(Graph::Base_key{.u = pu}).value().pu;
                    }
                }
            }
        }
        //Init graph again
        for (Graph::virtex_t v : virtex_range{G1}) {
            //Init graph again
            auto adj_c = G1.search(Graph::Base_key{.u = v}).value().adj_c;
            G1.ins_sync(Graph::Base_key{.u = v}, Graph::Attributes{.pu = v, .eQ = true, .non = 0, .adj_c = adj_c});
            auto btwc = G1.search(Graph::Path_key{.u = v}).value().btwc;
            G1.ins_sync(Graph::Path_key{.u = v}, Graph::Shortest_path{.du = INF, .btwc = btwc});
        }
    }
}

Пример работы алгоритма для графа-решетки показан на следующем рисунке:

Пример поиска и визуализации свойства центральности на графе решетки

Рисунок 19 - Пример поиска и визуализации свойства центральности на графе решетки

Центральность продемонстрирована с помощью размера и цвета вершины.

4.4.3. Алгоритмы поиска сообществ

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

Для оценки целесообразности объединения вершин в сообщества используется числовая характеристика, которая описывает выраженность структуры сообществ в данном графе, и называемая модулярностью:

где δ(Ci, Cj) — дельта-функция, равная единице, если Ci = Cj и нулю иначе.

Физический смысл модулярности состоит в следующем. Возьмём две произвольные вершины i и j. Вероятность появления ребра между ними при генерации случайного графа с таким же количеством вершин и рёбер, как у исходного графа, равна didj/2m. Реальное количество рёбер в сообществе C будет равняться Σi,j ∈ C Ai,j.

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

4.4.3.1. Выделение сообществ на основе алгоритма MultiLevel

Алгоритм основан на оптимизации модулярности. В начале работы алгоритма каждой вершине сначала ставится в соответствие по сообществу. Далее чередуются следующие этапы:

  1. Первый этап

    • Для каждой вершины перебираем её соседей

    • Перемещаем в сообщество соседа, при котором модулярность увеличивается максимально

    • Если перемещение в любое другое сообщество может только уменьшить модулярность, то вершина остаётся в своём сообществе

    • Последовательно повторяем, пока какое-либо улучшение возможно

  2. Второй этап

    • Создать метаграф из сообществ-вершин. При этом рёбра будут иметь веса, равные сумме весов всех рёбер из одного сообщества в другое или внутри сообщества (т.е. будет взвешенная петля)

    • Перейти на первый этап для нового графа

Работа алгоритма Multilevel: два прохода, для первого показаны оба этапа

Рисунок 20 - Работа алгоритма Multilevel: два прохода, для первого показаны оба этапа

Работа алгоритма Multilevel: два прохода, для первого показаны оба этапа

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

4.4.3.2. Центральность ребер (Edge Betweenness) – метод Girvan – Newman

Для каждой пары вершин связного графа можно вычислить кратчайший путь, их соединяющий. Будем считать, что каждый такой путь имеет вес, равный 1/N, где N — число возможных кратчайших путей между выбранной парой вершин. Если такие веса посчитать для всех пар вершин, то каждому ребру можно поставить в соответствие значение Edge betweenness — сумму весов путей, прошедших через это ребро.

Для ясности приведём следующую иллюстрацию:

Граф, для рёбер которого посчитаны значения Edge betweenness

В данном графе можно выделить два сообщества: с вершинами 1-5 и 6-10. Граница же будет проходить через ребро, имеющее максимальный вес (25). На этой идее и основывается алгоритм: поэтапно удаляются ребра с наибольшим весом, а оставшиеся компоненты связности объявляем сообществами. Алгоритм состроит из 6 этапов:

  1. Инициализировать веса

  2. Удалить ребро с наибольшим весом

  3. Пересчитать веса для рёбер

  4. Сообществами считаются все компоненты связности

  5. Подсчитать модулярность

  6. Повторять шаги 2-6, пока есть рёбра

4.4.4. Алгоритмы раскладки графов

Раскладка неориентированных графов используется при проектировании топологии СБИС, целью которого является оптимизация схемы для получения наименьшего количества пересечений линий. Eades (1984) ввел модель Spring-Embedder, в которой вершины в графе заменяются стальными кольцами, а каждое ребро заменяется пружиной. Пружинная система запускается со случайным начальным состоянием, и вершины соответственно перемещаются под действием пружинных сил. Оптимальная компоновка достигается за счет того, что энергия системы сводится к минимуму.

Эта интуитивная идея была развита Камада и Каваи (1989), Фрухтерман и Рейнгольд (1991) в соответствующих алгоритмах.

4.4.4.1. The Spring Model

Модель spring-embedder была первоначально предложена Eades (1984) и в настоящее время является одним из самых популярных алгоритмов для рисования неориентированных графов с прямолинейными ребрами, широко используемого в системах визуализации информации.

Алгоритм Идеса следует двум эстетическим критериям: равномерная длина ребер и максимально возможная симметрия. В модели Spring-Embedder вершины графа обозначаются набором колец, и каждая пара колец соединена пружиной. Пружина связана с двумя видами сил: силами притяжения и силами отталкивания, в зависимости от расстояния и свойств соединительного пространства.

Раскладка графа приближается к оптимальной по мере уменьшения энергии пружинной системы. К узлам, соединенным пружиной, приложена сила притяжения (fa), а к разъединенным узлам приложена сила отталкивания (fr). Эти силы определяются следующим образом:

ka и kr — константы, а d — текущее расстояние между узлами. Для соединенных узлов это расстояние d является длиной пружины. Начальная компоновка графа настраивается случайным образом. В каждой итерации силы рассчитываются для каждого узла, и узлы соответственно перемещаются, чтобы уменьшить напряжение. Однако модель Spring-Embedder может не работать на очень больших графах.

4.4.4.2. Local Minimum

Модель spring-embedder привела к созданию ряда модифицированных и расширенных алгоритмов раскладки неориентированных графов. Например, силы отталкивания обычно вычисляются между всеми парами вершин, а силы притяжения могут быть рассчитаны только между соседними вершинами. Упрощенная модель уменьшает временную сложность: вычисление сил притяжения между соседями выполняется за O(|E|), хотя вычисление силы отталкивания выполняется за O(|V|²), что в является недостатком алгоритмов с n телами. Камада и Каваи (1989) представили алгоритм, основанный на модели пружинного внедрения Идса, который пытается достичь следующих двух критериев или эвристик рисования графа:

  • Количество пересечений ребер должно быть минимальным.

  • Вершины и ребра распределены равномерно.

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

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

Для динамической системы из n частиц, соединенных между собой пружинами, пусть p1, p2 … pn будут частицами в области поля визуализации, соответствующими вершинам v1, v2 … vn V графа соответственно. Сбалансированное расположение вершин может быть достигнуто с помощью динамически сбалансированной пружинной системы. Камада и Каваи сформулировали степень дисбаланса как общую энергию пружин:

Данная модель подразумевает, что наилучшее расположение графа — это состояние с минимальным значением E. Расстояние dij между двумя вершинами vi и vj в графе определяется как длина кратчайшего пути между vi и vj. Алгоритм направлен на согласование длины пружины lij между частицами pi и pj с кратчайшим расстоянием пути, чтобы достичь оптимальной длины между ними на чертеже. Длина lij определяется следующим образом:

где L — желаемая длина одного ребра в области рисования. L можно определить на основе наибольшего расстояния между вершинами в графе. Если L0 — длина стороны квадрата области рисования, L можно получить следующим образом:

Сила пружины, соединяющей pi и pj, обозначается параметром kij:

Затем алгоритм ищет визуальное положение для каждого узла v в топологии сети и пытается уменьшить функцию энергии во всей сети. То есть алгоритм вычисляет частные производные для всех узлов топологии сети с точки зрения каждого xv и yv, которые равны нулю (т.е. ∂E / ∂xv = ∂E / ∂yv = 0; 1 < v < n).

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

4.4.4.3. Force-Directed Placement

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

Сила притяжения (fa) и сила отталкивания (fr) определяются следующим образом:

где d — расстояние между двумя узлами, а k — константа идеального попарного расстояния. Константа идеального расстояния k = √(area / n). Здесь area — область рамки чертежа, n — общее количество узлов в топологии сети.

Алгоритм Фрухтермана-Рейнгольда выполняется итеративно, и все узлы перемещаются одновременно после расчета сил для каждой итерации. Алгоритм добавляет атрибут «смещения» для контроля смещения положения узлов. В начале итерации алгоритм Фрухтермана-Рейнгольда вычисляет начальное значение смещения для всех узлов с использованием силы отталкивания (fr). Алгоритм также использует силу притяжения (fa) для многократного обновления визуального положения узлов на каждом ребре. Наконец, он обновляет смещение положения узлов, используя значение смещения.

Рисунок 21 — Раскладка графа Local minmum - Kamada - Kawaii

Рисунок 22 — Раскладка графа Force-directed layout - Fruchterman - Reingold

4.5. Библиотека gpc64io

Библиотека gpc64io для языка Python3 позволяет разрабатывать приложения, использующие аппаратные ресурсы микпропроцессора Леонард Эйлер. Библиотека состоит из двух частей:

  • Описание и реализация основного объекта GPC на языке Python3, обеспечивающего высокоуровневый интерфейс связи прогрммного кода Python3 с аппаратным ядром обработки графов через библиотеку lnh64.so.
  • Высокопроизволительная библиотека lnh64.so, написанная на языке C++, которая обеспечивает низкоуровневое взаимодействие в драйвером символьного устройства /dev/gpc*.

Для использование ресурсов библиотеки программист должен создать экземплярр класса GPC одним из способов:

# 1. Создание экземпляра класса и получение доступа к свободному устройству
gpc = GPC()
# 2. Создание экземпляра класса и получение доступа к указанному устройству  						
gpc = GPC(device_path=<путь к символьному устройству /dev/gpc*>) 	
# 3. Создание экземпляра класса, получение доступа к указанному устройству и загрузка sw_kernel  						
gpc = GPC(device_path=<путь к символьному устройству /dev/gpc*>,swk_path=<путь к sw_kernel файлу rawbinary>) 	

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

Метод класса Назначение Параметры Возвращаемое значение  
load_swk(swk_path: str) Загрузка программного ядра sw_kernel swk_path - полный путь к файлу rawbinary ‘0’ - загрузка выполнена успешно; ‘-1’ - загрузка завершилась с ошибкой  
def_handlers(handler_file_path: str) Формирование списка обработчиков handler_file_path - полный путь к файлу gpc_handlers.h с объявлением заголовков обработчиков -  
mq_send_uint64(message: int) Пересылка короткого сообщения в gpc message - посылаемое сообщение размером до 8 байт (unsigned long long) -  
mq_send_buf(ba: bytearray) Передача блока сообщений в gpc ba - массив значений типа bytearray Указатель на объект потока приема (thread)  
close_dev() Освобождение символьного устроства gpc - ‘0’ - устройство обвобождено; другое значение - код ошибки  
finish() Ожидание готовности gpc к запуску обработчика (состояние ready) - -  
sync_with_gpc() Синхронизация с ядром (операция рукопожатия) - -  
join(thread) Ожидание завершения потока thread - объект потока, возвращаемый методами mq_receive_buf или mq_send_buf -  
mq_receive_uint64() Прием короткого сообщения из gpc - полученное сообщение размером до 8 байт (unsigned long long)  
mq_receive_buf(buf_size: int, ba: bytearray) Прием блока сообщений из gpc buf_size - размер блока передаваемых сообщений (в байтах); ba - массив значений типа bytearray Указатель на объект потока приема (thread)  

В следующем примере производится инициализация ядра gpc, после чего выполняется тестовая передача и прием массива. Результат сравнивается с ожидаемым:

#Импорт библиотек
import array
import pathlib as pth
import ctypes
from gpc64io.base import GPC

#Количество передаваемых значений
TEST_PACK_SIZE=1000000

#Получить доступ к свободному gpc
gpc = GPC() 
print(gpc.dev_path)

#Загрузить sw_kernel
swk_path=str(pth.Path().absolute()/"sw-kernel/sw_kernel.rawbinary") #Путь к проекту sw_kenrel
if gpc.load_swk(swk_path)!=0:
    print("Error when loading sw_kernel file "+swk_path)

#Загрузить id и имен обработчиков из файла gpc_handlers.h
handlers_path=str(pth.Path().absolute()/"include/gpc_handlers.h")
gpc.def_handlers(handlers_path)
print(gpc.handlers)

#Запуск обработчика эхо-пакетов
gpc.start_handler("echo_mq")

#Тест приемо-передачи коротких сообщений
test = 0x123456789
gpc.mq_send_uint64(test)
if test == gpc.mq_receive_uint64():
   print("Echo uint64_t test result: True")
else:
   print("Echo uint64_t test result: False")

#Тест блочной приемо-передачи
arr=array.array('Q',(i for i in range(0,TEST_PACK_SIZE)))
buf_out=bytearray(arr)
buf_in=bytearray(arr)
write_thread = gpc.mq_send_buf(buf_out)
read_thread = gpc.mq_receive_buf(len(buf_in),buf_in)

#Ожидание завершения потоков передачи и приема
gpc.join(write_thread)
gpc.join(read_thread)

#Проверка корректности данных
if buf_out == buf_in:
   print("Echo test result: True")
else:
   print("Echo test result: False")

#Освобождение ресурсов
del(gpc) 

С кодом примера можно ознакомиться тут.

4.6. Примеры создания и применения графов знаний

4.6.1. Пример выделения сообществ

В данном примере создается случайный граф, в котором задаются сообщества сильно-связанных компонент. Далее применяется алгоритм поиска центральности для получения оценок параметра virtex betweeness (центральность вершин). Этот параметр позволяет получить раскладку вершин внутри сообществ, имеющую топологический смысл: вершины с большей центральностью располагаются на в пространстве ближе к центру сообщества. Такой способ визуализации сообществ демонстрирует не только наличие сообществ, но и их структуру.

Ниже представлен код формирования случайного графа:

#Генерируем случайный связный граф с сообществами
gpc.start_handler("delete_graph")
#Создадим несколько собществ
def make_community(virtex,width):
    for u in range(virtex,virtex+width):
        for v in range(u+1,virtex+width):
            insert_edge(gpc,u,v,randrange(1,MAX_WEIGHT))
for community in range(10): make_community(community*VERTEX_COUNT//10, VERTEX_COUNT//30)
#Случайные слабые ребра
u = randrange(VERTEX_COUNT)
for edge in range(EDGE_COUNT):
    while True: 
        v = randrange(VERTEX_COUNT)
        if u!=v: break
    insert_edge(gpc,u,v,1)
    u=v

Функция insert_edge() выполняет передачу в gpc двух ребер: uv и vu:

#Функция вставки ребра в граф
def insert_edge(gpci, u, v, w):
    #ребро UV
    gpci.start_handler("insert_edges")
    gpci.mq_send_uint64(u) #вершина u 
    gpci.mq_send_uint64(v) #вершина v
    gpci.mq_send_uint64(w) #вес ребра   
    #ребро VU
    gpci.start_handler("insert_edges")
    gpci.mq_send_uint64(v) #вершина u 
    gpci.mq_send_uint64(u) #вершина v
    gpci.mq_send_uint64(w) #вес ребра   

Далее выполняется запуск алгоритма поиска центральности:

#Запустить расчет центральности
gpc.start_handler("btwc")

Следующее действие запускает одну из раскладок, выбранных пользователем:

match VISUALIZATION:
    case 1: #Cоздать inbox визуализацию на основе модулярности
        gpc.start_handler("create_communities_forest_vizualization")
    case 2: #Cоздать визуализацию на основе силового алгоритма Фрухтерамана-Рейнгольда
        gpc.start_handler("create_communities_forced_vizualization")
    case 3: #Cоздать спиральную визуализацию на основе центральности
        gpc.start_handler("create_centrality_spiral_visualization")
    case 4: #Cоздать визуализацию на основе центральности
        gpc.start_handler("create_centrality_visualization")
    case 5: #Cоздать матричную визуализацию на основе центральности
        gpc.start_handler("create_visualization")
        gpc.mq_send_uint64(int(math.sqrt(VERTEX_COUNT))) #сторона x
        gpc.mq_send_uint64(int(math.sqrt(VERTEX_COUNT))) #сторона y 

После завершения процедуры рендера его результат передается на сервер bokeh и выдается на экран.

Рисунок 23 — Визуализация графа на основе модулярности Ньюмана

Рисунок 24 — Визуализация на основе силового алгоритма Фрухтерамана-Рейнгольда

С кодом примера можно ознакомиться тут.

4.6.2. Пример визуализации графа деБрюйна музыкального произведения

Данный пример использует в качестве исходных данных музыкальные произведения, записанные в midi формате.

Формат midi файлов (смотри описание midi) представляет собой двоично кодированный файл, состоящий из последовательностей событий. Стандарт предусматривает 16 независимых каналов, состояние каждого из которых задается событиями. Канал 10 используется для записи партий ударных инструментов, а нотами в нем одируются ударные инструменты. Остальные каналы могут использоваться для представления событий музыкальных инструментов. Ниже показан пример событий одного канала:

Track 0: Acoustic Guitar
<meta message track_name name='Acoustic Guitar' time=0>
<note_on channel=0 note=58 velocity=72 time=0>
<note_off channel=0 note=58 velocity=64 time=50>
<note_on channel=0 note=60 velocity=72 time=0>
<note_off channel=0 note=60 velocity=64 time=30>
<note_on channel=0 note=61 velocity=72 time=0>
<note_off channel=0 note=61 velocity=64 time=30>
<note_on channel=0 note=60 velocity=72 time=0>
<note_off channel=0 note=60 velocity=0 time=30>
<note_on channel=0 note=70 velocity=72 time=0>
<note_off channel=0 note=70 velocity=64 time=100>
<note_on channel=0 note=66 velocity=72 time=0>
<note_off channel=0 note=66 velocity=64 time=33>
<note_on channel=0 note=72 velocity=72 time=0>
<note_off channel=0 note=72 velocity=64 time=38>
<note_on channel=0 note=66 velocity=72 time=0>
<note_off channel=0 note=66 velocity=64 time=21>
<note_on channel=0 note=70 velocity=72 time=0>
<note_off channel=0 note=70 velocity=64 time=41>
<note_on channel=0 note=66 velocity=72 time=0>
<note_off channel=0 note=66 velocity=64 time=33>
<note_on channel=0 note=70 velocity=72 time=0>
...

Событие note_on означает нажатую ноту (высота звучания задается полем note), а сообщение note_off - отпущенную ноту (отметим, что звучание к моменту отпускания ноты может завершиться, или продолжаться еще и после возникновения события note_off в зависимости от инструмента). Поле channel определяет канал, к которому относится сообщение. Поле velocity означает динамическую арактеристику атаки ноты (например, скорость/силу удара в ударных инструмента для клавишно-ударного инструмента фортепиано). Поле time означает время в настроенных временных единицах (долях ноты и количестве нот в секунду), прошедшее с момента предыдущего сообщения. Таким образом события в midi следую относительно друг друга. Если два события должны произойти одновременно, то для второго события должно быть задан time=0.

В рассмариваемом примере используется следующий конвейер обработки:

Рисунок 25 — Конвейер генерации музыки. Этап 1 - создание графов де Брюйна

  1. Объединение треков и каналов — инструменты исходного midi сводятся в один голос.
  2. Определение тональности — используется алгоритм на основе Байесовского классификатора (TemperleyKostkaPayne алгоритм)
  3. Восстановление цепочек аккордов — последовательность событий преобразуется в состояния
  4. Формирование кода вершины — каждое состояние кодируется в виде последовательности нот. Для состояния из Словаря уникальных кодов вершин получается уникальный ключ вершины.
  5. Запись вершины и ребра в граф - ключ вершины добавляется в граф деБрюйна и соединяется ребром с предыдущей вершиной.
  6. Граф ДеБрюйна передается в gpc и выполняется его анализ алгоритмом Ньюмана (выделение сообществ).
  7. Рендер графа передается в хост-подсистему для визуализации.

Рисунок 26 — Выделение сообществ (тем музыкального произведения) и визуализация графа деБрюйна. И.С.Бах, Токата ре-минор BWV565

С кодом примера можно ознакомиться тут.

4.7. Сборка и запуск примеров проектов

4.7.1. Пример 4. Использования языка python и библиотеки gpc64io для приемо-передачи данных между хост-подсистемой и sw_kernel

Пример демонстрирует основные механизмы инициализации гетерогенных ядер gpc и взаимодействие хост-подсистемы с Graph Processor Core, используются аппаратные очереди. Для хост подсистемы используется бибилиотека gpc64io

Установка

Для установки требуется рекурсивно клонировать репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab4.git
cd lab4

Сборка проекта

Следует выполнить команду:

make

Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.

Запуск проекта

Заупуск проекта осуществляется в ноутбуке lab4.ipynb.

Очистка проекта

Следует выполнить команду:

make clean

4.7.2. Пример 5. Демонстрация примения Jupyter ноутбуков и языка python для визуализации графов

Пример демонстрирует варианты анализа графов знаний и их визуализацию. Реализованы пять алгоритмов визуализации:

Для выбора варианта визуализации используется параметр VIZUALIZATION

Установка

Для установки требуется рекурсивно клонировать репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab5.git
cd lab5

Далее в облаке devlab.bmstu.ru необходимо открыть файл lab5.ipynb

Сборка sw-kernel части проекта

Следует выполнить команду:

make

Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.

Запуск проекта

Заупуск проекта осуществляется в ноутбуке lab5.ipynb.

Очистка проекта

Следует выполнить команду:

make clean

4.7.3. Пример 6. Демонстрация использования микропроцессора Леонард Эйлер для анализа графов знаний

Пример демонстрирует визуализацию графа гармоний музыкального произведения. Для формирования графа знаний используется запись музыкального произведения в формате midi. По последовательности аккордов строится граф ДеБрюйна с размером окна L, задаваемого параметрически в программе.

Установка

Для установки требуется рекурсивно клонировать репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab6.git
cd lab6

Далее в облаке devlab.bmstu.ru необходимо открыть файл lab6.ipynb

Сборка sw-kernel части проекта

Следует выполнить команду:

make

Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.

Запуск проекта

Заупуск проекта осуществляется в ноутбуке lab6.ipynb. Исходные midi файлы должны быть помещены в папку data/midi_sources/

Очистка проекта

Следует выполнить команду:

make clean

4.8. Индивидуальные задания

Ознакомиться с проектами Примера 4, Примера 5, Примера 6.

Выбрать пять музыкальных произведений различных композиторов и жанров. Произведение должно быть доступно в формате midi. Используя код Проекта 6 получить по две визуализации для каждого музыкального произведения.


5. Командный практикум. Обработка и визуализация графов в вычислительном комплексе Тераграф

Данная часть практикума выполняется командами от 5 до 10 человек. Задачей третьей части является создание музыкального произведения. Рассматриваются два подхода к созданию алгоритмической музыки на основе графов знаний:

  • Генерация музыки на основе графов ДеБрюйна.
  • Генерация музыки технологией структурного синтеза музыкальных произведений.

Результатом командной разработки является музыкальная композиция в формате mp3 длительностью от 30 до 60 секунд.

5.1. Подготовка данных для работы конвейера генерации музыки

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

На основе многих музыкальных произведений получаются многочисленные графы ДюБрюйна, которые далее объединяются в единый граф знаний. Участникам практикума предоставляется два варианта заранее подготовленных графов знаний:

  1. Библиотека цепочек аккордов и музыкальных фраз (168 тысяч отрывков в формате Midi). Графы ДеБрюйна с параметром L=5 собраны на облачной платформе devlab в /data/hackathon2023/PianoChords_dst_l5_concatenated.
  2. Библиотека музыкальных произведений (116 тысяч композиций всех жанров в формате Midi). Графы ДеБрюйна с параметром L=5 собраны на облачной платформе devlab в /data/hackathon2023/WorldMusic_dst_l5_concatenated.

Помимо этого можно формировать собственные графы деБрюйна с помощью кода Примера 6.

В процессе генерации музыки применяется нейросетевой перенос стиля (проект Петра Шумнова, ИУ7). Таким образом удается добиться естественного звучания фортепианного варианта исполнения. Однако, фортепиано является струнным клавишным инструментом, что определяет характер звучания. Для других вариантов звукоизвлечения такая стилизация может быть не пригодна (например, для духовых инструментов), поэтому целесообразно также формировать исходную (не стилизованную) композицию, удобную для последующей обработки.

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

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

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

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

Выбор музыкальных инструментов и звуковых шрифтов позволяет существенно улучшить звучание музыкального произведения. Звуковые шрифты могут быть скачаны из сети Интернет в формате *.sf2.

В итоге получается сформировать не только исходные Midi файлы всего произведения и отдельных инструментов, но и синтезировать файлы mp3.

Далее на полученное произведение может быть наложен ритм, изменен тем и прочее. Это выполняется в специальных редакторах (DAW).

5.2. Конвейер генерации музыки на основе графов ДеБрюйна

5.2.1. Стадия 1. Формирование графов де Брюйна

Описание стадии 1 подробно представлено в разделе: Примера 6.

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

5.2.2. Стадия 2. Обход графов де Брюйна

Для используется следующий конвейер обработки:

Рисунок 26 — Конвейер генерации музыки. Этап 2 - обход графов де Брюйна

  1. Объединение графов ДеБрюйна — поиск в директории всех графов с заданной тональностью. Граф записывается в ядро обработки графов вычислительного комплекса Тераграф
  2. Обход графа — проход по графу ДеБрюйна по случайному маршруту с заданным количеством шагов
  3. Восстановление аккорда — выборка полной записи о вершине из словаря аккордов, хранимого в GPC суперЭВМ Тераграф
  4. Запись потока событий — аккорд преобразуется в последовательность событий midi.

В пункте 1 можно указать любую из существующих тональностей (24 шт.). Будут выбраны только те цепочки аккордов, которые были встречены в произведении, написанном в указанной тональности. Последующая обработка выполняется автоматически и настроек не требует.

5.2.3. Стадия 3. Стилизация, многоголосие и синтез звука

Рисунок 27 — Конвейер генерации музыки. Этап 3 - стилизация, многоголосие и синтез звука

  1. Разделение на одноголосные треки — выделяются ноты в каждом аккорде: нижняя, верхняя, 2-я, 3-я, и т. д. для стилевой обработки одноголосных партий.
  2. Нейросетевой перенос стиля — используется стилевая композиция для определения вектора коэффициентов. Далее применяется итерационное изменение параметров Velocity обрабатываемого midi для минимизации ошибки
  3. Объединение midi — стилизованные голоса сводятся вместе
  4. Разделение на голоса — по настройкам пользователя выделяются верхний, нижний голаса, или голоса по заданному диапазону нот. Каждый голос записывается в отдельный канал.
  5. Синтезатор — для каждого голоса задается soundfont (.sf2) и номер инструмента. Звук генерируется с использованием timidity, далее wav кодируется в mp3 с использованием ffmpeg.

На данном этапе погут применять настойки генерации звукового файла, доступные для утилиты timidity. Например, могут быть выдраны звуковые шрифты, и параметры генерации каждого голоса. В приведенном ниже примере конфигурационного файла задается файл со звуковым шрифтом, номер банка и номер инструмента в нем (по терминологии звуковых шрифтов - номера программы). Например, усиление amp задается для каждого голоса в отдельности и позволяет сделать голос громче или тише. Параметр pan определяет смещение голоса относительно центра влево (-100) или вправо (+100) в стерео звучании. Допускаются также параметры left,right,center.

#Гитара
3 %font '/data/hackathon2023/soundfonts/STEEL_STRING_GUITAR.sf2' 0 0 pan=-80 amp=400
#Хор Ах
2 %font '/data/hackathon2023/soundfonts/KBH_Real_and_Swell_Choir.sf2' 0 2 pan=-20 amp=40
#Две виолончели
1 %font '/data/hackathon2023/soundfonts/Essential Keys-sforzando-v9.6.sf2' 0 29 pan=80 amp=120

Для подбора голосов используется утилита sf2_nfo, доступная на сервере devlab:

sf2_nfo '/data/hackathon2023/soundfonts/Essential Keys-sforzando-v9.6.sf2'

fluidsynth: warning: Failed to pin the sample data to RAM; swapping is possible.
fluidsynth: warning: No preset found on channel 9 [bank=128 prog=0]
bank: 0 prog: 0 name: Yamaha C5 Grand
bank: 0 prog: 1 name: Large Concert Grand
bank: 0 prog: 2 name: Mellow C5 Grand
bank: 0 prog: 3 name: Bright C5 Grand
bank: 0 prog: 4 name: Upright Piano
bank: 0 prog: 5 name: Chateau Grand
bank: 0 prog: 6 name: Mellow Chateau Grand
bank: 0 prog: 7 name: Dark Chateau Grand
bank: 0 prog: 8 name: Rhodes EP
bank: 0 prog: 9 name: DX7 EP
bank: 0 prog: 10 name: Rhodes Bell EP
bank: 0 prog: 11 name: Rotary Organ
bank: 0 prog: 12 name: Small Pipe Organ
bank: 0 prog: 13 name: Pipe Organ Full
bank: 0 prog: 14 name: Small Plein-Jeu
bank: 0 prog: 15 name: Flute Sml Plein-Jeu
bank: 0 prog: 16 name: FlutePad Sml Plein-J
bank: 0 prog: 17 name: Plein-jeu Organ Lge
bank: 0 prog: 18 name: Pad Plein-Jeu Large
bank: 0 prog: 19 name: Warm Pad
bank: 0 prog: 20 name: Synth Strings
bank: 0 prog: 21 name: Voyager-8
bank: 0 prog: 22 name: Full Strings Vel
bank: 0 prog: 23 name: Full Orchestra
bank: 0 prog: 24 name: Chamber Strings 1
bank: 0 prog: 25 name: Chamber Str 2 (SSO)
bank: 0 prog: 26 name: Violin (all around)
bank: 0 prog: 27 name: Two Violins
bank: 0 prog: 28 name: Cello 1
bank: 0 prog: 29 name: Cello 2 (SSO)
bank: 0 prog: 30 name: Trumpet
bank: 0 prog: 31 name: Trumpet+8 Vel
bank: 0 prog: 32 name: Tuba
bank: 0 prog: 33 name: Oboe
bank: 0 prog: 34 name: Tenor Sax
bank: 0 prog: 35 name: Alto Sax
bank: 0 prog: 36 name: Flute Expr+8 (SSO)
bank: 0 prog: 37 name: Flute 2
bank: 0 prog: 38 name: Timpani
bank: 0 prog: 39 name: Banjo 5 String
bank: 0 prog: 40 name: Steel Guitar
bank: 0 prog: 41 name: Nylon Guitar
bank: 0 prog: 42 name: Spanish Guitar
bank: 0 prog: 43 name: Spanish V Slide
bank: 0 prog: 44 name: Clean Guitar
bank: 0 prog: 45 name: LP Twin Elec Gtr
bank: 0 prog: 46 name: LP Twin Dynamic
bank: 0 prog: 47 name: Muted LP Twin
bank: 0 prog: 48 name: Jazz Guitar
bank: 0 prog: 49 name: Chorus Guitar
bank: 0 prog: 50 name: YamC5 + Pad
bank: 0 prog: 51 name: YamC5+LowStrings
bank: 0 prog: 52 name: YamC5+ChamberStr
bank: 0 prog: 53 name: YamC5+Strings
bank: 0 prog: 54 name: Chateau Grand+Pad
bank: 0 prog: 55 name: Ch Grand+LowStrings
bank: 0 prog: 56 name: Ch Grand+ChamberStr
bank: 0 prog: 57 name: Ch Grand+Strings
bank: 0 prog: 58 name: DX7+Pad
bank: 0 prog: 59 name: DX7+LowStrings
done

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

./midi2mp3.sh <Темп> <Максимальная длительность в секундах> <Исходный Midi> <Имя файла результата.mp3> <Конфигурационный файл>

Дополнительные опции можно найти тут: Timidity man

В итоге формируется композиция, подобная следующей:

Загадочная мелодия в Фа-миноре:

5.3. Пример 7. Использование микропроцессора Леонард Эйлер для генерации музыкальныых произведений

5.3.1. Общее описание

Пример демонстрирует генерацию последовательности аккордов по графу ДеБрюйна, созданному в примере 6. Для формирования графа знаний используется база графов ДеБрюйна, полученных на основе больших сборников midi произведений в различных тональностях (120 тысяч произведений и 168 тысяч аккоровых последовательностей). Граф ДеБрюйна обходится случайным образом и формируется композиция. Далее она разделяется на голоса и стилизуется с помощью нейронной сети. Таким образом получается midi файл с естественно выставленными значениями поля velocity (динамика звукоизвлечения). Далее производитя генерация звука с помощью консольного синтезатора timidity. Итогом работы является произвдеение в трех вариантах:

  • Фортепианное исполнение произведения.
  • Стилизованное исполнение с разделением на голоса и инструменты.
  • Оригинальное не стилизованное произведение с разделением на голоса и инструменты.

5.3.2. Установка

Для установки требуется рекурсивно клонировать репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab7.git
cd lab7

Далее в облаке devlab.bmstu.ru необходимо открыть файл lab7.ipynb

5.3.3. Зависимости

Зависимости для сборки проекта:

  • набор средст сборки riscv toolchain и экспорт исполняемых файлов в PATH

  • набор библиотек picolib и экспорт в C_INCLUDE_PATH

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

5.3.4. Сборка sw-kernel части проекта

Следует выполнить команду:

make

Результатом выполнения команды станет файлы sw_kernel_main.rawbinary в директории sw_kernel.

5.3.5. Запуск проекта

Заупуск проекта осуществлдяется в ноутбуке lab7.ipynb.

Для очистки следует выполнить команду:

make clean

Для удаление генерированных произведений следует выполнить команду:

make clear

5.4 Генерация музыки технологией структурного синтеза музыкальных произведений

5.4.1 Общее описание

Суть данного подхода в использовании для генерации одновременно двух различных графов - графа ритма и графа мелодии. В отличие от предыдущего метода, набор следующих друг за другом звуков получается посредством коротких проходов по данным графам с учетом большого количества параметров, таких как тональность, размер, предпочтительный темп и др. Таким образом генерируются короткие (длиной 1-2 музыкальных такта) ритмическо-мелодические последовательности, которые записываются в объекты класса “Мотив”. Далее управление процессом генерации все больше переходит от непосредственно графов к иерархической структуре, построенной по принципам музыкальной формы. Последовательность из двух стоящих друг за другом “Мотивов” включается в объект класса “Фраза”, представляющего собой, по сути, их обертку со своими методами управления музыкальной последовательностью. Точно так же “Фразы” включаются в “Предложения”, “Предложения” в “Периоды”, а “Периоды” в единую “Форму”, повышая уровень абстракции с каждым иерархическим переходом.

Подробно иерархический принцип работы описан в разделе 5.4.3. Инструкции и примеры работы приведены в блокнотах GPC_music.ipynb и Composing.ipynb в репозитории.

Рисунок 28 — Иерархический процесс структурного синтеза музыки

5.4.2 Установка

Клонируйте репозиторий:

git clone --recursive https://latex.bmstu.ru/gitlab/hackathon2023/lab8.git
cd lab8

Выполните команду:

make install

5.4.3 Принцип работы

Используемые графы

В работе используются графы двух типов - граф ритма (rhythm_graph) и граф мелодии (melody_graph). В графе ритма вершинами являются длительности звуков (1/2, 1/4 и т.д.), представленные в виде десятичной дроби. По графу ритма генерируется последовательность длительностей для мелодии. Граф мелодии содержит отдельные ноты в качестве вершин. В каждой вершине записано название ноты, а также ее целочисленное значение высоты звука для интерпретации в MIDI формате.

В обоих графах вершины соединены направленными ребрами, имеющими веса. Точных границ значений весов нет, считается, что 100 - вес достаточно большой, 10 - достаточно малый. Упрощенно, чем больше вес, тем больше вероятность пройти по этому ребру.

Алгоритм прохода по графу

В общем случае, проход по графу начинается со случайной вершины. Каждый раз, попадая в новую вершину, алгоритм формирует массив из весов ребер, выходящих из данной вершины. Каждый вес домножается на случайный кожффициент в диапазоне от 0,05 до 1,0. То ребро, чей вес оказался больше остальных после домножения на коэффициент, считается победителем, осуществляется переход в вершину, в которую ведет данное ребро. Коэффициенты генерируются в зависимости от задаваемого внешне сида (random_state).

Проход по графу ритма

Каждая вершина в графе ритма представляет собой длительность, зарезервированную для ноты либо для паузы, это указано в специальном ее свойстве duration_type, принимающем значения “note” либо “pause”.

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

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

Проход по графу мелодии

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

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

Каждая тональность состоит из ступеней, где каждая ступень - звук, входящий в данную тональность. Всего ступеней, как правило, 5 либо 7. 1, 3, 5 ступени считаются устойчивыми, остальные - неустойчивыми. Считается, что у неустойчивых ступеней существует тяготение к устойчивым. Помимо этого, отдельно существует тяготение 5 ступени к 1. Поэтому, когда алгоритм попадает в вершину графа, происходит определение текущей ступени. Если ступень имеет тяготение к другим ступеням, таковые ищутся среди соседей данной вершины. Если таковые находятся, веса ребер, ведущих к ним, увеличиваются на 100 в рамках данной итерации, создавая таким образом оное тяготение.

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

Генерация мотивов

В алгоритме генерации, используемом в данной работе, мотив является элементарной ритмо-мелодической единицей. Состоит из одного либо двух тактов. Представлен классом Motiff.

Параметры, используемые в конструкторе объекта, следующие:

  • gpci - объект, содержащий ссылку на используемое ядро GPC
  • rv_num - количество вершин в загруженном в GPC графе ритма
  • mv_num - количество вершин в загруженном в GPC графе мелодии
  • signature - размер, по умолчанию 4/4
  • signature_big_endian - предназначен для разметки сложных неквадратных размеров, типа 5/4. Если True, то размечает их в таком отношении, что больший размер оказывается впереди (5/4 = 3/4 + 2/4)
  • key_stages - структура, содержащая информацию о тональности и ступенях в ней. Опционален, но без него генерация будет происходить не в тональности
  • allow_alterations - Разрешение/запрет альтераций
  • cadence - завершенность, full - оканчивается на сильную долю и 1 ступень, incomplete - на слабую долю и 4, 5 ступень, half - на любую долю, на неустойчивую ступень, None - регуляция отсутствует;
  • random_state - сид для генерации случайных коэффициентов
  • rhythm - уже готовая последовательность длительностей
  • melody - уже готовая последовательность нот
  • melody_vertices - уже готовый список идентификаторов посещенных при генерации вершин;
  • movement - тип мелодического движения: ascend - восходящее, descend - нисходящее, wave - волнообразное, None - регуляция отсутствует;
  • movement_start_move - для типа движения wave, тип движения, с которого начинается волна (ascend - восходящее, descend - нисходящее);
  • phrase_role - роля мотива во фразе, opening - открывающий, должен начинаться с устойчивой ступени, closing - закрывающий, завершается согласно параметру cadence;
  • motiff - уже готовый мотив, дает возможность создания из копии.

Основной метод генерации - create(). Принимает следующие параметры:

  • start_duration - длительность, с которой следует начать, опционален;
  • start_note - имя ноты, с которой следует начать, оционален;
  • include_start_duration - включать ли стартовую длительность в итоговый результат, по умолчанию True;
  • include_start_note - включать ли стартовую ноту в итоговый результат, по умолчанию True;
  • rhythm_type - тип ритмики, может принимать следующие значения: slow, medium, fast, None. В зависимости от значения, при генерации алгоритм будет отдавать предпочтения меньшим или большим длительностям: slow - от 1/2 и больше, medium - от 1/8 до 1/4, fast - от 1/8 и меньше.

В ходе работы метода сначала генерируется последовательность длительностей, если она не была задана извне, затем над ней выполняются вариации, затем в зависимости от параметра cadence последняя длительность в мотиве изменяется таким образом, чтобы попасть на сильную либо слабую долю. Затем генерируется последовательность нот, если она не была задана извне. В конце мелодия также подстраивается в зависимости от параметра cadence, последняя нота “подтягивается” к нужной ступени.

Готовый мотив содержит последовательность длительностей и нот.

Еще один метод - develop(). Служит для развития сгенерированной музыкальной темы и осуществления вариаций. Параметры метода:

  • devtype - тип развития: melody_variance - мелодическая вариация, rhythm_variance - вариация ритма, full_variance - полная перегенерация, sequence - секвенция, повторение мелодии на другой высоте, rhythm_stretch - растяжение/сжатие длительностей, melodic_reversal - обращение, движение мелодических интервалов в обратную сторону, inversion - инверсия, движение мелодии в обратную сторону;
  • rhythm_vartype - тип ритмической вариации, unite - объединение малых длительностей в большие, divide - расщепление больших длительностей на малые;
  • melody_vartype - тип мелодической вариации, weak - изменить ноты на слабых долях, strong - на сильных долях;
  • melody_varproba - вероятность, с которой будет изменена каждая конкретная нота;
  • seq_semitones - для типа развития sequence, кол-во полутонов, на которое необходимо поднять/опустить мелодию;
  • rhythm_stretch_coef - для типа развития rhythm_stretch, коэффициент сжатия/растяжения длительностей.

Метод может быть применен несколько раз подряд с разными типами развития.

Генерация фраз

Фраза - последовательность, состоящая из двух мотивов. Представлена классом Phrase. Параметры конструктора во многом повторяют параметры класса Motiff. Появилась дополнительная опция для параметра movement: dwave. Теперь значение данного параметра wave будет означать, что в открывающем мотиве мелодическое движение будет идти в одну сторону, в закрывающем - в другую. Если же нужно, чтобы внутри мотивов сохранялось волнообразное движение, необходимо использовать значение dwave.

В рамках конструктора можно сразу задать оба мотива, пользуясь параметрами motiff_1 и motiff_2. Также их можно задать отдельным методом set_motiff(). Для класса Phrase также доступно создание из копии, посредством параметра конструктора phrase.

В конструкторе создаются два объекта класса Motiff, в которые сразу передаются касающиеся их параметры.

Основной метод генерации - create(), с уже описанными параметрами. Поочередно запускает генерацию каждого из мотивов.

Присутствует метод develop(), обладающий теми же параметрами, что и в классе Motiff. Развитие применяется ко всей фразе.

Генерация предложений

Предложение - единая музыкальная мысль, состоит из 2, реже 3, еще реже 4, фраз. Последняя фраза называется каденцией - завершением предложения. Представлена классом Sentence.

Параметры конструктора частично повторяют параметры предыдущих классов, собственные параметры:

  • key_note_name - имя ноты, от которой строится тональность;
  • key_tone_name - имя тональности (“major”, “natural_minor” и т.д.). Последние два параметра нужны для определения тональности и key_stages внутри самого предложения, без того, чтобы задавать их извне;
  • phrase_num - количество фраз;
  • movement - параметр, регулирующий мелодическое движение. Тип данных - tuple, первое значение в котором - общая тенденция мелодического движения в предложении: ascend, descend либо None, а второе - полнота выполнения этой тенденции: full - тип движения сохраняется во всех фразах предложения, incomplete - меняется от фразы к фразе, half - движение становится волнообразным в рамках каждой фразы, None - движение становится волнообразным в рамках каждого мотива, фактически, регуляция теряется;
  • repetitiveness - повторяемость. Он обязателен, в зависимости от данного параметра выстраивается структура предложения:
    • full” - генерируется первая фраза, вторая копируется из первой, к ней применяется мелодическая динамика (о ней ниже). Последующие фразы, если они есть, являются точными копиями первой и второй последовательно.
    • strong” - более мягкий вариант предыдущей опции. Все фразы, следующие за первой, получаются ее копированием и применением к ней любых видов вариаций;
    • opening” - открывающий мотив у всех фраз одинаковый, закрывающий же подвергается вариациям;
    • closing” - аналогично предыдущему, только наоборот: закрывающий мотив остается неизменным, открывающий варьируется;
    • weak” - каждая фраза генерируется отдельно, без копирования материала других. Вариации и динамики применимы к уже сгенерированным последовательностям.
  • melody_vartype - тип вариации мелодии, weak, strong либо None. Не работает для полной повторяемости;
  • melody_varproba - вероятность, с которой будет изменена каждая конкретная нота;
  • rhythm_dynamics - динамика ритма, также tuple. Первое значение - основная ритмическая тенденция, то же, что и параметр rhythm_type. Второе значение - динамическое изменение ритма в процессе развития темы: accel - ускорение, расщепление больших длительностей, deccel - замедление, слияние мелких длительностей в большие, None - нет динамики;
  • melody_dynamics - динамика мелодии, тип dict. Представляет собой словарь, в который заносятся типы вариаций мелодической динамики вместе с вероятностью их применения. Пример: {'inversion': 0.5, 'sequence': 0.2, 'melodic_reversal': 1.0}. Всего может быть три типа вариаций, все предствалены в примере. Применяются они в следующем порядке: сначала инверсия, затем мелодическое обращение, затем секвенция;
  • seq_semitones - уже известный параметр, количество полутонов секвенции;
  • sentence - параметр для создания из копии.

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

Метод change_key() позволяет поменять тональность уже сгенерированной мелодии. В метод могут быть переданы как наименование тональности (key_note_name, key_tone_name), так и сразу структура key_stages. Необязательный параметр direction (ascend, descend) определяет, в сторону повышения либо понижения должна отклониться мелодия.

Генерация периодов

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

В рамках реализации представлен классом Period.

  • Инкапсулирует объекты класса Sentence;
  • Тональность задается входным параметром key, представляющим собой кортеж, в котором обозначены нота и сама тональность (("C", "major"), ("D", "natural_minor") и т.д.);
  • Параметры repetitiveness, melody_dynamics работают аналогичным предложению образом;
  • Параметр structure задает тип структуры периода: rsquare - квадратный повторного строения (2 предложения, А + А1), nrsquare - квадратный неповторного строения (2 предложения, А + В), nsquare - неквадратный (2 предложения, 2 фразы + 3 фразы), third - троичный (3 предложения), double - сложный, двойной (4 предложения, А + В1 + А + В2), uniform - единый (1 предложение, от 4 до 16 фраз);
  • Период имеет роль - основную либо вспомогательную, задаваемую параметром role. main - основная, auxiliary - вспомогательная. Отличие одних от других состоит в том, что вспомогательные периоды короткие, не могут иметь другой структуры кроме rsquare, nrsquare и uniform с малым количеством предложений. Вспомогательные периоды нужны в качестве промежуточных звеньев между основными;
  • Ввиду сложности в рамках данной реализации регулировать квадратность периода в привычном музыкальном понимании, под квадратностью здесь понимается не количество тактов, соответствующее степени двойки, а количество фраз, соответствующее степени двойки;
  • В рамках периода происходит вывод музыкальных характеристик на качественно новый уровень, т.к. на структуре такого размера мы уже можем говорить о настроении музыки, ее характере и сложности. Нововведенные параметры периода перечислены ниже:
    • character - характер музыки, может быть активным active либо пассивным passive. Активный характер подразумевает постоянное развитие динамики, высокую вероятность мелодических вариаций, но при этом склонность придерживаться уже высказанных утверждений (звуки на сильных долях остаются на своих местах). Пассивный же характер, напротив, не склонен к динамичности, стремится остаться в той конфигурации, в который уже был, но при этом подчиняется насильным изменениям извне (вариации на сильные доли);
    • complexity - сложность. Отвечает за склонность алгоритма к формированию сложных ритмическо-мелодических конструкций. Имеет диапазон в 3 значения: low, medium, high. При низкой сложности предпочтение отдается медленным либо средним длительностям, мелодические и ритмические вариации сведены к минимуму, снижены вероятности мелодических динамик. Средняя сложность использует средние и быстрые длительности, вероятность вариаций и мелодических динамик остается в районе заданной. Высокая сложность представляет собой засилие быстрых длительностей, имеет большую вероятность вариаций и динамик.
    • mood - настрой, может быть позитивным positive, негативным negative и нейтральным neutral. При позитивном настрое напряжение либо все время понижается, либо падает к концу периода, при негатвном - возрастает, при нейтральном мелодическое движение волнообразно и не дает четкого ощущения роста/спада напряжения.

Генерация форм

Форма - крупная музыкальная единица, в некоторых случаях представляющая собой целое произведение. Инкапсулирует периоды, представлена классом Form.

  • Параметры key, repetitiveness, melody_dynamics, character, complexity, mood остаются без изменений по сравнению с периодом;
  • Основной параметр формы один - partition_type, отвечает за структуру всей формы в целом. Принимает следующие значения:
    • 2pnrep - простая двухчастная безрепризная форма. Состоит из двух периодов, оба из которых квадратные и генерируются по отдельности;
    • 2prep - простая двухчастная репризная форма. От безрепризной отличается тем, что второе предложение первого периода повторяется в качестве второго предложения второго периода, возможно с вариациями, зависит от сложности (complexity);
    • 3p - простая трехчастная форма, состоит из двух вспомогательных периодов (1 и 3) и одного основного (второй). Третий период повторяет первый, возможно, с вариациями. Если сложность низкая, то второй период представляет собой повторение первого в подчиненной тональности. По мере увеличения сложности усложняется структура второго периода - квадратный повторного строения, квадратный неповторного строения, двойной;
    • 35p - трехпятичастная форма. То же, что трехчастная, но второй и третий периоды повторяются;
    • ornamental - форма темы с вариациями (орнаментальная). Конструкция вида A + A1 + A2 + A3 + … Основная тема формируется в первом периоде, все последующие повторяют ее, внося изменения в любое предложение;
    • rondo - форма рондо. Крупная конструкция вида A + B + A + C + A + … Тема из первого периода (рефрен) повторяется раз за разом, между повторениями вставлены сторонние темы (эпизоды). В зависимости от сложности, эпизоды генерируются в основной либо подчиненных тональностях, от сложности также зависит размер всей конструкции.

Выгрузить получившиеся последовательности в MIDI можно методом make_midi(), присутствующим у каждого из классов Motiff, Phrase, Sentence. В метод передаются:

  • filename - имя выгружаемого файла;
  • track_name - название MIDI-трека.

Ручная инкапсуляция структур

Генерация музыкальных последовательностей “сверху вниз”, когда мы создаем объект (к, примеру, предложение) и сразу вызываем метод create(), это один из способов работы с данным пакетом. Никто не запрещает производить генерацию “снизу вверх”, когда отдельно создаются мотивы, фразы и т.д., которые потом можно инкапсулировать в объект, стоящий на более высокой иерархической ступени, и после работать уже с ним. Для этого у каждого класса есть ряд однотипных методов:

  • Для фразы: set_motiff(motiff, motiff_num) - объект класса “Мотив”, переданный в поле motiff, вставляется на позицию под номером motiff_num (не больше 2 для фразы). Обратите внимание, здесь, как и во всех прочих подобных методах, нумерация идет с 1;
  • Для предложения: set_phrase(phrase, phrase_num) и set_phrases(phrases), на случай, если необходимо задать не одну фразу, а сразу весь список;
  • Для периода: set_sentence(sentence, sentence)num), set_sentences(sentences);
  • Для формы: set_period(period, period_num), set_periods(periods).

Также, нет никаких ограничений на обращение непосредственно к объектам, находящимся внутри более крупных структур. Например:

sentence_1.phrases[1].develop()

Здесь мы вызвали метод develop() для второй фразы, содержащейся в пердложении sentence_1.