\documentclass[a4paper, final]{article}
\usepackage[14pt]{extsizes} 
\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[russian]{babel}
\usepackage{amsmath}
\usepackage{subfig}
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
\usepackage{ragged2e} 
\usepackage{setspace}
\usepackage{float}
\usepackage{stackengine}
\usepackage{indentfirst} 
\usepackage{moreverb} 
\renewcommand\verbatimtabsize{4\relax}
\renewcommand\listingoffset{0.2em} 
\renewcommand{\arraystretch}{1.4} 
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} 
\usepackage{listingsutf8}
\usepackage{xcolor} 
\usepackage{listings}
\usepackage{hyperref}
\usepackage{enumitem} 
\usepackage{verbatim}
\usepackage{tikz} 
\usepackage{amssymb}
\usepackage{pdflscape} %для pdf
\usepackage{pdfpages} %для pdf
\usepackage{graphicx}
\usepackage{tabularx}
\usepackage{multirow}
\usepackage{cmap}
\usepackage{longtable}
\usepackage{booktabs}

\definecolor{apricot}{HTML}{FFF0DA}
\definecolor{mygreen}{rgb}{0,0.6,0}
\definecolor{string}{HTML}{B40000} % цвет строк в коде
\definecolor{comment}{HTML}{008000} % цвет комментариев в коде
\definecolor{keyword}{HTML}{1A00FF} % цвет ключевых слов в коде
\definecolor{morecomment}{HTML}{8000FF} % цвет include и других элементов в коде
\definecolor{captiontext}{HTML}{FFFFFF} % цвет текста заголовка в коде
\definecolor{captionbk}{HTML}{999999} % цвет фона заголовка в коде
\definecolor{bk}{HTML}{FFFFFF} % цвет фона в коде
\definecolor{frame}{HTML}{999999} % цвет рамки в коде
\definecolor{brackets}{HTML}{B40000} % цвет скобок в коде

\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях

\hypersetup{colorlinks,
  allcolors=[RGB]{061 087 255}}



\textheight=24cm 
\textwidth=16cm
\oddsidemargin=0pt 
\topmargin=-1.5cm
\parindent=24pt 
\parskip=0pt 
\tolerance=2000 
\flushbottom


\lstset{
  language=Java, % Язык кода по умолчанию
  morekeywords={*,...}, % если хотите добавить ключевые слова, то добавляйте
  % Цвета
  keywordstyle=\color{keyword}\ttfamily\bfseries,
  %stringstyle=\color{string}\ttfamily,
  stringstyle=\ttfamily\color{red!50!brown},
  commentstyle=\color{comment}\ttfamily,
  morecomment=[l][\color{morecomment}]{\#},
  % Настройки отображения
  breaklines=true, % Перенос длинных строк
  basicstyle=\ttfamily\footnotesize, % Шрифт для отображения кода
  backgroundcolor=\color{bk}, % Цвет фона кода
  frame=single,xleftmargin=\fboxsep,xrightmargin=-\fboxsep, % Рамка, подогнанная к заголовку
  rulecolor=\color{frame}, % Цвет рамки
  tabsize=3, % Размер табуляции в пробелах
  % Настройка отображения номеров строк. Если не нужно, то удалите весь блок
  numbers=left, % Слева отображаются номера строк
  stepnumber=1, % Каждую строку нумеровать
  numbersep=5pt, % Отступ от кода
  numberstyle=\small\color{black}, % Стиль написания номеров строк
  % Для отображения русского языка
  extendedchars=true,
  showstringspaces=false, % не показывать пробелы в виде подчёркиваний
  keepspaces=true,        % сохранять исходное количество пробелов
  columns=fullflexible,
  literate={\\}{{\textbackslash}}1
  {Ö}{{\"O}}1
  {+}{{\texttt{+}}}1
  {-}{{\texttt{-}}}1
  {*}{{\texttt{*}}}1
  {/}{{\texttt{/}}}1
  {|}{{\texttt{|}}}1
  {(}{{\texttt{(}}}1
  {)}{{\texttt{)}}}1
  {[}{{[}}1
  {]}{{]}}1
  {^}{{\texttt{\^}}}1
  {\$}{{\texttt{\$}}}1
  {~}{{\textasciitilde}}1
  {а}{{\selectfont\char224}}1
  {б}{{\selectfont\char225}}1
  {в}{{\selectfont\char226}}1
  {г}{{\selectfont\char227}}1
  {д}{{\selectfont\char228}}1
  {е}{{\selectfont\char229}}1
  {ё}{{\"e}}1
  {ж}{{\selectfont\char230}}1
  {з}{{\selectfont\char231}}1
  {и}{{\selectfont\char232}}1
  {й}{{\selectfont\char233}}1
  {к}{{\selectfont\char234}}1
  {л}{{\selectfont\char235}}1
  {м}{{\selectfont\char236}}1
  {н}{{\selectfont\char237}}1
  {о}{{\selectfont\char238}}1
  {п}{{\selectfont\char239}}1
  {р}{{\selectfont\char240}}1
  {с}{{\selectfont\char241}}1
  {т}{{\selectfont\char242}}1
  {у}{{\selectfont\char243}}1
  {ф}{{\selectfont\char244}}1
  {х}{{\selectfont\char245}}1
  {ц}{{\selectfont\char246}}1
  {ч}{{\selectfont\char247}}1
  {ш}{{\selectfont\char248}}1
  {щ}{{\selectfont\char249}}1
  {ъ}{{\selectfont\char250}}1
  {ы}{{\selectfont\char251}}1
  {ь}{{\selectfont\char252}}1
  {э}{{\selectfont\char253}}1
  {ю}{{\selectfont\char254}}1
  {я}{{\selectfont\char255}}1
  {А}{{\selectfont\char192}}1
  {Б}{{\selectfont\char193}}1
  {В}{{\selectfont\char194}}1
  {Г}{{\selectfont\char195}}1
  {Д}{{\selectfont\char196}}1
  {Е}{{\selectfont\char197}}1
  {Ё}{{\"E}}1
  {Ж}{{\selectfont\char198}}1
  {З}{{\selectfont\char199}}1
  {И}{{\selectfont\char200}}1
  {Й}{{\selectfont\char201}}1
  {К}{{\selectfont\char202}}1
  {Л}{{\selectfont\char203}}1
  {М}{{\selectfont\char204}}1
  {Н}{{\selectfont\char205}}1
  {О}{{\selectfont\char206}}1
  {П}{{\selectfont\char207}}1
  {Р}{{\selectfont\char208}}1
  {С}{{\selectfont\char209}}1
  {Т}{{\selectfont\char210}}1
  {У}{{\selectfont\char211}}1
  {Ф}{{\selectfont\char212}}1
  {Х}{{\selectfont\char213}}1
  {Ц}{{\selectfont\char214}}1
  {Ч}{{\selectfont\char215}}1
  {Ш}{{\selectfont\char216}}1
  {Щ}{{\selectfont\char217}}1
  {Ъ}{{\selectfont\char218}}1
  {Ы}{{\selectfont\char219}}1
  {Ь}{{\selectfont\char220}}1
  {Э}{{\selectfont\char221}}1
  {Ю}{{\selectfont\char222}}1
  {Я}{{\selectfont\char223}}1
  {\#}{{\texttt{\#}}}1
  {\{}{{{\color{brackets}\{}}}1 % Цвет скобок {
  {\}}{{{\color{brackets}\}}}}1 % Цвет скобок }
}


\begin{document} 

\newcommand{\smalltexttt}[1]{\texttt{\small #1}}

\begin{center}
\hfill \break
\hfill \break
\normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
 федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
\normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt] 
\normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt] 
\normalsize{Направление: 02.03.01 Математика и компьютерные науки}\\

\hfill \break
\hfill \break
\hfill \break
\large{<<Архитектура суперкомпьютерных систем>>}\\
\large{Отчет по выполению лабораторной работы}\\
\large{Вариант 18}\\
\hfill \break
\vspace{2cm}


\end{center}
 
\small{ 
\begin{tabular}{lrrl}
\!\!\!Студент, & \hspace{2cm} & & \\
\!\!\!группы 5130201/20101 & \hspace{2cm} & \underline{\hspace{3cm}} & Тищенко А. А. \\\\
\!\!\!Преподаватель & \hspace{2cm} & \underline{\hspace{3cm}} & Чуватов М. В.\\\\
&&\hspace{5cm}
\end{tabular}
\begin{flushright}
<<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2026г.
\end{flushright}
}


\hfill \break
\begin{center} \small{Санкт-Петербург, 2026} \end{center}
\thispagestyle{empty} 

\begin{center}
\section*{РЕФЕРАТ}
\end{center}
% 79 с., 1 кн., 2 рис., 5 табл., 15 источн., 3 прил.


ГЕТЕРОГЕННЫЕ ВЫЧИСЛИТЕЛЬНЫЕ СИСТЕМЫ, ПАРАЛЛЕЛЬНОЕ ВЫЧИСЛЕНИЕ, MPI, GPU-ВЫЧИСЛЕНИЯ, OPENMPI, HYPER-V, CUDA, ВРЕМЕННЫЕ РЯДЫ, АГРЕГАЦИЯ ДАННЫХ.


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


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


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

\newpage

\begin{center}
\tableofcontents
\end{center}

\newpage

\begin{center}
\section*{ТЕРМИНЫ И ОПРЕДЕЛЕНИЯ}
\end{center}
\addcontentsline{toc}{section}{ТЕРМИНЫ И ОПРЕДЕЛЕНИЯ}
\begin{enumerate}
    \item \textbf{Гетерогенные вычислительные системы} — электронные системы, использующие различные типы вычислительных блоков. Они позволяют эффективно решать задачи за счёт использования компонентов с различными архитектурами.
    \item \textbf{GPU (Graphics Processing Unit)} — графический процессор, предназначенный для параллельной обработки данных, особенно эффективен для вычислений с высокой степенью параллелизма.
    \item \textbf{CPU (Central Processing Unit)} — центральный процессор общего назначения, оптимизированный для последовательных вычислений.
    \item \textbf{Hyper-V} — технология виртуализации от Microsoft, позволяющая создавать и управлять виртуальными машинами на Windows.
    \item \textbf{NFS (Network File System)} — протокол сетевой файловой системы, позволяющий разделять данные между узлами кластера.
    \item \textbf{MPI (Message Passing Interface)} — стандарт взаимодействия между процессами в параллельных вычислительных системах.
    \item \textbf{Slurm} — менеджер ресурсов и планировщик задач для кластерных систем.
    \item \textbf{Контейнеризация} — технология виртуализации, которая позволяет изолировать программное обеспечение в контейнерах для повышения переносимости и устранения конфликтов версий.
    \item \textbf{CUDA (Compute Unified Device Architecture)} — программная платформа от NVIDIA для разработки параллельных приложений на графических процессорах.
    \item \textbf{OpenMPI} — высокопроизводительная реализация стандарта MPI, обеспечивающая взаимодействие между процессами в распределённых системах.
    \item \textbf{MUNGE} — инструмент аутентификации, используемый для обеспечения безопасности в вычислительных кластерах.
    \item \textbf{Rank} — идентификатор процесса в системе MPI, используемый для определения роли процесса.
    \item \textbf{MPI\_Send, MPI\_Recv} — функции MPI для отправки и получения данных между процессами.
\end{enumerate}

\newpage
\begin{center}
\section*{ПЕРЕЧЕНЬ СОКРАЩЕНИЙ И ОБОЗНАЧЕНИЙ}
\end{center}
\addcontentsline{toc}{section}{ПЕРЕЧЕНЬ СОКРАЩЕНИЙ И ОБОЗНАЧЕНИЙ}

\begin{enumerate}
    \item \textbf{GPU} — Graphics Processing Unit (графический процессор).
    \item \textbf{CPU} — Central Processing Unit (центральный процессор).
    \item \textbf{NFS} — Network File System (сетевая файловая система).
    \item \textbf{MPI} — Message Passing Interface (интерфейс передачи сообщений).
    \item \textbf{Slurm} — Simple Linux Utility for Resource Management (утилита управления ресурсами для Linux).
    \item \textbf{CUDA} — Compute Unified Device Architecture (единая архитектура вычислений от NVIDIA).
    \item \textbf{OpenMPI} — Open Message Passing Interface (реализация интерфейса передачи сообщений).
    \item \textbf{MUNGE} — MUNGE Uid 'N' Gid Emporium (инструмент аутентификации для кластеров).
    \item \textbf{HPC} — High-Performance Computing (высокопроизводительные вычисления).
    \item \textbf{SSH} — Secure Shell (безопасная оболочка).
    \item \textbf{RAM} — Random Access Memory (оперативная память).
    \item \textbf{TCP/IP} — Transmission Control Protocol/Internet Protocol (протокол управления передачей/интернет-протокол).
    \item \textbf{OS} — Operating System (операционная система).
\end{enumerate}

\newpage
\begin{center}
\section*{ВВЕДЕНИЕ}
\end{center}
\addcontentsline{toc}{section}{ВВЕДЕНИЕ}

Использование графических ускорителей (GPU) наряду с центральными процессорами (CPU) является распространенной практикой в области параллельных вычислений на гетерогенных платформах. Гетерогенные вычислительные системы объединяют различные типы вычислительных блоков, что позволяет эффективно решать задачи, выбирая оптимальный вычислительный ресурс для каждого этапа обработки данных.

GPU и CPU имеют различную архитектуру и изначально проектировались для решения разных классов задач. GPU обладает большим количеством простых вычислительных ядер, оптимизированных для массово-параллельных операций, в то время как CPU имеет меньше ядер, но с более сложной логикой и лучшей производительностью на одно ядро. Совместное использование GPU и CPU осложняется рядом особенностей: они имеют раздельную память, различные адресные пространства, и для передачи данных требуется явное копирование через системные вызовы. Однако при правильном распределении задач и росте объёма данных использование GPU может значительно ускорить вычисления.

Обеспечение взаимодействия между узлами вычислительного кластера осуществляется с помощью Message Passing Interface (MPI) — стандарта передачи сообщений между процессами. Основными реализациями являются Open MPI и MPICH, которые позволяют процессам синхронизироваться и обмениваться данными.

Для GPU-вычислений используются специализированные технологии, такие как CUDA (для устройств NVIDIA), ROCm (для устройств AMD) и OpenCL (кроссплатформенный стандарт). В данной работе используется CUDA Toolkit и библиотека CUB для эффективной параллельной обработки данных на GPU.

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

Доступ к физическим суперкомпьютерным системам не всегда возможен, поэтому для разработки, тестирования и отладки параллельных приложений целесообразно создание собственного виртуального кластера. В данной работе используется технология виртуализации Hyper-V, разработанная Microsoft, которая позволяет создавать и управлять виртуальными машинами с возможностью проброса GPU-ресурсов. \cite{hyperv}

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

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


\newpage
\begin{center}
\section*{ПОСТАНОВКА ЗАДАЧИ}
\end{center}
\addcontentsline{toc}{section}{ПОСТАНОВКА ЗАДАЧИ}

В рамках лабораторных работ необходимо выполнить следующие задачи:
\begin{enumerate}
   \item Создание виртуальных машин с разнородными типами вычислительных ресурсов:
   \begin{itemize}
      \item CPU-узлы;
      \item GPU-узлы.
   \end{itemize}
   \item Настройка сети для связи хост-системы и виртуальных узлов;
   \item Решение задачи по выбранному варианту:
   \begin{itemize}
      \item Необходимо разработать параллельное приложение, задействующее вычислительные
ресурсы CPU-узлов и CUDA-узлов, используя механизм OpenMPI, выполняющее на
предоставленном наборе данных следующие действия. Задача разбита на 2 этапа, которые необходимо выполнить, используя разнородный тип вычислительных ресурсов;
      \item Этап 1. Агрегация данных: для временного ряда исторических данных о стоимости Bitcoin (исходные данные содержат информацию по каждым 10 секундам) необходимо выполнить группировку по дням и для каждого дня вычислить среднюю цену как математическое ожидание значений Low и High, а также минимальные и максимальные значения Open и Close.
      \item Этап 2. Поиск интервалов изменения цены: на основе дневных агрегированных данных необходимо выявить интервалы дат (начиная с начальной даты в наборе данных), в которых средняя дневная цена изменилась не менее чем на 10\% относительно начала интервала. Для каждого интервала необходимо вывести начальную и конечную даты, а также минимальные и максимальные значения Open и Close за все дни внутри интервала.
   \end{itemize}
\end{enumerate}

Файл с исходными данными доступен в свободном доступе в сети интернет\cite{kaggle}.

\newpage
\begin{center}
\section{ОСНОВНАЯ ЧАСТЬ РАБОТЫ}
\end{center}

\subsection{Создание виртуального кластера}
В рамках лабораторной работы необходимо создать виртуальный вычислительный кластер, использующий разнородный вид вычислителей: GPU и CPU. В рамках лабораторной работы необходимо создать виртуальные машины используя нативный механизм аппаратной виртуализации Windows: Hyper-V.

В качестве общей конфигурации виртуальной машины выбраны следующие параметры:
\begin{itemize}
    \item Ubuntu Server 22.04.05 LTS;
    \item выделенное ОЗУ: 4096 MB;
    \item число виртуальных процессоров: 2;
    \item имя виртуальной машины: "tishcpuX", "tishgpuX" (X - номер узла).
\end{itemize}

\subsubsection*{Создание виртуальной машины}
Для создания виртуальной машины (узла) для будущего кластера, необходимо выполнить следующие шаги:
\begin{enumerate}
    \item Открыть "Диспетчер Hyper-V".
    \item Открыть меню для сервера (В текущей работе сервер: XSPMAIN).
    \item Выбрать "Создать" $\rightarrow$ "Виртуальная машина...". Для мастера создания виртуальной машины выбрать следующие параметры:
    \begin{enumerate}
        \item "Укажите имя и местанохождение"
        \begin{itemize}
            \item Имя: \smalltexttt{"tishcpu1"};
            \item Сохранить виртуальную машину: \smalltexttt{V:$\backslash$ Virtual machines}.
        \end{itemize}
        \item "Укажите поколение"
        \begin{itemize}
            \item Выбрать \smalltexttt{"Поколение 2"} (Далее этот параметр поменять невозможно).
        \end{itemize}
        \item "Выделить память":
        \begin{itemize}
            \item "Память, выделяемое при запуске": \smalltexttt{4096 МБ};
            \item "Использовать для этой виртуальной машины динамическую память": \smalltexttt{Ставим галочку}.
        \end{itemize}
        \item "Настройка сети": пока данный раздел просто пропускается.
        \item "Подключить виртуальный жесткий диск":
        \begin{itemize}
            \item "Имя": tishcpu1.vhdx;
            \item "Расположение": \smalltexttt{V:$\backslash$Virtual machines$\backslash$Virtual Hard Disks$\backslash$};
            \item "Размер": \smalltexttt{100 ГБ}.
        \end{itemize}
        \item "Параметры установки":
        \begin{itemize}
            \item "Установить операционную систему из загрузочного образа": выбираем путь к \smalltexttt{*.iso} файлу образа ОС.
        \end{itemize}
    \end{enumerate}
\end{enumerate}

После создания виртуальной машины необходимо настроить параметры:
\begin{enumerate}
    \item В списке виртуальных машин в контекстном меню выбрать "Параметры...".
    \item Далее необходимо настроить следующие параметры:
    \begin{itemize}
        \item "Процессор"$\rightarrow$"Число виртуальных процессоров": \smalltexttt{2};
        \item "Безопасность"$\rightarrow$"Включить безопасную загрузку": \smalltexttt{Не ставим галочку};
        \item "SCSI-контроллер": DVD-дисковод;
        \item "Сетевой адаптер"$\rightarrow$"Виртуальный коммутатор": \smalltexttt{Default Switch};
        \item "Автоматическое действие при запуске": \smalltexttt{Ничего};
        \item "Память"$\rightarrow$"Включить динамическую память": \smalltexttt{Не ставим галочку}.
    \end{itemize}
    \item Нажать "Применить"$\rightarrow$"Ок".
\end{enumerate}


Для установки ОС необходимо запустить созданную ранее виртуальную машину. Для этого необходимо через консоль диспетчера Hyper-V и запустить ее:
\subsubsection*{Установка ОС}
При установке ОС выполняются следующие шаги:
\begin{enumerate}
    \item Выбор языка: \smalltexttt{"English"}.
    \item Для раскладки выбираем предлагаемую английскую раскладку.
    \item Тип установки: \smalltexttt{"Ubuntu Server"}.
    \item Настройки сети пока не трогаем.
    \item Настройки пространства памяти:
    \begin{enumerate}
        \item Выбираем опцию: \smalltexttt{"Custom Storage Layout"}.
        \item Для нашего свободного пространства: заводим swap и основное пространство памяти. (Размеры 50 и 4 ГБ)
    \end{enumerate}
    \item Настраиваем параметры профиля системы:
    \begin{itemize}
        \item "Your name": имя (например: \smalltexttt{Arity});
        \item "Your server's name": \smalltexttt{tishcpu1} (как у виртуальной машины);
        \item "Pick a username": \smalltexttt{arity};
        \item Пароль выбирается на свое усмотрение.
    \end{itemize}
\end{enumerate}

\subsubsection*{Создание пользователя root}
При создании виртуальной машины, не был сконфигурирован пользователь root. Для его создания необходимо выполнить следующие команды:




\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
aritytishcpu1:~$ sudo su -
root@tishcpu1:~# passwd
# Конфигурация пароля для пользователя root
\end{lstlisting}

\subsection{Конфигурация пакетов}
В рамках лабораторной работы необходимо установить следующие пакеты:
\begin{itemize}
    \item libopenmpi3 - пакет для библиотеки Open MPI;
    \item slurmd - пакет для управляющего задачами демона;
    \item openssh-client - клиент OpenSSH;
    \item openssh-server - сервер OpenSSH.
\end{itemize}
Для этого необходимо выполнить следующие команды:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
aritytishcpu1:~$ sudo apt update
aritytishcpu1:~$ sudo apt upgrade
aritytishcpu1:~$ sudo apt install libopenmpi3
aritytishcpu1:~$ sudo apt install slurmd
aritytishcpu1:~$ sudo apt install openssh-client
aritytishcpu1:~$ sudo apt install openssh-server
aritytishcpu1:~$ sudo apt clean
\end{lstlisting}

\cite{ubuntuDocs}
\begin{enumerate}
    \item \smalltexttt{apt update} - обновляет локальный индекс пакетов в системе, скачивая актуальную информацию о доступных пакетах из репозиториев, указанных в файлах \smalltexttt{/etc/apt/sources.list} и \smalltexttt{/etc/apt/sources.list.d/}. Это нужно для того, чтобы ОС знала о новых версиях пакетов и их зависимостях.
    
    \item \smalltexttt{apt upgrade} - обновляет установленные пакеты в системе. Предварительно рекомендуется обновить локальные индексы пакетов в системе.
    
    \item \smalltexttt{apt install <name>} - устанавливает в системе пакет с именем \smalltexttt{<name>}.

    \item \smalltexttt{apt clean} - удаляет все загруженные архивы пакетов из кеша APT, освобождая место на диске.
\end{enumerate}

\subsubsection*{Настройка сети на виртуальной машине}
Для настройки сети на виртуальных машинах необходимо настроить следующие файлы:
\begin{enumerate}
    \item \smalltexttt{/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg} - отключение управления сетевыми настройками через cloud-init, чтобы использовать другие способы конфигурации сети на системе.
    \item \smalltexttt{/etc/netplan/50-cloud-init.yaml} - используется для конфигурации сетевых настроек в системе через cloud-init, и обычно генерируется автоматически в облачных окружениях для настройки сети при запуске. Однако при настройке файла \smalltexttt{.../99-disable-network-config.cfg} можно самостоятельно настроить сетевые параметры так, чтобы система не генерировала файл автоматически.
\end{enumerate}
Конфигурация netplan:

\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
root@tishgpu1:/home/arity# cat /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
network: {config: disabled}
\end{lstlisting}


Содержимое файла {\small \smalltexttt{/etc/netplan/50-cloud-init.yaml}}:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
aritytishcpu1:~$ sudo vim /etc/netplan/50-cloud-init.yaml
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
            addresses:
            - 10.200.166.125/24
            nameservers:
                addresses:
                - 8.8.8.8
                search:
                - tish
            routes:
            -   to: default
                via: 10.200.166.254
    version: 2
\end{lstlisting}
\begin{enumerate}
    \item Интерфейс \smalltexttt{eth0}:
    \begin{itemize}
    \item \smalltexttt{addresses: [10.200.166.125/24]}: Интерфейсу \smalltexttt{eth0} присваивается IP-адрес \smalltexttt{10.200.166.125} с маской подсети \smalltexttt{255.255.255.0} (CIDR \smalltexttt{/24}).
    \item \smalltexttt{routes:}
    \begin{itemize}
        \item Указывает маршрут по умолчанию (default route), который направляет весь остальной трафик через шлюз \smalltexttt{10.200.166.254}.
    \end{itemize}
    \item \smalltexttt{nameservers:}
    \begin{itemize}
        \item Настраиваются DNS-серверы: \smalltexttt{8.8.8.8} (это публичные DNS от Google);
        \item Указываем \smalltexttt{tish} для параметра search.
    \end{itemize}
\end{itemize}
    \item Версия netplan:
    \begin{itemize}
    \item \smalltexttt{version: 2}: Указывает, что используется вторая версия формата Netplan.
\end{itemize}
\end{enumerate}


\subsection{Конфигурация сети}
Для взаимодействия с виртуальными машинами с помощью хост-узла, необходимо пробросить порты на настроенные ранее адреса. Для этого необходимо воспользоваться следующими PowerShell Cmdlet'ами:
\begin{enumerate}
    \item New-VMSwitch.
    \item New-NetIPAddress.
    \item New-NetNat.
    \item Add-NetNatStaticMapping.
\end{enumerate}
\subsubsection*{New-VMSwitch}
\smalltexttt{New-VMSwitch} - Создает новый виртуальный сетевой адаптер для виртуальных машин.

Принимаемые параметры:
\begin{itemize}
    \item \smalltexttt{SwitchName} - алиас для \smalltexttt{Name}. Уточняет имя виртуального сетевого адаптера. Обязательный параметр;
    \smalltexttt{SwitchType} - Уточняет тип создаваемого адаптера. Доступные значения для типа коммутатора — \smalltexttt{Internal} (внутренний) и \smalltexttt{Private} (частный). Чтобы создать \smalltexttt{External} (внешний) виртуальный коммутатор, нужно указать либо параметр \smalltexttt{NetAdapterInterfaceDescription}, либо \smalltexttt{NetAdapterName}, что автоматически установит тип коммутатора как \smalltexttt{External}.
    \smalltexttt{Internal} и \smalltexttt{Private} — это типы сетевых адаптеров, которые могут быть использованы для настройки виртуальных машин в Hyper-V:
    \begin{itemize}
        \item \smalltexttt{Internal} - позволяет виртуальной машине обмениваться данными с хостовой машиной и другими виртуальными машинами на том же хосте;
        \item \smalltexttt{External} - позволяет виртуальной машине общаться только с другими виртуальными машинами, но не имеет доступа к хосту или внешней сети;
    \end{itemize}
    \cite{newVmSwitch}
\end{itemize}

Создание виртуального сетевого адаптера в PowerShell:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> New-VMSwitch -SwitchName "TishNet" -SwitchType Internal
\end{lstlisting}

\subsubsection*{New-NetIPAddress}
\smalltexttt{New-NetIPAddress} - Создает новый IP-адрес и привязывает его к указанному сетевому интерфейсу.

Принимаемые параметры:
\begin{itemize}
    \item \smalltexttt{InterfaceAlias} - Указывает имя сетевого интерфейса, к которому будет привязан новый IP-адрес. Обязательный параметр;
    \item \smalltexttt{IPAddress} - Указывает IP-адрес, который нужно настроить. Обязательный параметр;
    \item \smalltexttt{PrefixLength} - Указывает длину префикса подсети для IP-адреса. Например, для маски подсети \smalltexttt{255.255.255.0} длина префикса равна \smalltexttt{24}. Обязательный параметр;
    \item \smalltexttt{DefaultGateway} - Указывает адрес шлюза по умолчанию, который будет использоваться для указанного IP-адреса. Необязательный параметр.
\end{itemize}

Пример настройки нового IP-адреса в PowerShell:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> New-NetIPAddress -InterfaceAlias "vEthernet (TishNet)" -IPAddress 10.200.166.254 -PrefixLength 24
\end{lstlisting}

Параметры:
\begin{itemize}
    \item \smalltexttt{InterfaceAlias} - Имя интерфейса, например, \smalltexttt{"vEthernet (TishNet)"}, к которому привязывается IP-адрес;
    \item \smalltexttt{IPAddress} - Указанный IP-адрес (например, \smalltexttt{10.200.166.254});
    \item \smalltexttt{PrefixLength} - Длина префикса подсети, например, \smalltexttt{24} для \smalltexttt{255.255.255.0};
    \item \smalltexttt{DefaultGateway} - Адрес шлюза по умолчанию (опционально).
\end{itemize}

\cite{newNetIpAddress}

Для проверки что IP-адресс корректно сконфигурирован можно воспользоваться команд-летом: {\small \smalltexttt{Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias "vEthernet (t1)"}}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias "vEthernet (TishNet)"
IPAddress         : 10.200.166.254
InterfaceIndex    : 10
InterfaceAlias    : vEthernet (TishNet)
AddressFamily     : IPv4
Type              : Unicast
PrefixLength      : 24
PrefixOrigin      : Manual
SuffixOrigin      : Manual
AddressState      : Preferred
ValidLifetime     :
PreferredLifetime :
SkipAsSource      : False
PolicyStore       : ActiveStore
\end{lstlisting}

% \iffalse
% \begin{comment}


% IPAddress         : 172.27.144.1
% InterfaceIndex    : 18
% InterfaceAlias    : vEthernet (Default Switch)
% AddressFamily     : IPv4
% Type              : Unicast
% PrefixLength      : 20
% PrefixOrigin      : Manual
% SuffixOrigin      : Manual
% AddressState      : Preferred
% ValidLifetime     :
% PreferredLifetime :
% SkipAsSource      : False
% PolicyStore       : ActiveStore

% IPAddress         : 192.168.0.166
% InterfaceIndex    : 16
% InterfaceAlias    : Ethernet
% AddressFamily     : IPv4
% Type              : Unicast
% PrefixLength      : 24
% PrefixOrigin      : Dhcp
% SuffixOrigin      : Dhcp
% AddressState      : Preferred
% ValidLifetime     : 21:30:01
% PreferredLifetime : 21:30:01
% SkipAsSource      : False
% PolicyStore       : ActiveStore

% IPAddress         : 127.0.0.1
% InterfaceIndex    : 1
% InterfaceAlias    : Loopback Pseudo-Interface 1
% AddressFamily     : IPv4
% Type              : Unicast
% PrefixLength      : 8
% PrefixOrigin      : WellKnown
% SuffixOrigin      : WellKnown
% AddressState      : Preferred
% ValidLifetime     :
% PreferredLifetime :
% SkipAsSource      : False
% PolicyStore       : ActiveStore


% PS C:\Windows\system32> New-NetIPAddress -InterfaceAlias "vEthernet (t1)" -IPAddress "10.200.166.254" -PrefixLength 24

% IPAddress         : 10.200.166.254
% InterfaceIndex    : 32
% InterfaceAlias    : vEthernet (t1)
% AddressFamily     : IPv4
% Type              : Unicast
% PrefixLength      : 24
% PrefixOrigin      : Manual
% SuffixOrigin      : Manual
% AddressState      : Tentative
% ValidLifetime     :
% PreferredLifetime :
% SkipAsSource      : False
% PolicyStore       : ActiveStore

% IPAddress         : 10.200.166.254
% InterfaceIndex    : 32
% InterfaceAlias    : vEthernet (t1)
% AddressFamily     : IPv4
% Type              : Unicast
% PrefixLength      : 24
% PrefixOrigin      : Manual
% SuffixOrigin      : Manual
% AddressState      : Invalid
% ValidLifetime     :
% PreferredLifetime :
% SkipAsSource      : False
% PolicyStore       : PersistentStore
% \end{comment}
% \fi

\subsubsection*{New-NetNat}

\smalltexttt{New-NetNat} - Создает новый NAT (Network Address Translation) для указанного интерфейса внутренней сети.

Принимаемые параметры:
\begin{itemize}
    \item \smalltexttt{Name} - Указывает имя NAT-объекта. Обязательный параметр;
    \item \smalltexttt{InternalIPInterfaceAddressPrefix} - Указывает адресный префикс внутренней сети, который будет использоваться для NAT. Обязательный параметр;
    \item \smalltexttt{ExternalIPInterfaceAddressPrefix} - Указывает адресный префикс внешней сети. Необязательный параметр;
    \item \smalltexttt{Description} - Добавляет описание к NAT-объекту. Необязательный параметр.
\end{itemize}

Пример создания NAT в PowerShell:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> New-NetNat -Name TishNat -InternalIPInterfaceAddressPrefix 10.200.166.0/24
\end{lstlisting}

Параметры:
\begin{itemize}
    \item \smalltexttt{Name} - Имя NAT-объекта, например, \smalltexttt{TishNat};
    \item \smalltexttt{InternalIPInterfaceAddressPrefix} - Префикс внутренней сети, например, \smalltexttt{10.200.166.0/24}.
\end{itemize}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> New-NetNat -Name TishNat -InternalIPInterfaceAddressPrefix 10.200.166.0/24
Name                             : TishNat
ExternalIPInterfaceAddressPrefix :
InternalIPInterfaceAddressPrefix : 10.200.166.0/24
IcmpQueryTimeout                 : 30
TcpEstablishedConnectionTimeout  : 1800
TcpTransientConnectionTimeout    : 120
TcpFilteringBehavior             : AddressDependentFiltering
UdpFilteringBehavior             : AddressDependentFiltering
UdpIdleSessionTimeout            : 120
UdpInboundRefresh                : False
Store                            : Local
Active                           : True
\end{lstlisting}
\cite{newNetNat}

\subsubsection*{Add-NetNatStaticMapping}
\smalltexttt{Add-NetNatStaticMapping} - Добавляет статическое сопоставление портов между внешними и внутренними адресами для NAT (Network Address Translation). Это позволяет направлять трафик, поступающий на внешний адрес и порт, к определенному внутреннему адресу и порту.

Принимаемые параметры:
\begin{itemize}
    \item \smalltexttt{NatName} - Указывает имя существующего объекта NAT, к которому применяется статическое сопоставление. Обязательный параметр;
    \item \smalltexttt{ExternalIPAddress} - Указывает внешний IP-адрес, на который поступает входящий трафик. Для разрешения любых IP-адресов можно использовать \smalltexttt{0.0.0.0/0}. Обязательный параметр;
    \item \smalltexttt{ExternalPort} - Указывает порт внешнего IP-адреса, на который будет перенаправляться трафик. Обязательный параметр;
    \item \smalltexttt{InternalIPAddress} - Указывает IP-адрес устройства внутри сети, к которому будет перенаправляться трафик. Обязательный параметр;
    \item \smalltexttt{InternalPort} - Указывает порт устройства внутри сети, который будет использоваться для трафика. Обязательный параметр;
    \item \smalltexttt{Protocol} - Указывает протокол (например, \smalltexttt{TCP} или \smalltexttt{UDP}), который будет использоваться для сопоставления. Обязательный параметр.
\end{itemize}

\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> Add-NetNatStaticMapping -NatName TishNat -ExternalIPAddress 0.0.0.0/0 -ExternalPort 22801 -InternalIPAddress 10.200.166.125 -InternalPort 22 -Protocol TCP
\end{lstlisting}
\cite{addNetNatStaticMapping}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> Add-NetNatStaticMapping -NatName TishNat -ExternalIPAddress 0.0.0.0/0 -ExternalPort 22801 -InternalIPAddress 10.200.166.125 -InternalPort 22 -Protocol TCP


StaticMappingID               : 0
NatName                       : TishNat
Protocol                      : TCP
RemoteExternalIPAddressPrefix : 0.0.0.0/0
ExternalIPAddress             : 0.0.0.0
ExternalPort                  : 22801
InternalIPAddress             : 10.200.166.125
InternalPort                  : 22
InternalRoutingDomainId       : {00000000-0000-0000-0000-000000000000}
Active                        : True



PS C:\Windows\system32> Add-NetNatStaticMapping -NatName TishNat -ExternalIPAddress 0.0.0.0/0 -ExternalPort 22802 -InternalIPAddress 10.200.166.126 -InternalPort 22 -Protocol TCP


StaticMappingID               : 1
NatName                       : TishNat
Protocol                      : TCP
RemoteExternalIPAddressPrefix : 0.0.0.0/0
ExternalIPAddress             : 0.0.0.0
ExternalPort                  : 22802
InternalIPAddress             : 10.200.166.126
InternalPort                  : 22
InternalRoutingDomainId       : {00000000-0000-0000-0000-000000000000}
Active                        : True



PS C:\Windows\system32> Add-NetNatStaticMapping -NatName TishNat -ExternalIPAddress 0.0.0.0/0 -ExternalPort 22803 -InternalIPAddress 10.200.166.127 -InternalPort 22 -Protocol TCP


StaticMappingID               : 4
NatName                       : TishNat
Protocol                      : TCP
RemoteExternalIPAddressPrefix : 0.0.0.0/0
ExternalIPAddress             : 0.0.0.0
ExternalPort                  : 22803
InternalIPAddress             : 10.200.166.127
InternalPort                  : 22
InternalRoutingDomainId       : {00000000-0000-0000-0000-000000000000}
Active                        : True


PS C:\Windows\system32> Add-NetNatStaticMapping -NatName TishNat -ExternalIPAddress 0.0.0.0/0 -ExternalPort 22804 -InternalIPAddress 10.200.166.128 -InternalPort 22 -Protocol TCP


StaticMappingID               : 5
NatName                       : TishNat
Protocol                      : TCP
RemoteExternalIPAddressPrefix : 0.0.0.0/0
ExternalIPAddress             : 0.0.0.0
ExternalPort                  : 22804
InternalIPAddress             : 10.200.166.128
InternalPort                  : 22
InternalRoutingDomainId       : {00000000-0000-0000-0000-000000000000}
Active                        : True
\end{lstlisting}


Для более быстрой настройки других виртуальных машин, можно скопировать tishcpu1, а при настройке первой tishgpu1 выполнить копирование в tishgpu2.

В результате создания виртуального кластеры были получены следующие виртуальные машины:
\begin{itemize}
    \item tishcpu1 - "главный" вычислительный узел;
    \item tishcpu2;
    \item tishgpu1;
    \item tishgpu2.
\end{itemize}

\subsection{Конфигурация ресурсов GPU}
Для конфигурации ресурсов GPU для узлов, необходимо воспользоваться следующими PowerShell Cmdlet'ами:
\begin{enumerate}
    \item Get-VMHostPartitionableGpu.
    \item Add-VMGpuPartitionAdapter.
    \item Set-VM.
    %\item Get-CimInstance.
\end{enumerate}
Также необходимо установить драйвера GPU, CUDA-toolkit. 

\subsubsection*{Get-VMHostPartitionableGpu}
\smalltexttt{Get-VMHostPartitionableGpu} - Возвращает список GPU, установленных на хосте Hyper-V, которые поддерживают разделение ресурсов (Partitioning). Эти GPU могут быть разделены и назначены виртуальным машинам для эффективного использования.

Принимаемые параметры:
\begin{itemize}
    \item \smalltexttt{-CimSession} (необязательный) - Позволяет указать удаленную сессию CIM (Common Information Model) для выполнения команды на другом компьютере. Если параметр не указан, команда выполняется локально.
    \item \smalltexttt{-ThrottleLimit} (необязательный) - Ограничивает количество одновременных операций. Если параметр не указан, используется системное значение по умолчанию.
\end{itemize}

Выходные данные команды включают информацию о GPU, например:
\begin{itemize}
    \item \smalltexttt{Name} - Имя GPU;
    \item \smalltexttt{TotalMemory} - Общий объем памяти GPU;
    \item \smalltexttt{AvailableMemory} - Доступный объем памяти GPU;
    \item \smalltexttt{Status} - Текущий статус GPU (например, \smalltexttt{OK}).
\end{itemize}

Пример использования команды:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> Get-VMHostPartitionableGpu
\end{lstlisting}

Результат выполнения:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32> Get-VMHostPartitionableGpu


Name                          : \\?\PCI#VEN_1002&DEV_164E&SUBSYS_D0001458&REV_C5#4&16012499&0&0041#{064092b3-625e-43bf-
                                9eb5-dc845897dd59}\GPUPARAV
ValidPartitionCounts          : {32}
PartitionCount                : 32
TotalVRAM                     : 1000000000
AvailableVRAM                 : 1000000000
SupportsIncomingLiveMigration : False
MinPartitionVRAM              : 0
MaxPartitionVRAM              : 1000000000
OptimalPartitionVRAM          : 1000000000
TotalEncode                   : 18446744073709551615
AvailableEncode               : 18446744073709551615
MinPartitionEncode            : 0
MaxPartitionEncode            : 18446744073709551615
OptimalPartitionEncode        : 18446744073709551615
TotalDecode                   : 1000000000
AvailableDecode               : 1000000000
MinPartitionDecode            : 0
MaxPartitionDecode            : 1000000000
OptimalPartitionDecode        : 1000000000
TotalCompute                  : 1000000000
AvailableCompute              : 1000000000
MinPartitionCompute           : 0
MaxPartitionCompute           : 1000000000
OptimalPartitionCompute       : 1000000000
CimSession                    : CimSession: .
ComputerName                  : XSPMAIN
IsDeleted                     : False

Name                          : \\?\PCI#VEN_10DE&DEV_2F04&SUBSYS_F3261569&REV_A1#740E0A73882DB04800#{064092b3-625e-43bf
                                -9eb5-dc845897dd59}\GPUPARAV
ValidPartitionCounts          : {32}
PartitionCount                : 32
TotalVRAM                     : 1000000000
AvailableVRAM                 : 1000000000
SupportsIncomingLiveMigration : False
MinPartitionVRAM              : 0
MaxPartitionVRAM              : 1000000000
OptimalPartitionVRAM          : 1000000000
TotalEncode                   : 18446744073709551615
AvailableEncode               : 18446744073709551615
MinPartitionEncode            : 0
MaxPartitionEncode            : 18446744073709551615
OptimalPartitionEncode        : 18446744073709551615
TotalDecode                   : 1000000000
AvailableDecode               : 1000000000
MinPartitionDecode            : 0
MaxPartitionDecode            : 1000000000
OptimalPartitionDecode        : 1000000000
TotalCompute                  : 1000000000
AvailableCompute              : 1000000000
MinPartitionCompute           : 0
MaxPartitionCompute           : 1000000000
OptimalPartitionCompute       : 1000000000
CimSession                    : CimSession: .
ComputerName                  : XSPMAIN
IsDeleted                     : False
\end{lstlisting}

\cite{getVmHostPartitionableGpu}
Эта команда является первым шагом в настройке GPU для виртуальных машин. Она позволяет проверить, какие GPU на хосте поддерживают разделение и сколько ресурсов доступно для выделения.

CUDA-ядра есть в \smalltexttt{VEN\_10DE\&DEV}. Это графическое устройство с ядром Nvidia 5070. 
\subsubsection*{Add-VMGpuPartitionAdapter}
\smalltexttt{Add-VMGpuPartitionAdapter} - Добавляет адаптер для разделения ресурсов GPU к указанной виртуальной машине (VM). Этот адаптер позволяет виртуальной машине использовать определенную часть вычислительных, графических и других ресурсов физического GPU.

Принимаемые параметры:
\begin{itemize}
    \item \smalltexttt{-VMName} - Указывает имя виртуальной машины, к которой будет добавлен GPU-адаптер. Обязательный параметр;
    \item \smalltexttt{-InstancePath} - Указывает путь к конкретному GPU, который будет разделен для использования виртуальной машиной. Обязательный параметр;
    \item \smalltexttt{-MinPartitionVRAM}, \smalltexttt{-MaxPartitionVRAM}, \smalltexttt{-OptimalPartitionVRAM} - Устанавливают минимальный, максимальный и оптимальный объем видеопамяти (VRAM), который будет доступен виртуальной машине;
    \item \smalltexttt{-MinPartitionEncode}, \smalltexttt{-MaxPartitionEncode}, \smalltexttt{-OptimalPartitionEncode} - Устанавливают минимальное, максимальное и оптимальное количество ресурсов для кодирования видео;
    \item \smalltexttt{-MinPartitionDecode}, \smalltexttt{-MaxPartitionDecode}, \smalltexttt{-OptimalPartitionDecode} - Устанавливают минимальное, максимальное и оптимальное количество ресурсов для декодирования видео;
    \item \smalltexttt{-MinPartitionCompute}, \smalltexttt{-MaxPartitionCompute}, 
    
    \smalltexttt{-OptimalPartitionCompute} - Устанавливают минимальное, максимальное и оптимальное количество вычислительных ресурсов (Compute), доступных виртуальной машине.
\end{itemize}
Пример использования:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
PS C:\Windows\system32>  Add-VMGpuPartitionAdapter -VMName tishgpu1 -InstancePath "\\?\PCI#VEN_10DE&DEV_2F04&SUBSYS_F3261569&REV_A1#740E0A73882DB04800#{064092b3-625e-43bf-9eb5-dc845897dd59}\GPUPARAV" -MinPartitionVRAM 100000000 -MaxPartitionVRAM 1000000000 -OptimalPartitionVRAM 1000000000 -MinPartitionCompute 100000000 -MaxPartitionCompute 1000000000 -OptimalPartitionCompute 1000000000

PS C:\Windows\system32> Get-VMGpuPartitionAdapter -VMName tishgpu1


InstancePath                  : \\?\PCI#VEN_10DE&DEV_2F04&SUBSYS_F3261569&REV_A1#740E0A73882DB04800#{064092b3-625e-43bf
                                -9eb5-dc845897dd59}\GPUPARAV
SupportsOutgoingLiveMigration : False
CurrentPartitionVRAM          : 1000000000
MinPartitionVRAM              : 100000000
MaxPartitionVRAM              : 1000000000
OptimalPartitionVRAM          : 1000000000
CurrentPartitionEncode        : 1000000000
MinPartitionEncode            :
MaxPartitionEncode            :
OptimalPartitionEncode        :
CurrentPartitionDecode        : 1000000000
MinPartitionDecode            :
MaxPartitionDecode            :
OptimalPartitionDecode        :
CurrentPartitionCompute       : 0
MinPartitionCompute           : 100000000
MaxPartitionCompute           : 1000000000
OptimalPartitionCompute       : 1000000000
PartitionId                   : 0
PartitionVfLuid               : 050760292
Name                          : Параметры раздела GPU
Id                            : Microsoft:AE124752-47A6-4788-9C91-8DAE6D45A744\5B2FD022-36CF-4FD9-83D7-D5B60274737E
VMId                          : ae124752-47a6-4788-9c91-8dae6d45a744
VMName                        : tishgpu1
VMSnapshotId                  : 00000000-0000-0000-0000-000000000000
VMSnapshotName                :
CimSession                    : CimSession: .
ComputerName                  : XSPMAIN
IsDeleted                     : False
VMCheckpointId                : 00000000-0000-0000-0000-000000000000
VMCheckpointName              :
\end{lstlisting}

\subsubsection*{Set-VM}
\smalltexttt{Set-VM} - Изменяет параметры виртуальной машины (VM), включая настройки памяти, процессора и других ресурсов.

Принимаемые параметры:
\begin{itemize}
    \item \smalltexttt{-VMName} - Указывает имя виртуальной машины, для которой изменяются настройки. Обязательный параметр;
    \item \smalltexttt{-GuestControlledCacheTypes} - Указывает, разрешено ли гостевой операционной системе управлять типами кэширования. Значение \smalltexttt{\$true} включает эту возможность;
    \item \smalltexttt{-LowMemoryMappedIoSpace} - Устанавливает объем выделенного адресного пространства для низкоуровневого памяти, используемой устройствами, например, 3GB;
    \item \smalltexttt{-HighMemoryMappedIoSpace} - Устанавливает объем выделенного адресного пространства для высокоуровневого памяти, например, 32GB;
    \item \smalltexttt{-ProcessorCount} (необязательный) - Позволяет задать количество процессоров, доступных виртуальной машине;
    \item \smalltexttt{-DynamicMemory} (необязательный) - Разрешает использование динамической памяти для виртуальной машины.
\end{itemize}

Пример использования команды:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
Set-VM -VMName tishgpu1 -GuestControlledCacheTypes $true -LowMemoryMappedIoSpace 3GB -HighMemoryMappedIoSpace 32GB
\end{lstlisting}

Команда изменяет настройки виртуальной машины \smalltexttt{tishgpu1}, разрешая гостевой ОС управлять типами кэширования и выделяя 3GB для низкоуровневого адресного пространства и 32GB для высокоуровневого адресного пространства.

\cite{setVm}


% \iffalse
% \begin{comment}
    
% \begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
% PS C:\Windows\system32> Get-CimInstance -ClassName Win32_VideoController -Property *

% Caption                      : Intel(R) UHD Graphics 750
% Description                  : Intel(R) UHD Graphics 750
% InstallDate                  :
% Name                         : Intel(R) UHD Graphics 750
% Status                       : OK
% Availability                 : 8
% ConfigManagerErrorCode       : 0
% ConfigManagerUserConfig      : False
% CreationClassName            : Win32_VideoController
% DeviceID                     : VideoController1
% ErrorCleared                 :
% ErrorDescription             :
% LastErrorCode                :
% PNPDeviceID                  : PCI\VEN_8086&DEV_4C8A&SUBSYS_7D171462&REV_04\3&11583659&0&10
% PowerManagementCapabilities  :
% PowerManagementSupported     :
% StatusInfo                   :
% SystemCreationClassName      : Win32_ComputerSystem
% SystemName                   : 4E305-10
% MaxNumberControlled          :
% ProtocolSupported            :
% TimeOfLastReset              :
% AcceleratorCapabilities      :
% CapabilityDescriptions       :
% CurrentBitsPerPixel          :
% CurrentHorizontalResolution  :
% CurrentNumberOfColors        :
% CurrentNumberOfColumns       :
% CurrentNumberOfRows          :
% CurrentRefreshRate           :
% CurrentScanMode              :
% CurrentVerticalResolution    :
% MaxMemorySupported           :
% MaxRefreshRate               :
% MinRefreshRate               :
% NumberOfVideoPages           :
% VideoMemoryType              : 2
% VideoProcessor               : Intel(R) UHD Graphics Family
% NumberOfColorPlanes          :
% VideoArchitecture            : 5
% VideoMode                    :
% AdapterCompatibility         : Intel Corporation
% AdapterDACType               : Internal
% AdapterRAM                   : 2147479552
% ColorTableEntries            :
% DeviceSpecificPens           :
% DitherType                   :
% DriverDate                   : 19.08.2024 3:00:00
% DriverVersion                : 32.0.101.5972
% ICMIntent                    :
% ICMMethod                    :
% InfFilename                  : oem47.inf
% InfSection                   : iRKLD_w10_DS
% InstalledDisplayDrivers      : C:\Windows\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_4ec1a03daa49235f\igdum
%                                dim64.dll,C:\Windows\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_4ec1a03daa49
%                                235f\igd10iumd64.dll,C:\Windows\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_4
%                                ec1a03daa49235f\igd10iumd64.dll,C:\Windows\System32\DriverStore\FileRepository\iigd_dch.
%                                inf_amd64_4ec1a03daa49235f\igd12umd64.dll
% Monochrome                   : False
% ReservedSystemPaletteEntries :
% SpecificationVersion         :
% SystemPaletteEntries         :
% VideoModeDescription         :
% PSComputerName               :
% CimClass                     : root/cimv2:Win32_VideoController
% CimInstanceProperties        : {Caption, Description, InstallDate, Name...}
% CimSystemProperties          : Microsoft.Management.Infrastructure.CimSystemProperties






% Caption                      : NVIDIA GeForce RTX 3060
% Description                  : NVIDIA GeForce RTX 3060
% InstallDate                  :
% Name                         : NVIDIA GeForce RTX 3060
% Status                       : OK
% Availability                 : 3
% ConfigManagerErrorCode       : 0
% ConfigManagerUserConfig      : False
% CreationClassName            : Win32_VideoController
% DeviceID                     : VideoController2
% ErrorCleared                 :
% ErrorDescription             :
% LastErrorCode                :
% PNPDeviceID                  : PCI\VEN_10DE&DEV_2487&SUBSYS_40741458&REV_A1\4&D0467E6&0&0008
% PowerManagementCapabilities  :
% PowerManagementSupported     :
% StatusInfo                   :
% SystemCreationClassName      : Win32_ComputerSystem
% SystemName                   : 4E305-10
% MaxNumberControlled          :
% ProtocolSupported            :
% TimeOfLastReset              :
% AcceleratorCapabilities      :
% CapabilityDescriptions       :
% CurrentBitsPerPixel          : 32
% CurrentHorizontalResolution  : 1920
% CurrentNumberOfColors        : 4294967296
% CurrentNumberOfColumns       : 0
% CurrentNumberOfRows          : 0
% CurrentRefreshRate           : 60
% CurrentScanMode              : 4
% CurrentVerticalResolution    : 1080
% MaxMemorySupported           :
% MaxRefreshRate               : 165
% MinRefreshRate               : 50
% NumberOfVideoPages           :
% VideoMemoryType              : 2
% VideoProcessor               : NVIDIA GeForce RTX 3060
% NumberOfColorPlanes          :
% VideoArchitecture            : 5
% VideoMode                    :
% AdapterCompatibility         : NVIDIA
% AdapterDACType               : Integrated RAMDAC
% AdapterRAM                   : 4293918720
% ColorTableEntries            :
% DeviceSpecificPens           :
% DitherType                   : 0
% DriverDate                   : 14.08.2024 3:00:00
% DriverVersion                : 32.0.15.6094
% ICMIntent                    :
% ICMMethod                    :
% InfFilename                  : oem43.inf
% InfSection                   : Section070
% InstalledDisplayDrivers      : C:\Windows\System32\DriverStore\FileRepository
% \nv_dispi.inf_amd64_20ae8f14a487d5db\nvld
%                                umdx.dll,C:\Windows\System32\DriverSt
%                                ore\FileRepository\nv_dispig.inf_amd64_0afec3f20500
%                                14a0\nvldumdx.dll,C:\Windows\System32
%                                \DriverStore\FileRepository\nv_dispig.inf_amd64_0af
%                                ec3f2050014a0\nvldumdx.dll,C:\Windows\System32\DriverStore\FileRepository\nv_dispig.inf_
%                                amd64_0afec3f2050014a0\nvldumdx.dll
% Monochrome                   : False
% ReservedSystemPaletteEntries :
% SpecificationVersion         :
% SystemPaletteEntries         :
% VideoModeDescription         : Цвета: 1920 x 1080 x 4294967296
% PSComputerName               :
% CimClass                     : root/cimv2:Win32_VideoController
% CimInstanceProperties        : {Caption, Description, InstallDate, Name...}
% CimSystemProperties          : Microsoft.Management.Infrastructure.CimSystemProperties
% \end{lstlisting}

% \end{comment}
% \fi
\subsubsection*{Установка ПО}
Для установки драйверов графического устройства необходимо скопировать их с хост-устройства с помощью {\small \smalltexttt{scp}}.

Выполнение копирования драйверов GPU:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# Копируем содержимое папки с драйверами с хоста на виртуальную машину через SCP
# -r: копирование рекурсивно (включая подкаталоги)
# -P 22803: указание порта для подключения (22803)
scp -r -P 22803 C:\WINDOWS\System32\DriverStore\FileRepository\nv_dispi.inf_amd64_20ae8f14a487d5db arity@127.0.0.1:/tmp/

# Создаем директорию для драйверов в WSL (если еще не существует)
root@tishgpu1:/tmp# mkdir -p /usr/lib/wsl/drivers

# Переходим в созданную директорию
root@tishgpu1:/tmp# cd /usr/lib/wsl/drivers

# Проверяем содержимое директории (на данный момент она пуста)
root@tishgpu1:/usr/lib/wsl/drivers# ls

# Переходим на уровень выше, в директорию WSL
root@tishgpu1:/usr/lib/wsl/drivers# cd ..

# Проверяем содержимое директории /usr/lib/wsl (содержит только папку drivers)
root@tishgpu1:/usr/lib/wsl# ls drivers

# Создаем новую директорию lib в WSL (может быть нужна для других целей)
root@tishgpu1:/usr/lib/wsl# mkdir lib

# Проверяем, что теперь в /usr/lib/wsl есть две папки: drivers и lib
root@tishgpu1:/usr/lib/wsl# ls drivers lib

# Перемещаем скачанную папку драйвера из /tmp в директорию drivers
root@tishgpu1:/usr/lib/wsl# mv /tmp/nv_dispi.inf_amd64_20ae8f14a487d5db/ /usr/lib/wsl/drivers/

# Проверяем содержимое директории /usr/lib/wsl (папка drivers содержит перемещенные данные)
root@tishgpu1:/usr/lib/wsl# ls
drivers  lib

# Убеждаемся, что в папке drivers теперь находится папка с драйверами
root@tishgpu1:/usr/lib/wsl# ls drivers/
nv_dispi.inf_amd64_20ae8f14a487d5db

# Операция завершена, драйверы находятся в правильной директории
root@tishgpu1:/usr/lib/wsl#
\end{lstlisting}

Конфигурация драйверов на виртуальной машине:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# Копируем библиотеку lib из Windows на виртуальную машину через SCP
# -r: копирование рекурсивно
# -P 22803: указание порта для подключения
PS C:\Windows\system32> scp -r -P 22803 C:\Windows\System32\lxss\lib arity@127.0.0.1:/tmp/

# Перемещаем скопированную библиотеку в директорию WSL
root@tishgpu1:/usr/lib/wsl# mv /tmp/lib/ /usr/lib/wsl/

# Проверяем содержимое директории /usr/lib/wsl, чтобы убедиться, что библиотека перемещена
root@tishgpu1:/usr/lib/wsl# ls
drivers  lib

# Проверяем содержимое директории lib в WSL
root@tishgpu1:/usr/lib/wsl# ls lib
libcudadebugger.so.1  libd3d12core.so  libnvcuvid.so.1        libnvidia-ml.so.1           libnvwgf2umx.so
libcuda.so            libd3d12.so      libnvdxdlkernels.so    libnvidia-opticalflow.so    nvidia-smi
libcuda.so.1          libdxcore.so     libnvidia-encode.so    libnvidia-opticalflow.so.1
libcuda.so.1.1        libnvcuvid.so    libnvidia-encode.so.1  libnvoptix.so.1

# Устанавливаем права доступа на директорию WSL: только чтение и выполнение (555)
root@tishgpu1:/usr/lib# chmod -R 555 wsl/

# Устанавливаем владельцем директории WSL пользователя root и группу root
root@tishgpu1:/usr/lib# chown -R root:root wsl/

# Редактируем файл ld.so.conf.d для добавления пути к библиотекам WSL
root@tishgpu1:/usr/lib/wsl/lib# vim /etc/ld.so.conf.d/ld.wsl.conf

# Добавляем путь к библиотекам в файл ld.wsl.conf
/usr/lib/wsl/lib

# Применяем изменения с помощью ldconfig, чтобы обновить кэш динамических библиотек
root@tishgpu1:/usr/lib/wsl/lib# ldconfig
/sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link

# Редактируем или создаем файл /etc/profile.d/wsl.sh для добавления пути в системный PATH
root@tishgpu1:/usr/lib# vim /etc/profile.d/wsl.sh

# Проверяем содержимое файла wsl.sh, чтобы убедиться в правильности пути
root@tishgpu1:/usr/lib/wsl/lib# cat /etc/profile.d/wsl.sh
export PATH=$PATH:/usr/lib/wsl/lib

# Делаем файл wsl.sh исполняемым
root@tishgpu1:/usr/lib# chmod +x /etc/profile.d/wsl.sh
\end{lstlisting}

Сборка ядра с использованием готового shell скрипта:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# Загружаем и выполняем скрипт установки DXGKRNL (DirectX Kernel) через DKMS (Dynamic Kernel Module Support):
# - curl: утилита для загрузки данных из интернета.
#   -fsSL:
#     -f: завершить с ошибкой, если произошел сбой HTTP-запроса.
#     -s: тихий режим, скрывающий прогресс загрузки.
#     -S: отображение ошибок даже в тихом режиме.
#     -L: автоматическое следование за перенаправлениями.
# https://content.staralt.dev/dxgkrnl-dkms/main/install.sh: URL скрипта установки.
# |: передача загруженного содержимого скрипта как ввода следующей команде.
# sudo bash -es:
#   - bash: запускает загруженный скрипт в интерпретаторе команд bash.
#   - -e: завершает выполнение скрипта при любой ошибке.
#   - -s: интерпретирует данные, подаваемые через стандартный ввод, как сценарий bash.
aritytishgpu1:~$ curl -fsSL https://content.staralt.dev/dxgkrnl-dkms/main/install.sh | sudo bash -es

Target Kernel Version: 5.15.0-124-generic

Installing dependencies...
\end{lstlisting}

Для проверки корректности работы GPU на виртуальном узле можно воспользоваться утилитами:
\begin{itemize}
    \item lspci;
    \begin{itemize}
        \item Используется для отображения списка PCI-устройств, подключенных к системе. Ключ -v выводит подробную информацию о каждом устройстве. В данном случае, команда подтверждает наличие GPU, который использует драйвер dxgkrnl;
    \end{itemize}
    \item nvidia-smi;
    \begin{itemize}
        \item Утилита для управления и мониторинга графических карт NVIDIA. Показывает информацию о версии драйвера, состоянии GPU, использовании памяти, температуре и запущенных процессах. В данном примере отображается статус GPU NVIDIA GeForce RTX 5070, использование 1543 MiB памяти и базовая загрузка GPU;
    \end{itemize}
\end{itemize}

\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
aritytishgpu1:~$ lspci -v
c556:00:00.0 3D controller: Microsoft Corporation Device 008e
        Physical Slot: 4111917767
        Flags: bus master, fast devsel, latency 0, NUMA node 0
        Capabilities: <access denied>
        Kernel driver in use: dxgkrnl
        Kernel modules: dxgkrnl


aritytishgpu1:~$ nvidia-smi
Sat Jan  3 17:25:15 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.102.01             Driver Version: 581.80         CUDA Version: 13.0     |
+-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 5070        On  |   00000000:01:00.0  On |                  N/A |
|  0%   57C    P0             36W /  250W |    1543MiB /  12227MiB |      6%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI              PID   Type   Process name                        GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
|  No running processes found                                                             |
+-----------------------------------------------------------------------------------------+
\end{lstlisting}

% \iffalse
% \begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
% C:\Windows\System32\DriverStore\FileRepository\nv_dispi.inf_amd64_20ae8f14a487d5db\

% PS C:\Windows\system32> scp -P 41122 -r C:\Windows\System32\DriverStore\FileRepository\nv_dispi.inf_amd64_20ae8f14a487d5db arity@127.0.0.1:/tmp/drv

% PS C:\Windows\system32> scp -P 41122 -r C:\Windows\System32\lxss\lib arity@127.0.0.1:/tmp/drv-lib

% root@tishgpu1:/tmp# ls -l
% total 36
% drwx---r-x 5 arity arity 12288 Nov 19 10:27 drv
% drwx------ 2 arity arity  4096 Nov 19 10:32 drv-lib

% root@tishgpu1:/tmp# mkdir -p /usr/lib/wsl/lib

% root@tishgpu1:/usr/lib/wsl# cp -r /tmp/drv-lib/ /usr/lib/wsl/
% root@tishgpu1:/usr/lib/wsl# ls
% drv-lib
% root@tishgpu1:/usr/lib/wsl# mv drv-lib/ lib
% root@tishgpu1:/usr/lib/wsl# ls
% lib

% root@tishgpu1:/usr/lib/wsl# cp -r /tmp/drv /usr/lib/wsl/lib/
% root@tishgpu1:/usr/lib/wsl# ls
% lib
% root@tishgpu1:/usr/lib/wsl# ls lib/
% drv                   libdxcore.so           libnvidia-opticalflow.so
% libcudadebugger.so.1  libnvcuvid.so          libnvidia-opticalflow.so.1
% libcuda.so            libnvcuvid.so.1        libnvoptix.so.1
% libcuda.so.1          libnvdxdlkernels.so    libnvwgf2umx.so
% libcuda.so.1.1        libnvidia-encode.so    nvidia-smi
% libd3d12core.so       libnvidia-encode.so.1
% libd3d12.so           libnvidia-ml.so.1
% root@tishgpu1:/usr/lib/wsl/lib# mv drv/ drivers
% root@tishgpu1:/usr/lib/wsl/lib# ls -l
% total 242276
% drwx---r-x 5 root root     12288 Nov 19 10:47 drivers
% -rw-r--r-- 1 root root  10182600 Nov 19 10:46 libcudadebugger.so.1
% -rw-r--r-- 1 root root    162608 Nov 19 10:46 libcuda.so
% -rw-r--r-- 1 root root    162608 Nov 19 10:46 libcuda.so.1
% -rw-r--r-- 1 root root    162608 Nov 19 10:46 libcuda.so.1.1
% -rw-r--r-- 1 root root   5913128 Nov 19 10:46 libd3d12core.so
% -rw-r--r-- 1 root root    789192 Nov 19 10:46 libd3d12.so
% -rw-r--r-- 1 root root    832144 Nov 19 10:46 libdxcore.so
% -rw-r--r-- 1 root root  13061496 Nov 19 10:46 libnvcuvid.so
% -rw-r--r-- 1 root root  13061496 Nov 19 10:46 libnvcuvid.so.1
% -rw-r--r-- 1 root root 128602944 Nov 19 10:46 libnvdxdlkernels.so
% -rw-r--r-- 1 root root    584296 Nov 19 10:46 libnvidia-encode.so
% -rw-r--r-- 1 root root    584296 Nov 19 10:46 libnvidia-encode.so.1
% -rw-r--r-- 1 root root    245192 Nov 19 10:46 libnvidia-ml.so.1
% -rw-r--r-- 1 root root    362960 Nov 19 10:46 libnvidia-opticalflow.so
% -rw-r--r-- 1 root root    362960 Nov 19 10:46 libnvidia-opticalflow.so.1
% -rw-r--r-- 1 root root     72664 Nov 19 10:46 libnvoptix.so.1
% -rw-r--r-- 1 root root  72175656 Nov 19 10:46 libnvwgf2umx.so
% -rw-r--r-- 1 root root    729600 Nov 19 10:46 nvidia-smi
% root@tishgpu1:/usr/lib/wsl/lib#root@tishgpu1:/usr/lib/wsl/lib# mv drv/ drivers
% root@tishgpu1:/usr/lib/wsl/lib# ls -l
% total 242276
% drwx---r-x 5 root root     12288 Nov 19 10:47 drivers
% -rw-r--r-- 1 root root  10182600 Nov 19 10:46 libcudadebugger.so.1
% -rw-r--r-- 1 root root    162608 Nov 19 10:46 libcuda.so
% -rw-r--r-- 1 root root    162608 Nov 19 10:46 libcuda.so.1
% -rw-r--r-- 1 root root    162608 Nov 19 10:46 libcuda.so.1.1
% -rw-r--r-- 1 root root   5913128 Nov 19 10:46 libd3d12core.so
% -rw-r--r-- 1 root root    789192 Nov 19 10:46 libd3d12.so
% -rw-r--r-- 1 root root    832144 Nov 19 10:46 libdxcore.so
% -rw-r--r-- 1 root root  13061496 Nov 19 10:46 libnvcuvid.so
% -rw-r--r-- 1 root root  13061496 Nov 19 10:46 libnvcuvid.so.1
% -rw-r--r-- 1 root root 128602944 Nov 19 10:46 libnvdxdlkernels.so
% -rw-r--r-- 1 root root    584296 Nov 19 10:46 libnvidia-encode.so
% -rw-r--r-- 1 root root    584296 Nov 19 10:46 libnvidia-encode.so.1
% -rw-r--r-- 1 root root    245192 Nov 19 10:46 libnvidia-ml.so.1
% -rw-r--r-- 1 root root    362960 Nov 19 10:46 libnvidia-opticalflow.so
% -rw-r--r-- 1 root root    362960 Nov 19 10:46 libnvidia-opticalflow.so.1
% -rw-r--r-- 1 root root     72664 Nov 19 10:46 libnvoptix.so.1
% -rw-r--r-- 1 root root  72175656 Nov 19 10:46 libnvwgf2umx.so
% -rw-r--r-- 1 root root    729600 Nov 19 10:46 nvidia-smi

% root@tishgpu1:/usr/lib/wsl/lib# chmod 555 /usr/lib/wsl/lib/*
% root@tishgpu1:/usr/lib/wsl/lib# chown -R root:root /usr/lib/wsl

% root@tishgpu1:/usr/lib/wsl/lib# vim /etc/ld.so.conf.d/ld.wsl.conf

% /usr/lib/wsl/lib

% root@tishgpu1:/usr/lib/wsl/lib# root@tishgpu1:/usr/lib/wsl/lib# ldconfig
% /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link

% root@tishgpu1:/usr/lib/wsl/lib# vim /etc/profile.d/wsl.sh
% root@tishgpu1:/usr/lib/wsl/lib# root@tishgpu1:/usr/lib/wsl/lib# cat /etc/profile.d/wsl.sh
% export PATH=$PATH:/usr/lib/wsl/lib
% chmod +x /etc/profile.d/wsl.sh

% \end{lstlisting}
% \fi


Для установки библиотеки для работы с CUDA необходимо выполнить следующие команды:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# Загружаем публичный ключ репозитория NVIDIA для подписи пакетов.
# Важно для обеспечения безопасности и проверки пакетов.
aritytishgpu1:~$ sudo apt-key adv --fetch-keys https://developer.download.
nvidia.com
/compute/cuda/repos/ubuntu2204/x86_64/3bf863cc.pub

# Внимание: apt-key устарел. Рекомендуется использовать новый метод управления ключами через директорию trusted.gpg.d.
# Ключ успешно импортирован:
# "cudatools <cudatools@nvidia.com>"

# Добавляем репозиторий CUDA в список источников пакетов.
# Это позволяет получать доступ к последним версиям инструментов CUDA.
aritytishgpu1:~$ sudo add-apt-repository "deb http://developer.download.nvidia.com/compute/
cuda/repos/ubuntu2204/x86_64/ /"

# Указанный репозиторий добавлен в файл /etc/apt/sources.list.d.
# Обновляем список пакетов для загрузки метаданных из нового репозитория.
# Получаем пакеты из репозитория NVIDIA и стандартных репозиториев Ubuntu.
# Замечание: ключ репозитория сохранён в устаревшем формате, как указано в предупреждении.

# Устанавливаем пакет CUDA Toolkit версии 12.
# Этот пакет включает библиотеки, компиляторы и утилиты для разработки с использованием GPU.
aritytishgpu1:~$ sudo apt install cuda-toolkit-12

# Создаём скрипт окружения для автоматической настройки переменных PATH, CUDA_HOME и LD_LIBRARY_PATH.
aritytishgpu1:/usr/local/cuda/bin$ sudo touch /etc/profile.d/cuda.sh

# Редактируем файл cuda.sh для добавления переменных окружения.
# Эти переменные позволяют системе находить бинарные файлы и библиотеки CUDA.
aritytishgpu1:/usr/local/cuda$ sudo vim /etc/profile.d/cuda.sh

# Проверяем содержимое файла, чтобы убедиться в правильной настройке переменных.
aritytishgpu1:/usr/local/cuda$ cat /etc/profile.d/cuda.sh
export PATH=$PATH:/usr/local/cuda/bin
export CUDA_HOME=$CUDA_HOME:/usr/local/cuda
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64

# Делаем файл cuda.sh исполняемым, чтобы переменные окружения применялись при входе в систему.
aritytishgpu1:~$ sudo chmod +x /etc/profile.d/cuda.sh
\end{lstlisting}

\subsection{Конфигурация NFS}
NFS (Network File System) — это сетевой протокол, разработанный для предоставления общего доступа к файловым системам через сеть. С помощью NFS пользователи или приложения могут работать с файлами, расположенными на удалённом сервере, так, как будто они находятся на локальной машине. \cite{rfc1094}

Для корректной работы исполняемых и других файлов на виртуальных машинах, вместо постоянного копирования можно воспользоваться протоколом сетевого доступа к файловой системе одной из машин. Для этого можно настроить NFS на одном из узлов (например tishgpu1) и далее выполнить монтирование в каталоги других виртуальных машин.

\subsubsection*{Настройка etc/hosts}
Для удобной работы в среде виртуальных машин можно задать доменные имена в файле \smalltexttt{etc/hosts}. Для этого его необходимо отредактировать на каждой виртуальной машине.

Формат создания доменного имени:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# IP-адрес, Основное имя хоста, Полное доменное имя 
10.200.166.126 tishcpu2 tishcpu2.tish
\end{lstlisting}
Пример файла \smalltexttt{etc/hosts}:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
aritytishcpu1:~$ cat /etc/hosts
127.0.0.1 localhost
10.200.166.125 tishcpu1 tishcpu1.tish
10.200.166.126 tishcpu2 tishcpu2.tish
10.200.166.127 tishgpu1 tishgpu1.tish
10.200.166.128 tishgpu2 tishgpu2.tish

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
\end{lstlisting}

\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# Содержимое файла /etc/exports на tishcpu1
# Экспортируем директорию /home/arity для общего доступа через NFS
# *: доступ разрешён для всех хостов
# rw: разрешение на чтение и запись
# nohide: дочерние файловые системы видны
# no_subtree_check: отключает проверку вложенных поддеревьев для повышения производительности
aritytishcpu1:~$ cat /etc/exports
/home/arity *(rw,nohide,no_subtree_check)

# Применяем изменения в настройках NFS (перезапускаем экспорт)
# -a: экспорт всех записей
# -r: перезапускает экспорт
# -v: подробный вывод
aritytishcpu1:~$ sudo exportfs -arv
exporting *:/home/arity

# Устанавливаем клиент NFS на tishcpu2
sudo apt install nfs-common

# Удаляем старую папку (если существует) для монтирования
aritytishcpu2:/mnt$ sudo rmdir share

# Проверяем содержимое директории /mnt
aritytishcpu2:/mnt$ ls

# Создаём новую папку share для монтирования NFS
aritytishcpu2:/mnt$ sudo mkdir share

# Монтируем экспортированную директорию /home/arity с tishgpu1 в локальную папку /mnt/share
# -t nfs: указывает тип файловой системы (NFS)
aritytishcpu2:/mnt$ sudo mount -t nfs tishgpu1:/home/arity /mnt/share

# Проверяем содержимое смонтированной директории
aritytishgpu2:/mnt$ ls share/
hello.txt  install.sh

# Читаем файл hello.txt из смонтированной директории
aritytishgpu2:/mnt$ cat share/hello.txt
happy hacking
aritytishgpu2:/mnt$
\end{lstlisting}

\subsection{Конфигурация slurm}
SLURM (Simple Linux Utility for Resource Management) — это открытая система управления задачами и ресурсами в кластерах. Она используется для распределения вычислительных задач между узлами и управления их выполнением.

С помощью \smalltexttt{conf.html} файла конфигурации необходимо сформировать конфигурацию в \smalltexttt{/etc/slurm/slurm.conf}.

\begin{itemize}
    \item \textbf{ClusterName} - Имя кластера SLURM (например, "tish"), используется для идентификации кластера.
    \item \textbf{SlurmctldHost} - Имя хоста, на котором работает демон управления SLURM (slurmctld), в данном случае это \smalltexttt{tishcpu1}.
    \item \textbf{MpiDefault} - Указывает стандартную реализацию MPI (по умолчанию \smalltexttt{none}, т.е. MPI не используется).
    \item \textbf{ProctrackType} - Метод отслеживания процессов; \smalltexttt{proctrack/cgroup} означает использование cgroup для изоляции процессов.
    \item \textbf{ReturnToService} - Указывает, должен ли узел автоматически возвращаться в работу после восстановления (\smalltexttt{1} - включено).
    \item \textbf{SlurmctldPidFile} - Путь к файлу PID для демона slurmctld.
    \item \textbf{SlurmdPidFile} - Путь к файлу PID для демона slurmd.
    \item \textbf{SlurmdSpoolDir} - Директория, где slurmd хранит временные файлы и информацию о заданиях.
    \item \textbf{SlurmUser} - Имя пользователя, под которым запускаются процессы SLURM (обычно \smalltexttt{slurm}).
    \item \textbf{StateSaveLocation} - Директория для сохранения состояния кластера, необходима для восстановления после перезапуска.
    \item \textbf{SwitchType} - Тип сетевого коммутатора; \smalltexttt{switch/none} указывает, что коммутатор не используется.
    \item \textbf{TaskPlugin} - Плагин для управления задачами; \smalltexttt{task/affinity} позволяет задавать привязку задач к CPU.
    \item \textbf{SchedulerType} - Тип планировщика задач; \smalltexttt{sched/backfill} разрешает задачи меньшего размера выполняться параллельно с крупными.
    \item \textbf{SelectType} - Механизм выбора ресурсов; 
    \item \textbf{SelectTypeParameters} - Параметры выбора ресурсов; \smalltexttt{CR\_Core} указывает распределение по ядрам.
    \item \textbf{JobAcctGatherType} - Метод сбора данных о выполнении задач;
    \item \textbf{SlurmctldLogFile} - Путь к файлу журнала для демона slurmctld.
    \item \textbf{SlurmdLogFile} - Путь к файлу журнала для демона slurmd.
    \item \textbf{NodeName} - Описание вычислительных узлов; описывает 4 узла с 2 CPU на каждом.
    \item \textbf{PartitionName} - Имя раздела (partition), включающего все узлы (\smalltexttt{Nodes=ALL}); используется для распределения задач.
    \item \textbf{Default} - Указывает, что данный раздел является разделом по умолчанию (\smalltexttt{YES}).
    \item \textbf{MaxTime} - Максимальное время выполнения задачи; \smalltexttt{INFINITE} означает, что ограничений нет.
    \item \textbf{State} - Статус узлов или раздела (\smalltexttt{UP} - узлы в рабочем состоянии).
\end{itemize}

Пример установки конфигурации:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
aritytishcpu1:~$ sudo apt install slurm-wlm

aritytishcpu1:~$ nano /etc/slurm/slurm.conf
#Указываем:

# slurm.conf file generated by configurator easy.html.
# Put this file on all nodes of your cluster.
# See the slurm.conf man page for more information.
#
ClusterName=tish
SlurmctldHost=tishcpu1
#
#MailProg=/bin/mail
#MpiDefault=
#MpiParams=ports=#-#
ProctrackType=proctrack/cgroup
ReturnToService=2
SlurmctldPidFile=/var/run/slurmctld.pid
#SlurmctldPort=6817
SlurmdPidFile=/var/run/slurmd.pid
#SlurmdPort=6818
SlurmdSpoolDir=/var/spool/slurmd
SlurmUser=slurm
#SlurmdUser=root
StateSaveLocation=/var/spool/slurmctld
#SwitchType=
TaskPlugin=task/affinity,task/cgroup
#
#
# TIMERS
#KillWait=30
#MinJobAge=300
#SlurmctldTimeout=120
#SlurmdTimeout=300
#
#
# SCHEDULING
SchedulerType=sched/backfill
SelectType=select/cons_tres
#
#
# LOGGING AND ACCOUNTING
#AccountingStorageType=
#JobAcctGatherFrequency=30
#JobAcctGatherType=
#SlurmctldDebug=info
SlurmctldLogFile=/var/log/slurmctld.log
#SlurmdDebug=info
SlurmdLogFile=/var/log/slurmd.log
#
#
# COMPUTE NODES
NodeName=tishcpu[1-2],tishgpu[1-2] CPUs=2 State=UNKNOWN
PartitionName=tishpartition Nodes=ALL Default=YES MaxTime=INFINITE State=UP

#Копируем этот конфиг в машины tishcpu2, tishgpu1, tishgpu2
\end{lstlisting}
\subsection{Конфигурация munge}
MUNGE — это инструмент для аутентификации, который используется для обеспечения безопасности в кластерах.

Ниже приведены шаги конфигурации ключей munge для конфигурации машин.
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# Копируем файл ключа MUNGE с узла tishgpu1 на все машины кластера.
# Этот файл необходим для аутентификации в кластере.

root@tishcpu2:/tmp# chown munge:munge ./munge.key
# Изменяем владельца и группу файла ключа на пользователя и группу MUNGE.
# Это необходимо для корректной работы службы MUNGE.

root@tishcpu2:/tmp# ls -l
# Проверяем содержимое текущей директории, чтобы убедиться, что файл ключа имеет нужные права:
# -rw-------: доступ только для владельца.

root@tishcpu2:/tmp# mv /etc/munge/munge.key /etc/munge/munge_old.key
# Переименовываем существующий ключ в `munge_old.key` для резервного копирования.

root@tishcpu2:/tmp# mv munge.key /etc/munge/munge.key
# Перемещаем новый файл ключа в директорию /etc/munge и задаём ему правильное имя.

root@tishcpu2:/etc/munge# ls
# Проверяем содержимое директории /etc/munge:
# Убедились, что есть два файла: новый ключ (`munge.key`) и резервный ключ (`munge_old.key`).

# Переходим на другой узел (tishgpu1), повторяем процесс.
aritytishgpu1:/tmp$ su
Password:
root@tishgpu1:/tmp# mv /etc/munge/munge.key /etc/munge/munge_old.key
# Создаём резервную копию существующего ключа.

root@tishgpu1:/tmp# chown munge:munge munge.key
# Изменяем владельца нового ключа на пользователя MUNGE.

root@tishgpu1:/tmp# mv /tmp/munge.key /etc/munge/munge.key
# Перемещаем новый ключ в директорию /etc/munge.

# На узле tishgpu2 повторяем процесс.
root@tishgpu2:/tmp# chown munge:munge munge.key
# Меняем владельца файла ключа.

root@tishgpu2:/tmp# mv /etc/munge/munge.key /etc/munge/munge_old.key
# Резервируем старый ключ.

root@tishgpu2:/tmp# mv munge.key /etc/munge/munge.key
# Перемещаем новый ключ на место старого.

root@tishgpu2:/tmp# ls -l /etc/munge
# Проверяем права и владельца ключей в директории /etc/munge:
# Убедились, что оба ключа принадлежат пользователю `munge` и имеют доступ только для владельца.

# Вносим изменения в конфигурацию SLURM:
# Меняем механизм отслеживания процессов с `cgroup` на `linuxproc` в файле конфигурации /etc/slurm/slurm.conf.

# Перезапускаем службы, чтобы применить изменения:
sudo systemctl restart munge
# Перезапускаем службу MUNGE для применения нового ключа.

sudo systemctl restart slurmd
# Перезапускаем демон SLURM для рабочих узлов.

sudo systemctl restart slurmctld
# Перезапускаем демон SLURM для управляющего узла.

sudo systemctl status munge
sudo systemctl status slurmd
sudo systemctl status slurmctld
# Проверяем статус всех служб, чтобы убедиться в их корректной работе.

\end{lstlisting}

\subsection{Конфигурация OpenMPI}

OpenMPI (Open Message Passing Interface) — это высокопроизводительная, гибкая и открытая реализация стандарта MPI (Message Passing Interface). MPI — это стандарт для взаимодействия между процессами в распределённых и параллельных вычислительных системах, таких как кластеры и суперкомпьютеры.

Для обмена сообщениями узлы должны обменяться публичными ключами и каждый имел прямой доступ к друг другу через \smalltexttt{ssh}.

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

\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
# Создаём новую пару SSH-ключей:
ssh-keygen 

# Копируем публичный SSH-ключ на удалённый узел:
# - ssh-copy-id: утилита для добавления публичного ключа на удалённый сервер.
# - aritytishcpu1: имя пользователя и адрес узла, куда копируется ключ.
# После выполнения этой команды ключ будет добавлен в файл authorized_keys на удалённом сервере,
# что позволит подключаться по SSH без ввода пароля.
ssh-copy-id aritytishcpu1
\end{lstlisting}

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

\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
sudo apt install openmpi-bin # установить на все узлы
sudo apt install openmpi-dev # установить на 1 узел где будет разработка приложения 
\end{lstlisting}

\newpage
\subsection{Постановка задачи и прототип решения}

\subsubsection*{Описание задачи}
Необходимо разработать параллельное приложение, задействующее вычислительные ресурсы двух CPU-узлов и двух CUDA-узлов, используя механизм OpenMPI, выполняющее анализ временных рядов исторических данных о стоимости Bitcoin.

Задача разбита на два этапа:
\begin{enumerate}
    \item \textbf{Этап 1. Агрегация данных}: для временного ряда исторических данных о стоимости Bitcoin (исходные данные содержат информацию по каждым 10 секундам) необходимо выполнить группировку по дням и для каждого дня вычислить среднюю цену как математическое ожидание значений Low и High, а также минимальные и максимальные значения Open и Close.
    \item \textbf{Этап 2. Поиск интервалов изменения цены}: на основе дневных агрегированных данных необходимо выявить интервалы дат (начиная с начальной даты в наборе данных), в которых средняя дневная цена изменилась не менее чем на 10\% относительно начала интервала. Для каждого интервала необходимо вывести начальную и конечную даты, а также минимальные и максимальные значения Open и Close за все дни внутри интервала.
\end{enumerate}

\subsubsection*{Описание входных данных}
В задаче используется файл в формате CSV с историческими данными о стоимости Bitcoin\cite{kaggle}. В качестве разделителя используется запятая. Во входном файле заданы следующие поля:
\begin{enumerate}
    \item \textbf{Timestamp} - временная метка Unix в секундах.
    \item \textbf{Open} - цена открытия за период.
    \item \textbf{High} - максимальная цена за период.
    \item \textbf{Low} - минимальная цена за период.
    \item \textbf{Close} - цена закрытия за период.
    \item \textbf{Volume} - объём торгов (может быть пустым).
\end{enumerate}

\subsubsection*{Прототип решения на Python}
Для отладки алгоритма был создан прототип на языке Python. Код прототипа представлен ниже:

\begin{lstlisting}[language=Python, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
import pandas as pd

# Загрузка данных
df = pd.read_csv("data.csv")
df['Timestamp'] = pd.to_datetime(df['Timestamp'], unit='s', utc=True)

# Вычисление средней цены
df['Avg'] = (df['Low'] + df['High']) / 2

# Группировка по дням и агрегация
df_days = (
    df.groupby(df["Timestamp"].dt.date)
    .agg(
        Avg=("Avg", "mean"),
        OpenMin=("Open", "min"),
        OpenMax=("Open", "max"),
        CloseMin=("Close", "min"),
        CloseMax=("Close", "max"),
    )
    .reset_index()
)

# Поиск интервалов изменения цены на 10%
intervals = []
start_idx = 0
price_base = df_days.loc[start_idx, "Avg"]

for i in range(1, len(df_days)):
    price_now = df_days.loc[i, "Avg"]
    change = abs(price_now - price_base) / price_base

    if change >= 0.10:
        interval = df_days.loc[start_idx:i]
        
        intervals.append({
            "start_date": df_days.loc[start_idx, "Timestamp"],
            "end_date": df_days.loc[i, "Timestamp"],
            "min_open": interval["OpenMin"].min(),
            "max_open": interval["OpenMax"].max(),
            "min_close": interval["CloseMin"].min(),
            "max_close": interval["CloseMax"].max(),
            "start_avg": price_base,
            "end_avg": price_now,
            "change": change,
        })

        start_idx = i + 1
        if start_idx >= len(df_days):
            break
        price_base = df_days.loc[start_idx, "Avg"]

df_intervals = pd.DataFrame(intervals)
\end{lstlisting}

\subsubsection*{Увеличение объёма данных}
Исходные данные содержат информацию по каждой минуте и имеют размер около 360 МБ. При тестировании параллельной реализации обработка таких данных занимала слишком мало времени, что не позволяло достоверно оценить эффективность параллельных вычислений и преимущества использования GPU.

Для решения этой проблемы был разработан скрипт \smalltexttt{upsample.py}, выполняющий линейную интерполяцию данных. Алгоритм работы скрипта следующий:

\begin{enumerate}
    \item Для каждой пары соседних записей $(t_1, o_1, h_1, l_1, c_1, v_1)$ и $(t_2, o_2, h_2, l_2, c_2, v_2)$ вычисляется временной интервал $\Delta t = t_2 - t_1$.
    \item Интервал делится на $n = \Delta t / \text{step}$ равных частей, где $\text{step}$ - новый временной шаг (10 секунд).
    \item Для каждой промежуточной точки $i \in [0, n)$ вычисляются интерполированные значения с помощью линейной интерполяции:
    \begin{align*}
        \alpha &= i / n \\
        t_i &= t_1 + i \cdot \text{step} \\
        o_i &= o_1 + (o_2 - o_1) \cdot \alpha \\
        h_i &= h_1 + (h_2 - h_1) \cdot \alpha \\
        l_i &= l_1 + (l_2 - l_1) \cdot \alpha \\
        c_i &= c_1 + (c_2 - c_1) \cdot \alpha \\
        v_i &= v_1 + (v_2 - v_1) \cdot \alpha
    \end{align*}
\end{enumerate}

В результате применения интерполяции данные были преобразованы из формата "каждая минута" в формат "каждые 10 секунд", что увеличило объём данных в 6 раз - с примерно 360 МБ до 2.3 ГБ. Такой объём данных позволяет наглядно продемонстрировать эффективность параллельных вычислений и преимущества использования GPU-ускорения.

\newpage
\subsection{Параллельная реализация на CPU}

\subsubsection*{Проблема последовательной обработки}
При профилировании первоначальной реализации было выявлено, что операция чтения и парсинга CSV-файла размером 2.3 ГБ занимает значительную часть времени выполнения программы. Последовательное чтение такого объёма данных на одном узле приводило к неэффективному использованию вычислительных ресурсов кластера, так как остальные узлы простаивали в ожидании данных.

\subsubsection*{Параллельное чтение с перекрытием}
Для решения этой проблемы было реализовано параллельное чтение файла, при котором каждый MPI-ранк читает только свою часть файла. Алгоритм работает следующим образом:

\begin{enumerate}
    \item \textbf{Вычисление диапазонов байт}: размер файла делится на части пропорционально долям, указанным в переменной окружения \smalltexttt{DATA\_READ\_SHARES}. Для каждого ранка вычисляется диапазон байт $[\text{start}, \text{end})$, который он должен прочитать.
    
    \item \textbf{Добавление перекрытия}: к каждому диапазону добавляется перекрытие размером \smalltexttt{READ\_OVERLAP\_BYTES} (по умолчанию 128 КБ). Это необходимо для корректной обработки строк CSV, которые могут быть разделены на границах диапазонов:
    \begin{align*}
        \text{start}_\text{adj} &= \max(0, \text{start} - \text{overlap}) \\
        \text{end}_\text{adj} &= \min(\text{file\_size}, \text{end} + \text{overlap})
    \end{align*}
    
    \item \textbf{Обработка границ строк}:
    \begin{itemize}
        \item Ранк 0 пропускает заголовок CSV (первую строку) и начинает парсинг со второй строки.
        \item Ранки $1 \ldots n-1$ пропускают неполную строку в начале своего диапазона, начиная парсинг с первого символа новой строки после \texttt{\textbackslash n}.
        \item Ранк $n-1$ (последний) читает до конца файла, остальные ранки заканчивают чтение на последнем символе \texttt{\textbackslash n} перед концом диапазона.
    \end{itemize}
\end{enumerate}

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

\subsubsection*{Агрегация данных по периодам}
После параллельного чтения каждый ранк имеет свой набор записей \smalltexttt{Record}. Агрегация выполняется локально на каждом ранке:

\begin{enumerate}
    \item Записи последовательно обрабатываются, для каждой записи вычисляется индекс периода:
    $$\text{period} = \lfloor \text{timestamp} / \text{AGGREGATION\_INTERVAL} \rfloor$$
    
    \item Для каждого периода накапливаются следующие статистики:
    \begin{itemize}
        \item Сумма средних цен: $\sum_{i} (Low_i + High_i) / 2$
        \item Минимальное и максимальное значение Open: $\min(Open_i)$, $\max(Open_i)$
        \item Минимальное и максимальное значение Close: $\min(Close_i)$, $\max(Close_i)$
        \item Количество записей в периоде: $count$
    \end{itemize}
    
    \item При смене периода статистики сохраняются в структуру \smalltexttt{PeriodStats}, и начинается накопление для следующего периода.
\end{enumerate}

Агрегация может выполняться на CPU (последовательная обработка) или на GPU (параллельная обработка с использованием CUDA). При недоступности GPU автоматически выполняется fallback на CPU-версию.

\subsubsection*{Удаление граничных периодов}
Из-за параллельного чтения с перекрытием первый и последний периоды каждого ранка могут содержать неполные данные. Например, если период охватывает временной интервал $[t_1, t_2)$, а ранк прочитал записи только начиная с $t_1 + \delta$, то статистики для этого периода будут искажены.

Для устранения этой проблемы применяется функция \smalltexttt{trim\_edge\_periods}:
\begin{itemize}
    \item Ранк 0 удаляет только последний период (первый период гарантированно полный, так как чтение начинается с начала файла).
    \item Ранки $1 \ldots n-2$ удаляют первый и последний периоды.
    \item Ранк $n-1$ удаляет только первый период (последний период гарантированно полный, так как чтение идёт до конца файла).
\end{itemize}

\subsubsection*{Параллельный поиск интервалов изменения цены}
После агрегации каждый ранк имеет список периодов \smalltexttt{PeriodStats}, упорядоченных по времени. Для параллельного поиска интервалов используется следующий алгоритм:

\begin{enumerate}
    \item \textbf{Приём данных от предыдущего ранка}: ранк $i > 0$ ожидает от ранка $i-1$ информацию о незавершённом интервале. Если предыдущий ранк передал начало интервала, текущий ранк продолжает его обработку.
    
    \item \textbf{Локальная обработка периодов}: ранк последовательно обходит свои периоды, проверяя условие изменения цены:
    $$\text{change} = \frac{|\text{avg}_\text{current} - \text{avg}_\text{start}|}{\text{avg}_\text{start}} \geq 0.10$$
    
    Если условие выполнено, интервал завершается и сохраняется в результаты. Начинается новый интервал.
    
    \item \textbf{Передача данных следующему ранку}: если у ранка остался незавершённый интервал (не достигнуто изменение на 10\%), информация о начале этого интервала передаётся ранку $i+1$ через MPI.
\end{enumerate}

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

\subsubsection*{Сбор результатов}
После завершения локальной обработки ранк 0 собирает найденные интервалы от всех остальных ранков через MPI, сортирует их по времени начала и записывает в выходной файл \smalltexttt{result.csv}.

\newpage
\subsection{GPU-ускорение агрегации данных}

\subsubsection*{Общий алгоритм GPU-агрегации}
Агрегация данных на GPU реализована в модуле \smalltexttt{gpu\_plugin.cu} и использует библиотеку CUB (CUDA Unbound) для эффективной параллельной обработки. Общий алгоритм состоит из следующих шагов:

\begin{enumerate}
    \item \textbf{Копирование данных на GPU}: массивы timestamp, open, high, low, close копируются из оперативной памяти CPU в память GPU.
    
    \item \textbf{Вычисление индексов периодов}: для каждой записи параллельно вычисляется индекс периода:
    $$\text{period\_id}_i = \lfloor \text{timestamp}_i / \text{AGGREGATION\_INTERVAL} \rfloor$$
    Используется простое ядро с одномерной сеткой блоков.
    
    \item \textbf{Run-Length Encoding (RLE)}: применяется операция RLE из библиотеки CUB для нахождения уникальных последовательных периодов и подсчёта количества записей в каждом периоде. На выходе получаем:
    \begin{itemize}
        \item Массив уникальных периодов: $[\text{period}_0, \text{period}_1, \ldots, \text{period}_{n-1}]$
        \item Массив длин последовательностей: $[\text{count}_0, \text{count}_1, \ldots, \text{count}_{n-1}]$
    \end{itemize}
    
    \item \textbf{Exclusive Scan}: применяется префиксная сумма (exclusive scan) к массиву длин для вычисления смещений начала каждого периода в исходном массиве данных:
    $$\text{offset}_i = \sum_{j=0}^{i-1} \text{count}_j$$
    
    \item \textbf{Агрегация по периодам}: для каждого периода параллельно вычисляются статистики (среднее значение, минимумы и максимумы). Используется одно из двух ядер в зависимости от настроек (см. следующий раздел).
    
    \item \textbf{Копирование результатов обратно на CPU}: агрегированные статистики копируются из памяти GPU обратно в оперативную память CPU.
\end{enumerate}

\subsubsection*{Два варианта ядер агрегации}
Для агрегации по периодам реализовано два варианта CUDA-ядер, оптимизированных для разных сценариев использования:

\textbf{1. Блочное ядро (Block Kernel):}

Используется когда \smalltexttt{USE\_BLOCK\_KERNEL=1}. Оптимизировано для случая, когда в каждом периоде много записей (большой \smalltexttt{AGGREGATION\_INTERVAL}).

Алгоритм работы:
\begin{itemize}
    \item Один блок потоков обрабатывает один период.
    \item Потоки внутри блока параллельно обрабатывают записи периода, каждый поток накапливает локальные статистики.
    \item Используется shared memory для промежуточного хранения результатов.
    \item Атомарные операции (\texttt{atomicAdd}, \texttt{atomicMin}, \texttt{atomicMax}) используются для объединения локальных результатов потоков.
    \item Первый поток блока записывает финальный результат в глобальную память.
\end{itemize}

Преимущества: эффективное использование параллелизма внутри периода, минимизация обращений к глобальной памяти за счёт shared memory.

\textbf{2. Простое ядро (Simple Kernel):}

Используется когда \smalltexttt{USE\_BLOCK\_KERNEL=0}. Оптимизировано для случая, когда периодов много, но в каждом периоде мало записей (малый \smalltexttt{AGGREGATION\_INTERVAL}).

Алгоритм работы:
\begin{itemize}
    \item Один поток обрабатывает один период полностью.
    \item Поток последовательно обходит все записи своего периода, накапливая статистики.
    \item Не используется shared memory и атомарные операции.
    \item Результат сразу записывается в глобальную память.
\end{itemize}

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

\textbf{Выбор ядра:}

Выбор между ядрами осуществляется через переменную окружения \smalltexttt{USE\_BLOCK\_KERNEL}:
\begin{itemize}
    \item Блочное ядро предпочтительно при агрегации по дням/часам (86400 или 3600 секунд) - много записей в каждом периоде.
    \item Простое ядро предпочтительно при агрегации по минутам/секундам (60 или 10 секунд) - мало записей в каждом периоде, но много периодов.
\end{itemize}

\newpage
\subsection{Конфигурация через переменные окружения}

Все настройки параллельного приложения вынесены в переменные окружения, которые задаются в SLURM-скрипте \smalltexttt{run.slurm}. Это обеспечивает гибкость настройки без необходимости перекомпиляции программы.

\subsubsection*{Описание переменных окружения}

\begin{itemize}
    \item \textbf{DATA\_PATH} - полный путь к CSV-файлу с входными данными. Файл должен быть доступен на всех узлах кластера (рекомендуется использовать общую директорию, например, через NFS).
    
    Пример: \smalltexttt{/mnt/shared/supercomputers/data/data\_10s.csv}
    
    \item \textbf{DATA\_READ\_SHARES} - доли данных для каждого ранка при параллельном чтении файла, разделённые запятыми. Позволяет неравномерно распределить нагрузку по чтению, если узлы имеют разную производительность.
    
    Пример: \smalltexttt{10,11,13,14} означает, что файл будет разделён на части пропорционально $10:11:13:14$. Если количество значений не совпадает с количеством ранков, используется равномерное распределение.
    
    \item \textbf{READ\_OVERLAP\_BYTES} - размер перекрытия в байтах при параллельном чтении файла. Необходим для корректной обработки строк CSV на границах диапазонов. Значение должно быть достаточным для размещения хотя бы одной полной строки CSV.
    
    Значение по умолчанию: \smalltexttt{131072} (128 КБ)
    
    \item \textbf{AGGREGATION\_INTERVAL} - интервал агрегации в секундах. Определяет размер временного периода, по которому группируются данные.
    
    Типичные значения:
    \begin{itemize}
        \item \smalltexttt{60} - агрегация по минутам
        \item \smalltexttt{600} - агрегация по 10 минутам
        \item \smalltexttt{3600} - агрегация по часам
        \item \smalltexttt{86400} - агрегация по дням
    \end{itemize}
    
    \item \textbf{USE\_CUDA} - флаг использования GPU для агрегации данных. Если установлен в \smalltexttt{1}, программа попытается использовать GPU. Если GPU недоступен или флаг установлен в \smalltexttt{0}, используется CPU-версия агрегации.
    
    Значения: \smalltexttt{0} (отключено) или \smalltexttt{1} (включено)
    
    \item \textbf{USE\_BLOCK\_KERNEL} - выбор варианта CUDA-ядра для GPU-агрегации (действует только при \smalltexttt{USE\_CUDA=1}). Определяет, какое ядро будет использоваться для параллельной обработки на GPU.
    
    Значения:
    \begin{itemize}
        \item \smalltexttt{0} - использовать простое ядро (один поток на период)
        \item \smalltexttt{1} - использовать блочное ядро (один блок на период)
    \end{itemize}
    
    Рекомендации по выбору:
    \begin{itemize}
        \item Для больших интервалов агрегации (дни, часы) - \smalltexttt{USE\_BLOCK\_KERNEL=1}
        \item Для малых интервалов агрегации (минуты, секунды) - \smalltexttt{USE\_BLOCK\_KERNEL=0}
    \end{itemize}
\end{itemize}

Пример конфигурации в \smalltexttt{run.slurm}:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
export DATA_PATH="/mnt/shared/supercomputers/data/data_10s.csv"
export DATA_READ_SHARES="10,11,13,14"
export READ_OVERLAP_BYTES=131072
export AGGREGATION_INTERVAL=60
export USE_CUDA=1
export USE_BLOCK_KERNEL=0
\end{lstlisting}

\newpage
\subsection{Структура проекта}
Исходный проект содержит в себе следующие зависимости:
\begin{itemize}
    \item CUDA-Toolkit 12.8;
    \item OpenMPI 3.
\end{itemize}

Можно выделить следующие основные сущности:
\begin{itemize}
    \item \smalltexttt{run.slurm} - SLURM-скрипт для запуска параллельного приложения на 4 узлах с настройкой переменных окружения (путь к данным, доли данных для каждого ранка, интервал агрегации, использование CUDA). Исходный текст файла представлен в \hyperref[1]{Приложение A};
    \item \smalltexttt{Makefile} - файл системы сборки, описывающий компиляцию C++ исходников с помощью mpic++ и компиляцию CUDA-плагина с помощью nvcc, а также правила запуска и очистки. Исходный текст файла представлен в \hyperref[2]{Приложение Б};
    \item \smalltexttt{src/main.cpp} - основная MPI-программа: координирует выполнение параллельного чтения CSV данных, агрегацию данных по временным периодам (на CPU или GPU), поиск интервалов изменения цены и запись результатов. Исходный текст файла представлен в \hyperref[3]{Приложение В};
    \item \smalltexttt{src/csv\_loader.cpp}, \smalltexttt{src/csv\_loader.hpp} - модуль параллельной загрузки CSV-файла: каждый MPI-ранк читает свою часть файла с перекрытием для обработки границ строк, парсит записи Bitcoin данных. Исходный текст файла представлен в \hyperref[4]{Приложение Г};
    \item \smalltexttt{src/aggregation.cpp}, \smalltexttt{src/aggregation.hpp} - модуль агрегации временных рядов на CPU: группирует записи по временным интервалам, вычисляет среднее значение (Low+High)/2, минимумы и максимумы Open/Close за каждый период. Исходный текст файла представлен в \hyperref[5]{Приложение Д};
    \item \smalltexttt{src/gpu\_loader.cpp}, \smalltexttt{src/gpu\_loader.hpp} - модуль динамической загрузки GPU-плагина: проверяет доступность GPU, загружает функции из \smalltexttt{libgpu\_compute.so}, преобразует данные из AoS в SoA для передачи на GPU. Исходный текст файла представлен в \hyperref[6]{Приложение Е};
    \item \smalltexttt{src/gpu\_plugin.cu} - CUDA-модуль агрегации данных на GPU: использует библиотеку CUB для RLE и scan операций, реализует два ядра агрегации (блочное для больших интервалов и простое для множества малых периодов). Исходный текст файла представлен в \hyperref[7]{Приложение Ж};
    \item \smalltexttt{src/intervals.cpp}, \smalltexttt{src/intervals.hpp} - модуль параллельного поиска интервалов изменения цены: каждый ранк обрабатывает свою часть периодов, передаёт незавершённые интервалы следующему ранку через MPI, собирает результаты на ранке 0. Исходный текст файла представлен в \hyperref[8]{Приложение З};
    \item \smalltexttt{src/utils.cpp}, \smalltexttt{src/utils.hpp} - вспомогательный модуль: чтение переменных окружения, вычисление диапазонов байт для параллельного чтения файла, удаление граничных периодов, получение размера файла. Исходный текст файла представлен в \hyperref[9]{Приложение И};
    \item \smalltexttt{src/period\_stats.hpp} - заголовочный файл с определением структуры \smalltexttt{PeriodStats}, хранящей агрегированные статистики за один временной период (среднее значение, минимумы и максимумы Open/Close). Исходный текст файла представлен в \hyperref[10]{Приложение К};
    \item \smalltexttt{src/record.hpp} - заголовочный файл с определением структуры \smalltexttt{Record} для хранения одной записи из CSV-файла Bitcoin (timestamp, open, high, low, close, volume). Исходный текст файла представлен в \hyperref[11]{Приложение Л};
    \item \smalltexttt{data/data\_10s.csv} - текстовый файл с входными данными о стоимости Bitcoin по каждым 10 секундам в формате CSV;
    \item \smalltexttt{result.csv} - выходной файл с найденными интервалами изменения цены не менее чем на 10\%.
\end{itemize}

Также в рамках проекта используется система автоматизированной сборки. Для сборки и запуска проекта необходимо выполнить следующие команды:
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
make
make run
\end{lstlisting}

\newpage
\begin{center}
\section*{ЗАКЛЮЧЕНИЕ}
\end{center}
\addcontentsline{toc}{section}{ЗАКЛЮЧЕНИЕ}
В ходе выполнения лабораторной работы были решены задачи, направленные на освоение параллельных вычислений с использованием разнородных типов вычислительных ресурсов. В результате работы удалось:

\begin{itemize}
    \item Создать виртуальные машины, обеспечивающие выполнение задач как на GPU-узлах, так и на CPU-узлах;
    \item Настроить сеть для обеспечения стабильной связи между хост-системой и виртуальными узлами;
    \item Реализовать параллельное приложение с использованием механизма OpenMPI, задействующее ресурсы разнородных узлов;
    \item Изучить технологии CUDA, OpenMPI, Slurm.
\end{itemize}

Были изучены технологии OpenMPI, CUDA Toolkit и библиотека CUB. В рамках работы было разработано параллельное приложение на языке C++, использующее разнородный вид вычислительных ресурсов для анализа временных рядов исторических данных о стоимости Bitcoin.

Разработанная программа выполняет параллельную агрегацию временных рядов и поиск интервалов значительного изменения цены. Ключевые особенности реализации:

\begin{itemize}
    \item Параллельное чтение CSV-файла размером 2.3 ГБ с использованием техники перекрытия диапазонов для корректной обработки границ строк.
    \item Гибридная агрегация данных, поддерживающая вычисления как на CPU (последовательная обработка), так и на GPU (параллельная обработка с использованием CUDA).
    \item Два варианта CUDA-ядер: блочное ядро для больших интервалов агрегации (дни, часы) и простое ядро для малых интервалов (минуты, секунды).
    \item Динамическая загрузка GPU-плагина через dlopen, позволяющая запускать приложение на узлах без GPU без перекомпиляции.
    \item Параллельный поиск интервалов изменения цены с передачей незавершённых интервалов между ранками через MPI.
    \item Гибкая конфигурация через переменные окружения, позволяющая настраивать параметры работы без изменения исходного кода.
\end{itemize}

Для наглядной демонстрации эффективности параллельных вычислений был разработан скрипт линейной интерполяции данных, увеличивший объём исходного набора данных с 360 МБ до 2.3 ГБ. Это позволило достоверно оценить преимущества параллельной обработки и GPU-ускорения.



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


\newpage
\addcontentsline{toc}{section}{Список литературы}

\vspace{-1.5cm}
\begin{thebibliography}{0}
    \bibitem{kaggle}
    Zielak - Bitcoin Historical Data // kaggle URL: https://www.kaggle.com/datasets/mczielinski/bitcoin-historical-data (дата обращения: 10.01.2026).
   \bibitem{hyperv}
   Hyper-V Documentation // Microsoft URL: https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/ (дата обращения: 10.01.2026).
   \bibitem{ubuntuDocs}
   Ubuntu OS Docs // Ubuntu URL: https://ubuntu.com/server/docs (дата обращения: 10.01.2026).
   \bibitem{newVmSwitch}
   New-VMSwitch: Документация PowerShell // Microsoft 365 URL: https://learn.microsoft.com/en-us/powershell/module/hyper-v/new-vmswitch?view=windowsserver2025-ps (дата обращения: 10.01.2026).
   \bibitem{newNetIpAddress}
   New-NetIPAddress: Документация PowerShell // Microsoft 365 URL: https://learn.microsoft.com/en-us/powershell/module/nettcpip/new-netipaddress?view=windowsserver2025-ps (дата обращения: 10.01.2026).
   \bibitem{newNetNat}
   New-NetNat: Документация PowerShell // Microsoft 365 URL: https://learn.microsoft.com/en-us/powershell/module/netnat/New-NetNat?view=windowsserver2016-ps (дата обращения: 10.01.2026).
   \bibitem{addNetNatStaticMapping}
   Add-NetNatStaticMapping: Документация PowerShell // Microsoft 365 URL: https://learn.microsoft.com/ru-ru/powershell/module/netnat/add-netnatstaticmapping?view=windowsserver2022-ps (дата обращения: 10.01.2026).
   \bibitem{getVmHostPartitionableGpu}
   Get-VMHostPartitionableGpu: Документация PowerShell // Microsoft 365 URL: https://learn.microsoft.com/en-us/powershell/module/hyper-v/get-vmhostpartitionablegpu?view=windowsserver2025-ps (дата обращения: 10.01.2026).
   \bibitem{setVm}
   Set-VM: Документация PowerShell // Microsoft 365 URL: https://learn.microsoft.com/en-us/powershell/module/hyper-v/set-vm?view=windowsserver2025-ps (дата обращения: 10.01.2026).
   \bibitem{rfc1094}
   RFC 1094: NFS: Network File System Protocol Specification // Sun Microsystems, Inc. URL: https://datatracker.ietf.org/doc/html/rfc1094 (дата обращения: 10.01.2026).
\end{thebibliography}
\newpage

\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ А}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ А}
\label{1}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#!/bin/bash
#SBATCH --job-name=btc
#SBATCH --nodes=4
#SBATCH --ntasks=4
#SBATCH --cpus-per-task=2
#SBATCH --output=out.txt

# Путь к файлу данных (должен существовать на всех узлах)
export DATA_PATH="/mnt/shared/supercomputers/data/data_10s.csv"

# Доли данных для каждого ранка (сумма определяет пропорции)
export DATA_READ_SHARES="10,11,13,14"

# Размер перекрытия в байтах для обработки границ строк
export READ_OVERLAP_BYTES=131072

# Интервал агрегации в секундах (60 = минуты, 600 = 10 минут, 86400 = дни)
export AGGREGATION_INTERVAL=60

# Использовать ли CUDA для агрегации (0 = нет, 1 = да)
export USE_CUDA=1

# Использовать ли блочное ядро (быстрее для больших интервалов, 0 = нет, 1 = да)
export USE_BLOCK_KERNEL=0

cd /mnt/shared/supercomputers/build
mpirun -np $SLURM_NTASKS ./bitcoin_app
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ Б}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ Б}
\label{2}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
CXX = mpic++
CXXFLAGS = -std=c++17 -O2 -Wall -Wextra -Wno-cast-function-type -fopenmp

NVCC = nvcc
NVCCFLAGS = -O3 -std=c++17 -arch=sm_86 -Xcompiler -fPIC

SRC_DIR = src
BUILD_DIR = build

SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))

TARGET = $(BUILD_DIR)/bitcoin_app

PLUGIN_SRC = $(SRC_DIR)/gpu_plugin.cu
PLUGIN = $(BUILD_DIR)/libgpu_compute.so

all: $(PLUGIN) $(TARGET)

$(BUILD_DIR):
	mkdir -p $(BUILD_DIR)

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
	$(CXX) $(CXXFLAGS) -c $< -o $@

$(TARGET): $(OBJS)
	$(CXX) $(CXXFLAGS) $^ -o $@ -ldl

$(PLUGIN): $(PLUGIN_SRC) | $(BUILD_DIR)
	$(NVCC) $(NVCCFLAGS) -shared $< -o $@

clean:
	rm -rf $(BUILD_DIR)

run: all
	sbatch run.slurm

.PHONY: all clean run
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ В}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ В}
\label{3}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#include <mpi.h>
#include <iostream>
#include <vector>
#include <iomanip>

#include "csv_loader.hpp"
#include "record.hpp"
#include "period_stats.hpp"
#include "aggregation.hpp"
#include "intervals.hpp"
#include "utils.hpp"
#include "gpu_loader.hpp"

int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);
    double total_start = MPI_Wtime();

    int rank, size;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);

    // Проверяем доступность GPU
    bool use_cuda = get_use_cuda();
    bool have_gpu = gpu_is_available();
    bool use_gpu = use_cuda && have_gpu;
    
    std::cout << "Rank " << rank 
              << ": USE_CUDA=" << use_cuda 
              << ", GPU available=" << have_gpu 
              << ", using " << (use_gpu ? "GPU" : "CPU")
              << std::endl;

    // Параллельное чтение данных
    double read_start = MPI_Wtime();
    std::vector<Record> records = load_csv_parallel(rank, size);
    double read_time = MPI_Wtime() - read_start;

    std::cout << "Rank " << rank 
              << ": read " << records.size() << " records"
              << " in " << std::fixed << std::setprecision(3) << read_time << " sec"
              << std::endl;

    // Агрегация по периодам
    double agg_start = MPI_Wtime();
    std::vector<PeriodStats> periods;
    
    if (use_gpu) {
        int64_t interval = get_aggregation_interval();
        if (!aggregate_periods_gpu(records, interval, periods)) {
            std::cerr << "Rank " << rank << ": GPU aggregation failed, falling back to CPU" << std::endl;
            periods = aggregate_periods(records);
        }
    } else {
        periods = aggregate_periods(records);
    }
    
    double agg_time = MPI_Wtime() - agg_start;

    std::cout << "Rank " << rank 
              << ": aggregated " << periods.size() << " periods"
              << " [" << (periods.empty() ? 0 : periods.front().period) 
              << ".." << (periods.empty() ? 0 : periods.back().period) << "]"
              << " in " << std::fixed << std::setprecision(3) << agg_time << " sec"
              << std::endl;

    // Удаляем крайние периоды (могут быть неполными из-за параллельного чтения)
    trim_edge_periods(periods, rank, size);

    std::cout << "Rank " << rank 
              << ": after trim " << periods.size() << " periods"
              << " [" << (periods.empty() ? 0 : periods.front().period) 
              << ".." << (periods.empty() ? 0 : periods.back().period) << "]"
              << std::endl;

    // Параллельное построение интервалов
    IntervalResult iv_result = find_intervals_parallel(periods, rank, size);

    std::cout << "Rank " << rank 
              << ": found " << iv_result.intervals.size() << " intervals"
              << ", compute " << std::fixed << std::setprecision(6) << iv_result.compute_time << " sec"
              << ", wait " << iv_result.wait_time << " sec"
              << std::endl;

    // Сбор интервалов на ранке 0
    double collect_wait = collect_intervals(iv_result.intervals, rank, size);

    if (rank == 0) {
        std::cout << "Rank 0: collected " << iv_result.intervals.size() << " total intervals"
                  << ", wait " << std::fixed << std::setprecision(3) << collect_wait << " sec"
                  << std::endl;
    }

    // Запись результатов в файл (только ранк 0)
    if (rank == 0) {
        double write_start = MPI_Wtime();
        write_intervals("result.csv", iv_result.intervals);
        double write_time = MPI_Wtime() - write_start;

        std::cout << "Rank 0: wrote result.csv"
                  << " in " << std::fixed << std::setprecision(3) << write_time << " sec"
                  << std::endl;
    }

    // Вывод общего времени выполнения
    MPI_Barrier(MPI_COMM_WORLD);
    double total_time = MPI_Wtime() - total_start;
    if (rank == 0) {
        std::cout << "Total execution time: "
                  << std::fixed << std::setprecision(3)
                  << total_time << " sec" << std::endl;
    }

    MPI_Finalize();
    return 0;
}
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ Г}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ Г}
\label{4}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#include "csv_loader.hpp"
#include <fstream>
#include <sstream>
#include <iostream>
#include <stdexcept>

bool parse_csv_line(const std::string& line, Record& record) {
    if (line.empty()) {
        return false;
    }
    
    std::stringstream ss(line);
    std::string item;
    
    try {
        // timestamp
        if (!std::getline(ss, item, ',') || item.empty()) return false;
        record.timestamp = std::stod(item);
        
        // open
        if (!std::getline(ss, item, ',') || item.empty()) return false;
        record.open = std::stod(item);
        
        // high
        if (!std::getline(ss, item, ',') || item.empty()) return false;
        record.high = std::stod(item);
        
        // low
        if (!std::getline(ss, item, ',') || item.empty()) return false;
        record.low = std::stod(item);
        
        // close
        if (!std::getline(ss, item, ',') || item.empty()) return false;
        record.close = std::stod(item);
        
        // volume
        if (!std::getline(ss, item, ',')) return false;
        // Volume может быть пустым или содержать данные
        if (item.empty()) {
            record.volume = 0.0;
        } else {
            record.volume = std::stod(item);
        }
        
        return true;
    } catch (const std::exception&) {
        return false;
    }
}

std::vector<Record> load_csv_parallel(int rank, int size) {
    std::vector<Record> data;
    
    // Читаем настройки из переменных окружения
    std::string data_path = get_data_path();
    std::vector<int> shares = get_data_read_shares();
    int64_t overlap_bytes = get_read_overlap_bytes();
    
    // Получаем размер файла
    int64_t file_size = get_file_size(data_path);
    
    // Вычисляем диапазон байт для этого ранка
    ByteRange range = calculate_byte_range(rank, size, file_size, shares, overlap_bytes);
    
    // Открываем файл и читаем нужный диапазон
    std::ifstream file(data_path, std::ios::binary);
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file: " + data_path);
    }
    
    // Переходим к началу диапазона
    file.seekg(range.start);
    
    // Читаем данные в буфер
    int64_t bytes_to_read = range.end - range.start;
    std::vector<char> buffer(bytes_to_read);
    file.read(buffer.data(), bytes_to_read);
    int64_t bytes_read = file.gcount();
    
    file.close();
    
    // Преобразуем в строку для удобства парсинга
    std::string content(buffer.data(), bytes_read);
    
    // Находим позицию начала первой полной строки
    size_t parse_start = 0;
    if (rank == 0) {
        // Первый ранк: пропускаем заголовок (первую строку)
        size_t header_end = content.find('\n');
        if (header_end != std::string::npos) {
            parse_start = header_end + 1;
        }
    } else {
        // Остальные ранки: начинаем с первого \n (пропускаем неполную строку)
        size_t first_newline = content.find('\n');
        if (first_newline != std::string::npos) {
            parse_start = first_newline + 1;
        }
    }
    
    // Находим позицию конца последней полной строки
    size_t parse_end = content.size();
    if (rank != size - 1) {
        // Не последний ранк: ищем последний \n
        size_t last_newline = content.rfind('\n');
        if (last_newline != std::string::npos && last_newline > parse_start) {
            parse_end = last_newline;
        }
    }
    
    // Парсим строки
    size_t pos = parse_start;
    while (pos < parse_end) {
        size_t line_end = content.find('\n', pos);
        if (line_end == std::string::npos || line_end > parse_end) {
            line_end = parse_end;
        }
        
        std::string line = content.substr(pos, line_end - pos);
        
        // Убираем \r если есть (Windows line endings)
        if (!line.empty() && line.back() == '\r') {
            line.pop_back();
        }
        
        Record record;
        if (parse_csv_line(line, record)) {
            data.push_back(record);
        }
        
        pos = line_end + 1;
    }
    
    return data;
}
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ Д}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ Д}
\label{5}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#include "aggregation.hpp"
#include "utils.hpp"

#include <algorithm>
#include <cstdint>
#include <limits>
#include <vector>

std::vector<PeriodStats> aggregate_periods(const std::vector<Record>& records) {
    const int64_t interval = get_aggregation_interval();

    std::vector<PeriodStats> result;
    if (records.empty()) return result;

    struct PeriodAccumulator {
        double avg_sum = 0.0;
        double open_min = std::numeric_limits<double>::max();
        double open_max = std::numeric_limits<double>::lowest();
        double close_min = std::numeric_limits<double>::max();
        double close_max = std::numeric_limits<double>::lowest();
        int64_t count = 0;

        void add(const Record& r) {
            const double avg = (r.low + r.high) / 2.0;
            avg_sum += avg;
            open_min = std::min(open_min, r.open);
            open_max = std::max(open_max, r.open);
            close_min = std::min(close_min, r.close);
            close_max = std::max(close_max, r.close);
            ++count;
        }
    };

    PeriodIndex current_period =
        static_cast<PeriodIndex>(records[0].timestamp) / interval;

    PeriodAccumulator acc;
    acc.add(records[0]);

    for (size_t i = 1; i < records.size(); ++i) {
        const Record& r = records[i];
        const PeriodIndex period =
            static_cast<PeriodIndex>(r.timestamp) / interval;

        if (period != current_period) {
            PeriodStats stats;
            stats.period = current_period;
            stats.avg = acc.avg_sum / static_cast<double>(acc.count);
            stats.open_min = acc.open_min;
            stats.open_max = acc.open_max;
            stats.close_min = acc.close_min;
            stats.close_max = acc.close_max;
            stats.count = acc.count;
            result.push_back(stats);

            current_period = period;
            acc = PeriodAccumulator{};
        }

        acc.add(r);
    }

    // последний период
    PeriodStats stats;
    stats.period = current_period;
    stats.avg = acc.avg_sum / static_cast<double>(acc.count);
    stats.open_min = acc.open_min;
    stats.open_max = acc.open_max;
    stats.close_min = acc.close_min;
    stats.close_max = acc.close_max;
    stats.count = acc.count;
    result.push_back(stats);

    return result;
}
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ Е}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ Е}
\label{6}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#include "gpu_loader.hpp"
#include <dlfcn.h>
#include <iostream>
#include <cstdint>

// Структура результата GPU (должна совпадать с gpu_plugin.cu)
struct GpuPeriodStats {
    int64_t period;
    double avg;
    double open_min;
    double open_max;
    double close_min;
    double close_max;
    int64_t count;
};

// Типы функций из GPU плагина
using gpu_is_available_fn = int (*)();

using gpu_aggregate_periods_fn = int (*)(
    const double* h_timestamps,
    const double* h_open,
    const double* h_high,
    const double* h_low,
    const double* h_close,
    int num_ticks,
    int64_t interval,
    GpuPeriodStats** h_out_stats,
    int* out_num_periods
);

using gpu_free_results_fn = void (*)(GpuPeriodStats*);

static void* get_gpu_lib_handle() {
    static void* h = dlopen("./libgpu_compute.so", RTLD_NOW | RTLD_LOCAL);
    return h;
}

bool gpu_is_available() {
    void* h = get_gpu_lib_handle();
    if (!h) return false;
    
    auto fn = reinterpret_cast<gpu_is_available_fn>(dlsym(h, "gpu_is_available"));
    if (!fn) return false;
    
    return fn() != 0;
}

bool aggregate_periods_gpu(
    const std::vector<Record>& records,
    int64_t aggregation_interval,
    std::vector<PeriodStats>& out_stats)
{
    if (records.empty()) {
        out_stats.clear();
        return true;
    }
    
    void* h = get_gpu_lib_handle();
    if (!h) {
        std::cerr << "GPU: Failed to load libgpu_compute.so" << std::endl;
        return false;
    }
    
    auto aggregate_fn = reinterpret_cast<gpu_aggregate_periods_fn>(
        dlsym(h, "gpu_aggregate_periods"));
    auto free_fn = reinterpret_cast<gpu_free_results_fn>(
        dlsym(h, "gpu_free_results"));
    
    if (!aggregate_fn || !free_fn) {
        std::cerr << "GPU: Failed to load functions from plugin" << std::endl;
        return false;
    }
    
    int num_ticks = static_cast<int>(records.size());
    
    // Конвертируем AoS в SoA
    std::vector<double> timestamps(num_ticks);
    std::vector<double> open(num_ticks);
    std::vector<double> high(num_ticks);
    std::vector<double> low(num_ticks);
    std::vector<double> close(num_ticks);
    
    for (int i = 0; i < num_ticks; i++) {
        timestamps[i] = records[i].timestamp;
        open[i] = records[i].open;
        high[i] = records[i].high;
        low[i] = records[i].low;
        close[i] = records[i].close;
    }
    
    // Вызываем GPU функцию
    GpuPeriodStats* gpu_stats = nullptr;
    int num_periods = 0;
    
    int result = aggregate_fn(
        timestamps.data(),
        open.data(),
        high.data(),
        low.data(),
        close.data(),
        num_ticks,
        aggregation_interval,
        &gpu_stats,
        &num_periods
    );
    
    if (result != 0) {
        std::cerr << "GPU: Aggregation failed with code " << result << std::endl;
        return false;
    }
    
    // Конвертируем результат в PeriodStats
    out_stats.clear();
    out_stats.reserve(num_periods);
    
    for (int i = 0; i < num_periods; i++) {
        PeriodStats ps;
        ps.period = gpu_stats[i].period;
        ps.avg = gpu_stats[i].avg;
        ps.open_min = gpu_stats[i].open_min;
        ps.open_max = gpu_stats[i].open_max;
        ps.close_min = gpu_stats[i].close_min;
        ps.close_max = gpu_stats[i].close_max;
        ps.count = gpu_stats[i].count;
        out_stats.push_back(ps);
    }
    
    // Освобождаем память
    free_fn(gpu_stats);
    
    return true;
}
\end{lstlisting}
\newpage


\begin{center}
\section*{ПРИЛОЖЕНИЕ Ж}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ Ж}
\label{7}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#include <cuda_runtime.h>
#include <cub/cub.cuh>
#include <cstdint>
#include <cfloat>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <string>
#include <sstream>
#include <iomanip>

// ============================================================================
// Структуры данных
// ============================================================================

// Результат агрегации одного периода
struct GpuPeriodStats {
    int64_t period;
    double avg;
    double open_min;
    double open_max;
    double close_min;
    double close_max;
    int64_t count;
};

// ============================================================================
// Вспомогательные функции
// ============================================================================

static double get_time_ms() {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0;
}

#define CUDA_CHECK(call) do { \
    cudaError_t err = call; \
    if (err != cudaSuccess) { \
        printf("CUDA error at %s:%d: %s\n", __FILE__, __LINE__, cudaGetErrorString(err)); \
        return -1; \
    } \
} while(0)

// ============================================================================
// Kernel: вычисление period_id для каждого тика
// ============================================================================

__global__ void compute_period_ids_kernel(
    const double* __restrict__ timestamps,
    int64_t* __restrict__ period_ids,
    int n,
    int64_t interval)
{
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        period_ids[idx] = static_cast<int64_t>(timestamps[idx]) / interval;
    }
}

// ============================================================================
// Kernel: агрегация одного периода (один блок на период)
// ============================================================================

__global__ void aggregate_periods_kernel(
    const double* __restrict__ open,
    const double* __restrict__ high,
    const double* __restrict__ low,
    const double* __restrict__ close,
    const int64_t* __restrict__ unique_periods,
    const int* __restrict__ offsets,
    const int* __restrict__ counts,
    int num_periods,
    GpuPeriodStats* __restrict__ out_stats)
{
    int period_idx = blockIdx.x;
    if (period_idx >= num_periods) return;
    
    int offset = offsets[period_idx];
    int count = counts[period_idx];
    
    // Используем shared memory для редукции внутри блока
    __shared__ double s_avg_sum;
    __shared__ double s_open_min;
    __shared__ double s_open_max;
    __shared__ double s_close_min;
    __shared__ double s_close_max;
    
    // Инициализация shared memory первым потоком
    if (threadIdx.x == 0) {
        s_avg_sum = 0.0;
        s_open_min = DBL_MAX;
        s_open_max = -DBL_MAX;
        s_close_min = DBL_MAX;
        s_close_max = -DBL_MAX;
    }
    __syncthreads();
    
    // Локальные аккумуляторы для каждого потока
    double local_avg_sum = 0.0;
    double local_open_min = DBL_MAX;
    double local_open_max = -DBL_MAX;
    double local_close_min = DBL_MAX;
    double local_close_max = -DBL_MAX;
    
    // Каждый поток обрабатывает свою часть тиков
    for (int i = threadIdx.x; i < count; i += blockDim.x) {
        int tick_idx = offset + i;
        double avg = (low[tick_idx] + high[tick_idx]) / 2.0;
        local_avg_sum += avg;
        local_open_min = min(local_open_min, open[tick_idx]);
        local_open_max = max(local_open_max, open[tick_idx]);
        local_close_min = min(local_close_min, close[tick_idx]);
        local_close_max = max(local_close_max, close[tick_idx]);
    }
    
    // Редукция с использованием атомарных операций
    atomicAdd(&s_avg_sum, local_avg_sum);
    atomicMin(reinterpret_cast<unsigned long long*>(&s_open_min), 
              __double_as_longlong(local_open_min));
    atomicMax(reinterpret_cast<unsigned long long*>(&s_open_max),
              __double_as_longlong(local_open_max));
    atomicMin(reinterpret_cast<unsigned long long*>(&s_close_min),
              __double_as_longlong(local_close_min));
    atomicMax(reinterpret_cast<unsigned long long*>(&s_close_max),
              __double_as_longlong(local_close_max));
    
    __syncthreads();
    
    // Первый поток записывает результат
    if (threadIdx.x == 0) {
        GpuPeriodStats stats;
        stats.period = unique_periods[period_idx];
        stats.avg = s_avg_sum / static_cast<double>(count);
        stats.open_min = s_open_min;
        stats.open_max = s_open_max;
        stats.close_min = s_close_min;
        stats.close_max = s_close_max;
        stats.count = count;
        out_stats[period_idx] = stats;
    }
}

// ============================================================================
// Простой kernel для агрегации (один поток на период)
// Используется когда периодов много и тиков в каждом мало
// ============================================================================

__global__ void aggregate_periods_simple_kernel(
    const double* __restrict__ open,
    const double* __restrict__ high,
    const double* __restrict__ low,
    const double* __restrict__ close,
    const int64_t* __restrict__ unique_periods,
    const int* __restrict__ offsets,
    const int* __restrict__ counts,
    int num_periods,
    GpuPeriodStats* __restrict__ out_stats)
{
    int period_idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (period_idx >= num_periods) return;
    
    int offset = offsets[period_idx];
    int count = counts[period_idx];
    
    double avg_sum = 0.0;
    double open_min = DBL_MAX;
    double open_max = -DBL_MAX;
    double close_min = DBL_MAX;
    double close_max = -DBL_MAX;
    
    for (int i = 0; i < count; i++) {
        int tick_idx = offset + i;
        double avg = (low[tick_idx] + high[tick_idx]) / 2.0;
        avg_sum += avg;
        open_min = min(open_min, open[tick_idx]);
        open_max = max(open_max, open[tick_idx]);
        close_min = min(close_min, close[tick_idx]);
        close_max = max(close_max, close[tick_idx]);
    }
    
    GpuPeriodStats stats;
    stats.period = unique_periods[period_idx];
    stats.avg = avg_sum / static_cast<double>(count);
    stats.open_min = open_min;
    stats.open_max = open_max;
    stats.close_min = close_min;
    stats.close_max = close_max;
    stats.count = count;
    out_stats[period_idx] = stats;
}


// ============================================================================
// Проверка доступности GPU
// ============================================================================

extern "C" int gpu_is_available() {
    int n = 0;
    cudaError_t err = cudaGetDeviceCount(&n);
    if (err != cudaSuccess) return 0;
    return (n > 0) ? 1 : 0;
}

// ============================================================================
// Главная функция агрегации на GPU
// ============================================================================

extern "C" int gpu_aggregate_periods(
    const double* h_timestamps,
    const double* h_open,
    const double* h_high,
    const double* h_low,
    const double* h_close,
    int num_ticks,
    int64_t interval,
    GpuPeriodStats** h_out_stats,
    int* out_num_periods)
{
    if (num_ticks == 0) {
        *h_out_stats = nullptr;
        *out_num_periods = 0;
        return 0;
    }
    
    std::ostringstream output;
    double total_start = get_time_ms();
    
    // ========================================================================
    // Шаг 1: Выделение памяти и копирование данных на GPU
    // ========================================================================
    double step1_start = get_time_ms();
    
    double* d_timestamps = nullptr;
    double* d_open = nullptr;
    double* d_high = nullptr;
    double* d_low = nullptr;
    double* d_close = nullptr;
    int64_t* d_period_ids = nullptr;
    
    size_t ticks_bytes = num_ticks * sizeof(double);
    
    CUDA_CHECK(cudaMalloc(&d_timestamps, ticks_bytes));
    CUDA_CHECK(cudaMalloc(&d_open, ticks_bytes));
    CUDA_CHECK(cudaMalloc(&d_high, ticks_bytes));
    CUDA_CHECK(cudaMalloc(&d_low, ticks_bytes));
    CUDA_CHECK(cudaMalloc(&d_close, ticks_bytes));
    CUDA_CHECK(cudaMalloc(&d_period_ids, num_ticks * sizeof(int64_t)));
    
    CUDA_CHECK(cudaMemcpy(d_timestamps, h_timestamps, ticks_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_open, h_open, ticks_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_high, h_high, ticks_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_low, h_low, ticks_bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_close, h_close, ticks_bytes, cudaMemcpyHostToDevice));
    
    double step1_ms = get_time_ms() - step1_start;
    
    // ========================================================================
    // Шаг 2: Вычисление period_id для каждого тика
    // ========================================================================
    double step2_start = get_time_ms();
    
    const int BLOCK_SIZE = 256;
    int num_blocks = (num_ticks + BLOCK_SIZE - 1) / BLOCK_SIZE;
    
    compute_period_ids_kernel<<<num_blocks, BLOCK_SIZE>>>(
        d_timestamps, d_period_ids, num_ticks, interval);
    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());
    
    double step2_ms = get_time_ms() - step2_start;
    
    // ========================================================================
    // Шаг 3: RLE (Run-Length Encode) для нахождения уникальных периодов
    // ========================================================================
    double step3_start = get_time_ms();
    
    int64_t* d_unique_periods = nullptr;
    int* d_counts = nullptr;
    int* d_num_runs = nullptr;
    
    CUDA_CHECK(cudaMalloc(&d_unique_periods, num_ticks * sizeof(int64_t)));
    CUDA_CHECK(cudaMalloc(&d_counts, num_ticks * sizeof(int)));
    CUDA_CHECK(cudaMalloc(&d_num_runs, sizeof(int)));
    
    // Определяем размер временного буфера для CUB
    void* d_temp_storage = nullptr;
    size_t temp_storage_bytes = 0;
    
    cub::DeviceRunLengthEncode::Encode(
        d_temp_storage, temp_storage_bytes,
        d_period_ids, d_unique_periods, d_counts, d_num_runs,
        num_ticks);
    
    CUDA_CHECK(cudaMalloc(&d_temp_storage, temp_storage_bytes));
    
    cub::DeviceRunLengthEncode::Encode(
        d_temp_storage, temp_storage_bytes,
        d_period_ids, d_unique_periods, d_counts, d_num_runs,
        num_ticks);
    CUDA_CHECK(cudaGetLastError());
    
    // Копируем количество уникальных периодов
    int num_periods = 0;
    CUDA_CHECK(cudaMemcpy(&num_periods, d_num_runs, sizeof(int), cudaMemcpyDeviceToHost));
    
    cudaFree(d_temp_storage);
    d_temp_storage = nullptr;
    
    double step3_ms = get_time_ms() - step3_start;
    
    // ========================================================================
    // Шаг 4: Exclusive Scan для вычисления offsets
    // ========================================================================
    double step4_start = get_time_ms();
    
    int* d_offsets = nullptr;
    CUDA_CHECK(cudaMalloc(&d_offsets, num_periods * sizeof(int)));
    
    temp_storage_bytes = 0;
    cub::DeviceScan::ExclusiveSum(
        d_temp_storage, temp_storage_bytes,
        d_counts, d_offsets, num_periods);
    
    CUDA_CHECK(cudaMalloc(&d_temp_storage, temp_storage_bytes));
    
    cub::DeviceScan::ExclusiveSum(
        d_temp_storage, temp_storage_bytes,
        d_counts, d_offsets, num_periods);
    CUDA_CHECK(cudaGetLastError());
    
    cudaFree(d_temp_storage);
    
    double step4_ms = get_time_ms() - step4_start;
    
    // ========================================================================
    // Шаг 5: Агрегация периодов
    // ========================================================================
    double step5_start = get_time_ms();
    
    GpuPeriodStats* d_out_stats = nullptr;
    CUDA_CHECK(cudaMalloc(&d_out_stats, num_periods * sizeof(GpuPeriodStats)));
    
    // Выбор ядра через переменную окружения USE_BLOCK_KERNEL
    const char* env_block_kernel = std::getenv("USE_BLOCK_KERNEL");
    if (env_block_kernel == nullptr) {
        printf("Error: Environment variable USE_BLOCK_KERNEL is not set\n");
        return -1;
    }
    bool use_block_kernel = std::atoi(env_block_kernel) != 0;
    
    if (use_block_kernel) {
        // Блочное ядро: один блок на период, потоки параллельно обрабатывают тики
        // Лучше для больших интервалов с множеством тиков в каждом периоде
        aggregate_periods_kernel<<<num_periods, BLOCK_SIZE>>>(
            d_open, d_high, d_low, d_close,
            d_unique_periods, d_offsets, d_counts,
            num_periods, d_out_stats);
    } else {
        // Простое ядро: один поток на период
        // Лучше для множества периодов с малым количеством тиков в каждом
        int agg_blocks = (num_periods + BLOCK_SIZE - 1) / BLOCK_SIZE;
        aggregate_periods_simple_kernel<<<agg_blocks, BLOCK_SIZE>>>(
            d_open, d_high, d_low, d_close,
            d_unique_periods, d_offsets, d_counts,
            num_periods, d_out_stats);
    }


    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());
    
    double step5_ms = get_time_ms() - step5_start;
    
    // ========================================================================
    // Шаг 6: Копирование результатов на CPU
    // ========================================================================
    double step6_start = get_time_ms();
    
    GpuPeriodStats* h_stats = new GpuPeriodStats[num_periods];
    CUDA_CHECK(cudaMemcpy(h_stats, d_out_stats, num_periods * sizeof(GpuPeriodStats), 
                          cudaMemcpyDeviceToHost));
    
    double step6_ms = get_time_ms() - step6_start;
    
    // ========================================================================
    // Шаг 7: Освобождение GPU памяти
    // ========================================================================
    double step7_start = get_time_ms();
    
    cudaFree(d_timestamps);
    cudaFree(d_open);
    cudaFree(d_high);
    cudaFree(d_low);
    cudaFree(d_close);
    cudaFree(d_period_ids);
    cudaFree(d_unique_periods);
    cudaFree(d_counts);
    cudaFree(d_offsets);
    cudaFree(d_num_runs);
    cudaFree(d_out_stats);
    
    double step7_ms = get_time_ms() - step7_start;
    
    // ========================================================================
    // Итого
    // ========================================================================
    double total_ms = get_time_ms() - total_start;
    
    // Формируем весь вывод одной строкой
    output << "  GPU aggregation (" << num_ticks << " ticks, interval=" << interval << " sec, kernel=" << (use_block_kernel ? "block" : "simple") << "):\n";
    output << "    1. Malloc + H->D copy:  " << std::fixed << std::setprecision(3) << std::setw(7) << step1_ms << " ms\n";
    output << "    2. Compute period_ids:  " << std::setw(7) << step2_ms << " ms\n";
    output << "    3. RLE (CUB):           " << std::setw(7) << step3_ms << " ms (" << num_periods << " periods)\n";
    output << "    4. Exclusive scan:      " << std::setw(7) << step4_ms << " ms\n";
    output << "    5. Aggregation kernel:  " << std::setw(7) << step5_ms << " ms (" << (use_block_kernel ? "block" : "simple") << ")\n";
    output << "    6. D->H copy:           " << std::setw(7) << step6_ms << " ms\n";
    output << "    7. Free GPU memory:     " << std::setw(7) << step7_ms << " ms\n";
    output << "    GPU TOTAL:              " << std::setw(7) << total_ms << " ms\n";
    
    // Выводим всё одним принтом
    printf("%s", output.str().c_str());
    fflush(stdout);
    
    *h_out_stats = h_stats;
    *out_num_periods = num_periods;
    
    return 0;
}

// ============================================================================
// Освобождение памяти результатов
// ============================================================================

extern "C" void gpu_free_results(GpuPeriodStats* stats) {
    delete[] stats;
}
\end{lstlisting}
\newpage


\begin{center}
\section*{ПРИЛОЖЕНИЕ З}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ З}
\label{8}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#include "intervals.hpp"
#include "utils.hpp"
#include <mpi.h>
#include <algorithm>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <ctime>
#include <limits>

// Вспомогательная структура для накопления min/max в интервале
struct IntervalAccumulator {
    PeriodIndex start_period;
    double start_avg;
    double open_min;
    double open_max;
    double close_min;
    double close_max;
    
    void init(const PeriodStats& p) {
        start_period = p.period;
        start_avg = p.avg;
        open_min = p.open_min;
        open_max = p.open_max;
        close_min = p.close_min;
        close_max = p.close_max;
    }
    
    void update(const PeriodStats& p) {
        open_min = std::min(open_min, p.open_min);
        open_max = std::max(open_max, p.open_max);
        close_min = std::min(close_min, p.close_min);
        close_max = std::max(close_max, p.close_max);
    }
    
    Interval finalize(const PeriodStats& end_period, double change) const {
        Interval iv;
        iv.start_period = start_period;
        iv.end_period = end_period.period;
        iv.start_avg = start_avg;
        iv.end_avg = end_period.avg;
        iv.change = change;
        iv.open_min = std::min(open_min, end_period.open_min);
        iv.open_max = std::max(open_max, end_period.open_max);
        iv.close_min = std::min(close_min, end_period.close_min);
        iv.close_max = std::max(close_max, end_period.close_max);
        return iv;
    }
};

// Упакованная структура PeriodStats для MPI передачи (8 doubles)
struct PackedPeriodStats {
    double period;   // PeriodIndex as double
    double avg;
    double open_min;
    double open_max;
    double close_min;
    double close_max;
    double count;    // int64_t as double
    double valid;    // флаг валидности (1.0 = valid, 0.0 = invalid)
    
    void pack(const PeriodStats& ps) {
        period = static_cast<double>(ps.period);
        avg = ps.avg;
        open_min = ps.open_min;
        open_max = ps.open_max;
        close_min = ps.close_min;
        close_max = ps.close_max;
        count = static_cast<double>(ps.count);
        valid = 1.0;
    }
    
    PeriodStats unpack() const {
        PeriodStats ps;
        ps.period = static_cast<PeriodIndex>(period);
        ps.avg = avg;
        ps.open_min = open_min;
        ps.open_max = open_max;
        ps.close_min = close_min;
        ps.close_max = close_max;
        ps.count = static_cast<int64_t>(count);
        return ps;
    }
    
    bool is_valid() const { return valid > 0.5; }
    void set_invalid() { valid = 0.0; }
};

IntervalResult find_intervals_parallel(
    const std::vector<PeriodStats>& periods,
    int rank, int size,
    double threshold)
{
    IntervalResult result;
    result.compute_time = 0.0;
    result.wait_time = 0.0;
    
    if (periods.empty()) {
        if (rank < size - 1) {
            PackedPeriodStats invalid;
            invalid.set_invalid();
            MPI_Send(&invalid, 8, MPI_DOUBLE, rank + 1, 0, MPI_COMM_WORLD);
        }
        return result;
    }
    
    double compute_start = MPI_Wtime();
    
    size_t process_until = (rank == size - 1) ? periods.size() : periods.size() - 1;
    
    IntervalAccumulator acc;
    size_t start_idx = 0;
    bool have_pending_interval = false;
    
    if (rank > 0) {
        double wait_start = MPI_Wtime();
        
        PackedPeriodStats received;
        MPI_Recv(&received, 8, MPI_DOUBLE, rank - 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        
        result.wait_time = MPI_Wtime() - wait_start;
        compute_start = MPI_Wtime();
        
        if (received.is_valid()) {
            PeriodStats prev_period = received.unpack();
            
            for (start_idx = 0; start_idx < periods.size(); start_idx++) {
                if (periods[start_idx].period > prev_period.period) {
                    break;
                }
            }
            
            if (start_idx < process_until) {
                acc.init(prev_period);
                have_pending_interval = true;
                
                for (size_t i = start_idx; i < process_until; i++) {
                    acc.update(periods[i]);
                    
                    double change = std::abs(periods[i].avg - acc.start_avg) / acc.start_avg;
                    
                    if (change >= threshold) {
                        result.intervals.push_back(acc.finalize(periods[i], change));
                        have_pending_interval = false;
                        
                        start_idx = i + 1;
                        if (start_idx < process_until) {
                            acc.init(periods[start_idx]);
                            have_pending_interval = true;
                        }
                    }
                }
            }
        } else {
            if (process_until > 0) {
                acc.init(periods[0]);
                have_pending_interval = true;
                start_idx = 0;
            }
        }
    } else {
        if (process_until > 0) {
            acc.init(periods[0]);
            have_pending_interval = true;
            start_idx = 0;
        }
    }
    
    if (rank == 0 && have_pending_interval) {
        for (size_t i = 1; i < process_until; i++) {
            acc.update(periods[i]);
            
            double change = std::abs(periods[i].avg - acc.start_avg) / acc.start_avg;
            
            if (change >= threshold) {
                result.intervals.push_back(acc.finalize(periods[i], change));
                have_pending_interval = false;
                
                start_idx = i + 1;
                if (start_idx < process_until) {
                    acc.init(periods[start_idx]);
                    have_pending_interval = true;
                }
            }
        }
    }
    
    if (rank == size - 1 && have_pending_interval && !periods.empty()) {
        const auto& last_period = periods.back();
        double change = std::abs(last_period.avg - acc.start_avg) / acc.start_avg;
        result.intervals.push_back(acc.finalize(last_period, change));
    }
    
    result.compute_time = MPI_Wtime() - compute_start;
    
    if (rank < size - 1) {
        PackedPeriodStats to_send;
        
        if (have_pending_interval) {
            PeriodStats start_period;
            start_period.period = acc.start_period;
            start_period.avg = acc.start_avg;
            start_period.open_min = acc.open_min;
            start_period.open_max = acc.open_max;
            start_period.close_min = acc.close_min;
            start_period.close_max = acc.close_max;
            start_period.count = 0;
            to_send.pack(start_period);
        } else if (periods.size() >= 2) {
            to_send.pack(periods[periods.size() - 2]);
        } else {
            to_send.set_invalid();
        }
        
        MPI_Send(&to_send, 8, MPI_DOUBLE, rank + 1, 0, MPI_COMM_WORLD);
    }
    
    return result;
}

double collect_intervals(
    std::vector<Interval>& local_intervals,
    int rank, int size)
{
    double wait_time = 0.0;
    
    if (rank == 0) {
        for (int r = 1; r < size; r++) {
            double wait_start = MPI_Wtime();
            
            int count;
            MPI_Recv(&count, 1, MPI_INT, r, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
            
            if (count > 0) {
                std::vector<double> buffer(count * 9);
                MPI_Recv(buffer.data(), count * 9, MPI_DOUBLE, r, 2, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
                
                for (int i = 0; i < count; i++) {
                    Interval iv;
                    iv.start_period = static_cast<PeriodIndex>(buffer[i * 9 + 0]);
                    iv.end_period = static_cast<PeriodIndex>(buffer[i * 9 + 1]);
                    iv.open_min = buffer[i * 9 + 2];
                    iv.open_max = buffer[i * 9 + 3];
                    iv.close_min = buffer[i * 9 + 4];
                    iv.close_max = buffer[i * 9 + 5];
                    iv.start_avg = buffer[i * 9 + 6];
                    iv.end_avg = buffer[i * 9 + 7];
                    iv.change = buffer[i * 9 + 8];
                    local_intervals.push_back(iv);
                }
            }
            
            wait_time += MPI_Wtime() - wait_start;
        }
        
        std::sort(local_intervals.begin(), local_intervals.end(),
            [](const Interval& a, const Interval& b) {
                return a.start_period < b.start_period;
            });
    } else {
        int count = static_cast<int>(local_intervals.size());
        MPI_Send(&count, 1, MPI_INT, 0, 1, MPI_COMM_WORLD);
        
        if (count > 0) {
            std::vector<double> buffer(count * 9);
            for (int i = 0; i < count; i++) {
                const auto& iv = local_intervals[i];
                buffer[i * 9 + 0] = static_cast<double>(iv.start_period);
                buffer[i * 9 + 1] = static_cast<double>(iv.end_period);
                buffer[i * 9 + 2] = iv.open_min;
                buffer[i * 9 + 3] = iv.open_max;
                buffer[i * 9 + 4] = iv.close_min;
                buffer[i * 9 + 5] = iv.close_max;
                buffer[i * 9 + 6] = iv.start_avg;
                buffer[i * 9 + 7] = iv.end_avg;
                buffer[i * 9 + 8] = iv.change;
            }
            MPI_Send(buffer.data(), count * 9, MPI_DOUBLE, 0, 2, MPI_COMM_WORLD);
        }
    }
    
    return wait_time;
}

std::string period_index_to_datetime(PeriodIndex period) {
    int64_t interval = get_aggregation_interval();
    time_t ts = static_cast<time_t>(period) * interval;
    struct tm* tm_info = gmtime(&ts);
    
    std::ostringstream oss;
    oss << std::setfill('0') 
        << (tm_info->tm_year + 1900) << "-"
        << std::setw(2) << (tm_info->tm_mon + 1) << "-"
        << std::setw(2) << tm_info->tm_mday << " "
        << std::setw(2) << tm_info->tm_hour << ":"
        << std::setw(2) << tm_info->tm_min << ":"
        << std::setw(2) << tm_info->tm_sec;
    
    return oss.str();
}

void write_intervals(const std::string& filename, const std::vector<Interval>& intervals) {
    std::ofstream out(filename);
    
    out << std::fixed << std::setprecision(2);
    out << "start_datetime,end_datetime,open_min,open_max,close_min,close_max,start_avg,end_avg,change\n";
    
    for (const auto& iv : intervals) {
        out << period_index_to_datetime(iv.start_period) << ","
            << period_index_to_datetime(iv.end_period) << ","
            << iv.open_min << ","
            << iv.open_max << ","
            << iv.close_min << ","
            << iv.close_max << ","
            << iv.start_avg << ","
            << iv.end_avg << ","
            << std::setprecision(6) << iv.change << "\n";
    }
}
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ И}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ И}
\label{9}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#include "utils.hpp"
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <numeric>

int get_num_cpu_threads() {
    const char* env_threads = std::getenv("NUM_CPU_THREADS");
    int num_cpu_threads = 1;
    if (env_threads) {
        num_cpu_threads = std::atoi(env_threads);
        if (num_cpu_threads < 1) num_cpu_threads = 1;
    }
    return num_cpu_threads;
}

std::string get_env(const char* name) {
    const char* env = std::getenv(name);
    if (!env) {
        throw std::runtime_error(std::string("Environment variable not set: ") + name);
    }
    return std::string(env);
}

std::string get_data_path() {
    return get_env("DATA_PATH");
}

std::vector<int> get_data_read_shares() {
    std::vector<int> shares;
    std::stringstream ss(get_env("DATA_READ_SHARES"));
    std::string item;
    while (std::getline(ss, item, ',')) {
        shares.push_back(std::stoi(item));
    }
    return shares;
}

int64_t get_read_overlap_bytes() {
    return std::stoll(get_env("READ_OVERLAP_BYTES"));
}

int64_t get_aggregation_interval() {
    return std::stoll(get_env("AGGREGATION_INTERVAL"));
}

bool get_use_cuda() {
    return std::stoi(get_env("USE_CUDA")) != 0;
}

int64_t get_file_size(const std::string& path) {
    std::ifstream file(path, std::ios::binary | std::ios::ate);
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file: " + path);
    }
    return static_cast<int64_t>(file.tellg());
}

ByteRange calculate_byte_range(int rank, int size, int64_t file_size,
                               const std::vector<int>& shares, int64_t overlap_bytes) {
    std::vector<int> effective_shares;
    if (shares.size() == static_cast<size_t>(size)) {
        effective_shares = shares;
    } else {
        effective_shares.assign(size, 1);
    }
    
    int total_shares = std::accumulate(effective_shares.begin(), effective_shares.end(), 0);
    int64_t bytes_per_share = file_size / total_shares;
    
    int64_t base_start = 0;
    for (int i = 0; i < rank; i++) {
        base_start += bytes_per_share * effective_shares[i];
    }
    
    int64_t base_end = base_start + bytes_per_share * effective_shares[rank];
    
    ByteRange range;
    
    if (rank == 0) {
        range.start = 0;
        range.end = std::min(base_end + overlap_bytes, file_size);
    } else if (rank == size - 1) {
        range.start = std::max(base_start - overlap_bytes, static_cast<int64_t>(0));
        range.end = file_size;
    } else {
        range.start = std::max(base_start - overlap_bytes, static_cast<int64_t>(0));
        range.end = std::min(base_end + overlap_bytes, file_size);
    }
    
    return range;
}

void trim_edge_periods(std::vector<PeriodStats>& periods, int rank, int size) {
    if (periods.empty()) return;
    
    if (rank == 0) {
        periods.pop_back();
    } else if (rank == size - 1) {
        periods.erase(periods.begin());
    } else {
        periods.pop_back();
        periods.erase(periods.begin());
    }
}
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ К}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ К}
\label{10}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#pragma once
#include <cstdint>

using PeriodIndex = int64_t;

// Агрегированные данные за один период
struct PeriodStats {
    PeriodIndex period;   // индекс периода (timestamp / AGGREGATION_INTERVAL)
    double avg;           // среднее значение (Low + High) / 2 по всем записям
    double open_min;      // минимальный Open за период
    double open_max;      // максимальный Open за период
    double close_min;     // минимальный Close за период
    double close_max;     // максимальный Close за период
    int64_t count;        // количество записей, по которым агрегировали
};
\end{lstlisting}


\newpage
\begin{center}
\section*{ПРИЛОЖЕНИЕ Л}
\end{center}
\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ Л}
\label{11}
\begin{lstlisting}[language=sh, basicstyle=\ttfamily\scriptsize, escapeinside={{*@}{@*}}]
#pragma once
#include <cstdint>

struct Record {
    double timestamp;
    double open;
    double high;
    double low;
    double close;
    double volume;
};
\end{lstlisting}

\end{document}
