Русский
Русский
English
Статистика
Реклама

Cuda

Перевод О появлении поддержки CUDA в WSL 2

05.07.2020 16:17:42 | Автор: admin
Компания Microsoft, откликаясь на многочисленные просьбы пользователей, представила в мае 2020 года на конференции Build новую возможность подсистемы Windows для Linux 2 (Windows Subsystem for Linux 2, WSL 2) поддержку видеоускорителей. Это позволит запускать в WSL 2 приложения, занимающиеся специализированными вычислениями. Поддержка GPU откроет дорогу профессиональным инструментам, поможет решать в WSL 2 задачи, которые в настоящее время можно решать только в Linux. Теперь подобные задачи можно будет решать и в Windows, пользуясь возможностями GPU.

Крайне важно тут и то, что в WSL приходит поддержка программно-аппаратной архитектуры параллельных вычислений NVIDIA CUDA.

Материал, перевод которого мы публикуем, подготовлен специалистами NVIDIA. Здесь речь пойдёт о том, чего можно ожидать от CUDA в Public Preview-версии WSL 2.


Запуск AI-фреймворков, используемых в Linux, в WSL 2-контейнерах

Что такое WSL?


WSL это возможность Windows 10, которая позволяет использовать инструменты командной строки Linux непосредственно в Windows без необходимости сталкиваться со сложностями применения конфигурации двойной загрузки. WSL представляет собой контейнеризованное окружение, которое тесно интегрировано с ОС Microsoft Windows. Это позволяет запускать Linux-приложения вместе с традиционными Windows-приложения и с современными приложениями, распространяемыми через Microsoft Store.

WSL это, преимущественно, инструмент для разработчиков. Если вы работаете над некими проектами в контейнерах Linux, это значит, что вы можете заниматься теми же делами локально, на Windows-компьютере, используя привычные инструменты Linux. Обычно, чтобы запустить подобные приложения на Windows, нужно потратить много времени на настройку системы, нужны какие-то сторонние фреймворки, библиотеки. Теперь, с выходом WSL 2, всё изменилось. Благодаря WSL 2 в мир Windows пришла полная поддержка ядра Linux.

WSL 2 и технология паравиртуализации GPU (GPU Paravirtualization, GPU-PV) позволили Microsoft вывести поддержку Linux в Windows на новый уровень, сделав возможным запуск вычислительных нагрузок, рассчитанных на GPU. Ниже мы подробнее поговорим о том, как выглядит использование GPU в WSL 2.

Если вас интересует тема поддержки видеоускорителей в WSL 2 взгляните на этот материал и на этот репозиторий.

CUDA в WSL


Для того чтобы воспользоваться возможностями GPU в WSL 2, необходимо, чтобы на компьютере был бы установлен видеодрайвер, поддерживающий Microsoft WDDM. Подобные драйверы создают производители видеокарт такие, как NVIDIA.

Технология CUDA позволяет заниматься разработкой программ для видеоускорителей NVIDIA. Эта технология поддерживается в WDDM, в Windows, уже многие годы. Новый контейнер WSL 2 от Microsoft даёт возможности по GPU-ускорению вычислений, которыми может воспользоваться технология CUDA, что позволяет выполнять в среде WSL программы, рассчитанные на CUDA. Подробности об этом можно узнать в руководстве пользователя по работе с CUDA в WSL.

Поддержка CUDA в WSL включена в драйверы NVIDIA, рассчитанные на WDDM 2.9. Эти драйверы достаточно просто установить в Windows. Драйверы пользовательского режима CUDA в WSL (libcuda.so) автоматически становятся доступными внутри контейнера, их может обнаружить загрузчик.

Команда NVIDIA, занимающаяся разработкой драйверов, добавила в драйвер CUDA поддержку WDDM и GPU-PV. Сделано это для того чтобы эти драйверы могли бы работать в среде Linux, запущенной на Windows. Эти драйверы всё ещё находятся в статусе Preview, их релиз состоится только тогда, кода состоится официальный релиз WSL с поддержкой GPU. Подробности о выпуске драйверов можно найти здесь.

На следующем рисунке показана схема подключения драйвера CUDA к WDDM внутри гостевой системы Linux.


WDDM-драйвер пользовательского режима с поддержкой CUDA, выполняющийся в гостевой системе Linux

Предположим, вы разработчик, который установил дистрибутив WSL на последнюю сборку Windows из Fast Ring (сборка 20149 или старше) Microsoft Windows Insider Program (WIP). Если вы переключились на WSL 2 и у вас есть GPU NVIDIA, вы можете испытать драйвер и запустить свой код, выполняющий GPU-вычисления, в WSL 2. Для этого достаточно установить драйвер в хост-системе Windows и открыть WSL-контейнер. Здесь вам, без дополнительных усилий, будет доступна возможность работы с приложениями, использующими CUDA. На следующем рисунке показано, как в WSL 2-контейнере выполняется TensorFlow-приложение, использующее возможности CUDA.


TensorFlow-контейнер, выполняющийся в WSL 2

То, что в WSL теперь доступна технология CUDA, позволяет выполнять в WSL приложения, которые раньше можно было выполнять только в обычном Linux-окружении.

NVIDIA всё ещё активно работает над этим проектом и вносит в него улучшения. Мы, кроме прочего, работаем над добавлением в WDDM API, которые раньше были рассчитаны исключительно на Linux. Это приведёт к тому, что в WSL, без дополнительных усилий со стороны пользователя, сможет работать всё больше и больше приложений.

Ещё один интересующий нас вопрос это производительность. Как уже было сказано, поддержка GPU в WSL 2 серьёзно использует технологию GPU-PV. Это может плохо повлиять на скорость выполнения небольших задач на GPU, в ситуациях, когда не будет использоваться конвейеризация. Прямо сейчас мы работаем в направлении как можно более сильного сокращения подобных эффектов.

NVML


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

Мы начали работу с основного драйвера CUDA, что позволит пользователям запускать большую часть существующих CUDA-приложений даже на ранней стадии появления поддержки CUDA в WSL. Но, как оказалось, некоторые контейнеры и приложения используют NVML для получения информации о GPU даже до загрузки CUDA. Именно поэтому добавление поддержки NVML в WSL это одна из наших первоочередных задач. Вполне возможно то, что скоро мы сможем поделиться хорошими новостями по поводу решения этой задачи.

GPU-контейнеры в WSL


В дополнение к поддержке в WSL 2 DirectX и CUDA, NVIDIA работает над добавлением в WSL 2 поддержки NVIDIA Container Toolkit (раньше эта технология называлась nvidia-docker2). Контейнеризованные GPU-приложения, которые дата-сайентисты создают в расчёте на запуск в локальной или облачной среде Linux, теперь могут, без внесения в них каких-либо изменений, запускаться в WSL 2, на компьютерах, работающих под управлением Windows.

Каких-то особых пакетов WSL для этого не требуется. Библиотека времени выполнения NVIDIA (libnvidia-container) может динамически обнаруживать библиотеку libdxcore и пользоваться ей в ситуации, когда код выполняется в WSL 2-среде с поддержкой GPU-ускорения. Это происходит автоматически, после установки пакетов Docker и NVIDIA Container Toolkit, так же, как и на Linux. Это позволяет, без дополнительных усилий, запускать в WSL 2 контейнеры, в которых используются возможности GPU.

Мы настоятельно рекомендуем тем, кто хочет пользоваться опцией --gpus, установить последнюю версию инструментов Docker (19.03 или свежее). Для того чтобы включить поддержку WSL 2, следуйте инструкциям для вашего дистрибутива Linux и установите самую свежую из доступных версий nvidia-container-toolkit.

Как это работает? Все задачи, характерные для WSL 2, решаются средствами библиотеки libnvidia-container. Теперь эта библиотека может, во время выполнения, обнаруживать присутствие libdxcore.so и использовать эту библиотеку для обнаружения всех GPU, видимых этому интерфейсу.

Если эти GPU нужно использовать в контейнере, то, с помощью libdxcore.so, выполняется обращение к месту хранения драйверов, к папке, которая содержит все библиотеки драйверов для хост-системы Windows и WSL 2. Библиотека libnvidia-container.so отвечает за настройку контейнера таким образом, чтобы можно было бы корректно обратиться к хранилищу драйверов. Эта же библиотека отвечает за настройку базовых библиотек, поддерживаемых WSL 2. Схема этого показана на следующем рисунке.


Схема обнаружения и отображения в контейнер хранилища драйверов, используемая libnvidia-container.so в WSL 2

Кроме того, это отличается от логики, используемой за пределами WSL. Этот процесс полностью абстрагирован с помощью libnvidia-container.so и он, для конечного пользователя, должен быть как можно прозрачнее. Одно из ограничений этой ранней версии заключается в невозможности выбора GPU в окружениях, в которых имеется несколько GPU. В контейнере всегда видны все GPU.

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

Сейчас мы расскажем о том, как запускать в WSL 2 контейнеры TensorFlow и N-body, рассчитанные на использование GPU NVIDIA для ускорения вычислений.

Запуск контейнера N-body


Установим Docker, воспользовавшись скриптом установки:

user@PCName:/mnt/c$ curl https://get.docker.com | sh

Установим NVIDIA Container Toolkit. Поддержка WSL 2 доступна, начиная с nvidia-docker2 v2.3 и с библиотеки времени выполнения libnvidia-container 1.2.0-rc.1.

Настроим репозитории stable и experimental и GPG-ключ. Изменения в коде времени выполнения, рассчитанные на поддержку WSL 2, доступны в экспериментальном репозитории.

user@PCName:/mnt/c$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID)user@PCName:/mnt/c$ curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -user@PCName:/mnt/c$ curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.listuser@PCName:/mnt/c$ curl -s -L https://nvidia.github.io/libnvidia-container/experimental/$distribution/libnvidia-container-experimental.list | sudo tee /etc/apt/sources.list.d/libnvidia-container-experimental.list

Установим пакеты времени выполнения NVIDIA и их зависимости:

user@PCName:/mnt/c$ sudo apt-get updateuser@PCName:/mnt/c$ sudo apt-get install -y nvidia-docker2

Откроем WSL-контейнер и запустим в нём демон Docker. Если всё сделано правильно после этого можно будет увидеть служебные сообщения dockerd.

user@PCName:/mnt/c$ sudo dockerd


Запуск демона Docker

В другом окне WSL загрузим и запустим контейнер симуляции N-body. Нужно, чтобы у пользователя, выполняющего эту задачу, было бы достаточно полномочий для загрузки контейнера. Следующие команды может потребоваться запустить с использованием sudo. В выводимых данных можно увидеть сведения о GPU.

user@PCName:/mnt/c$ docker run --gpus all nvcr.io/nvidia/k8s/cuda-sample:nbody nbody -gpu -benchmark


Запуск контейнера N-body

Запуск контейнера TensorFlow


Испытаем в Docker, в среде WSL 2, ещё один популярный контейнер TensorFlow.

Загрузим Docker-образ TensorFlow. Для того чтобы избежать проблем с подключением к Docker, выполним следующую команду в режиме sudo:

user@PCName:/mnt/c$ docker pull tensorflow/tensorflow:latest-gpu-py3

Сохраним немного изменённую версию кода из 15 урока руководства по TensorFlow, посвящённого использованию GPU, на диск C хост-системы. Этот диск, по умолчанию, монтируется в контейнере WSL 2 как /mnt/c.

user@PCName:/mnt/c$ vi ./matmul.pyimport sysimport numpy as npimport tensorflow as tffrom datetime import datetimedevice_name = sys.argv[1] # Choose device from cmd line. Options: gpu or cpushape = (int(sys.argv[2]), int(sys.argv[2]))if device_name == "gpu":device_name = "/gpu:0"else:device_name = "/cpu:0"tf.compat.v1.disable_eager_execution()with tf.device(device_name):random_matrix = tf.random.uniform(shape=shape, minval=0, maxval=1)dot_operation = tf.matmul(random_matrix, tf.transpose(random_matrix))sum_operation = tf.reduce_sum(dot_operation)startTime = datetime.now()with tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(log_device_placement=True)) as session:result = session.run(sum_operation)print(result)# Вывод результатовprint("Shape:", shape, "Device:", device_name)print("Time taken:", datetime.now() - startTime)

Ниже показаны результаты выполнения этого скрипта, запущенного со смонтированного в контейнере диска C. Скрипт выполнялся, сначала, с использованием GPU, а потом с использованием CPU. Для удобства объём представленных здесь выходных данных сокращён.

user@PCName:/mnt/c$ docker run --runtime=nvidia --rm -ti -v "${PWD}:/mnt/c" tensorflow/tensorflow:latest-gpu-jupyter python /mnt/c/matmul.py gpu 20000


Результаты выполнения скрипта matmul.py

При использовании GPU в WSL 2-контейнере наблюдается значительное ускорение выполнения кода в сравнении с его выполнением на CPU.

Проведём ещё один эксперимент, рассчитанный на исследование производительности GPU-вычислений. Речь идёт о коде из руководства по Jupyter Notebook. После запуска контейнера вы должны увидеть ссылку на сервер Jupyter Notebook.

user@PCName:/mnt/c$ docker run -it --gpus all -p 8888:8888 tensorflow/tensorflow:latest-gpu-py3-jupyter


Запуск Jupyter Notebook

Теперь у вас должна появиться возможность запускать демонстрационные примеры в среде Jupyter Notebook. Обратите внимание на то, то, что для подключения к Jupyter Notebook с использованием браузера Microsoft Edge, нужно, вместо 127.0.0.1, использовать localhost.

Перейдите в tensorflow-tutorials и запустите блокнот classification.ipynb.

Для того чтобы увидеть результаты ускорения вычислений с помощью GPU, перейдите в меню Cell, выберите Run All и посмотрите журнал в WSL 2-контейнере Jupyter Notebook.


Журнал Jupyter Notebook

Этот демонстрационный пример, да и некоторые другие в данном контейнере, позволяют увидеть проблемы со слоем виртуализации, относящиеся к неоправданно высокой дополнительной нагрузке на систему при решении небольших задач. Выше мы уже говорили об этом. Так как мы запускаем тут очень маленькие учебные модели, время их выполнения на GPU меньше времени, необходимого на решение задач синхронизации. При решении таких вот игрушечных задач в WSL 2, CPU может оказаться эффективнее GPU. Мы занимаемся решением этой проблемы, стремясь ограничить её проявления лишь совсем небольшими рабочими нагрузками, к которым не применяется конвейеризация.

Обзор WSL


Для того чтобы понять то, как поддержка GPU была добавлена в WSL 2, сейчас мы поговорим о том, что собой представляет запуск Linux на Windows, и о том, как контейнеры видят аппаратное обеспечение.

Компания Microsoft представила технологию WSL на конференции Build в 2016 году. Эта технология быстро нашла широкое применение и стала популярной в среде Linux-разработчиков, которым нужно было запускать Windows-приложения, вроде Office, вместе с инструментами разработки для Linux и соответствующими программами.

Система WSL 1 позволяла запускать немодифицированные исполняемые файлы Linux. Однако здесь использовался слой эмуляции ядра Linux, который был реализован в виде подсистемы ядра NT. Эта подсистема обрабатывала вызовы, поступающие от Linux-приложений, перенаправляя их соответствующим механизмам Windows 10.

Система WSL 1 была полезным инструментом, но она не была совместима со всеми Linux-приложениям, так как ей требовалось эмулировать абсолютно все системные вызовы Linux. Кроме того, медленными были операции по работе с файловой системой, что приводило к недопустимо низкой производительности некоторых приложений.

Учитывая это, Microsoft решила пойти другим путём и выпустила WSL 2 новую версию WSL. Контейнеры WSL 2 выполняют полные Linux-дистрибутивы в виртуализованном окружении, но при этом используют все полезные возможности новой системы контейнеризации Windows 10.

В то время как WSL 2 использует Hyper-V-сервисы Windows 10, это не традиционная виртуальная машина, а, скорее, легковесный вспомогательный механизм виртуализации. Этот механизм отвечает за управление виртуальной памятью, связанной с физической памятью, позволяя WSL 2-контейнерам динамически выделять память, обращаясь к хост-системе Windows.

Среди основных целей создания WSL 2 можно отметить увеличение производительности работы с файловой системой и обеспечение совместимости со всеми системными вызовами. Кроме того, WSL 2 создавали, стремясь улучшить уровень интеграции WSL и Windows. Это позволяет удобно работать с Linux-системой, выполняемой в контейнере, пользуясь средствами командной строки Windows. Это, кроме того, повышает удобство работы с файловой системой хоста, автоматически монтируемой в выбранные директории файловой системы контейнера.

WSL 2 была представлена в Windows Insider Program в виде Preview-возможности и была выпущена в самом свежем обновлении Windows 10, в версии 2004.

В WSL 2 из самой свежей версии Windows внесено ещё больше улучшений, которые затрагивают много всего от сетевых стеков, до базовых VHD-механизмов системы хранения данных. Описание всех новых возможностей WSL 2 выходит за рамки этого материала. Подробнее о них можно узнать на этой странице, где приводится сравнение WSL 2 и WSL 1.

Linux-ядро WSL 2


Ядро Linux, применяемое в WSL 2, собрано Microsoft на основе самой свежей стабильной ветки, с использованием исходного кода, доступного на kernel.org. Это ядро было специально настроено для WSL 2, оптимизировано с точки зрения размеров и производительности с целью обеспечения работы Linux в среде Windows. Ядро поддерживается через механизм Windows Update. Это значит, что пользователю не нужно заботиться о том, чтобы загружать последние обновления безопасности и улучшения ядра. Всё это делается автоматически.

Microsoft поддерживает в WSL несколько дистрибутивов Linux. Компания, следуя правилам опенсорс-сообщества, опубликовала в GitHub-репозитории WSL2-Linux-Kernel исходный код ядра WSL 2 с модификациями, необходимыми для интеграции с Windows 10.

Поддержка GPU в WSL


Разработчики Microsoft добавили в WSL 2-контейнеры поддержку реальных GPU с использованием технологии GPU-PV. Здесь графическое ядро операционной системы (dxgkrnl) маршалирует драйверу режима ядра, который находится на хосте, вызовы от компонентов пользовательского режима, выполняемых в гостевой виртуальной машине.

Компания Microsoft разработала эту технологию в виде возможности WDDM, с момента её появления вышло уже несколько релизов Windows. Эта работа была проведена с привлечением независимых производителей аппаратного обеспечения (Independent Hardware Vendor, IHV). Графические драйверы NVIDIA поддерживали GPU-PV начиная с ранних дней появления этой технологии в Preview-версиях продуктов, доступных в Windows Insider Program. Все GPU NVIDIA, поддерживаемые в настоящий момент, могут быть доступны ОС Windows, выполняемой в гостевом режиме, в виртуальной машине, использующей Hyper-V.

Для того чтобы в WSL 2 можно было бы пользоваться возможностями GPU-PV, Microsoft пришлось создать базу своего графического фреймворка для гостевой системы Linux: WDDM с поддержкой протокола GPU-PV. Новый драйвер Microsoft находится за dxgkrnl, за системой, отвечающей за поддержку WDDM в Linux. Код драйвера можно найти в репозитории WSL2-Linux-Kernel.

Ожидается, что dxgkrnl обеспечит поддержку GPU-ускорения в контейнерах WSL 2 в WDDM 2.9. Microsoft говорит о том, что dxgkrnl это GPU-драйвер Linux, основанный на протоколе GPU-PV, и о том, что у него нет ничего общего с Windows-драйвером, имеющим похожее имя.

В настоящее время вы можете загрузить Preview-версию драйвера NVIDIA WDDM 2.9. В ближайшие несколько месяцев этот драйвер будет распространяться через Windows Update в WIP-версии Windows, что делает ненужными ручную загрузку и установку драйвера.

Основные сведения о GPU-PV


Драйвер dxgkrnl делает доступным, в пользовательском режиме гостевой системы Linux, новое устройство /dev/dxg. Сервисный слой ядра D3DKMT, который был доступен в Windows, тоже был портирован, как часть библиотеки dxcore, на Linux. Он взаимодействует с dxgkrnl, используя набор частных IOCTL-вызовов.

Гостевая Linux-версия dxgkrnl подключаются к ядру dxg на Windows-хосте, используя несколько каналов шины VM. Ядро dxg на хосте обрабатывает то, что ему приходит от Linux-процесса, так же, как то, что приходит от обычных Windows-приложений, использующих WDDM. А именно, ядро dxg отправляет то, что получило, KMD (Kernel Mode Driver, драйверу режима ядра, уникальному для каждого HIV). Драйвер режима ядра подготавливает то, что получил, для отправки аппаратному графическому ускорителю. На следующем рисунке показана упрощённая схема взаимодействия Linux-устройства /dev/dxg и KMD.


Упрощённая схема, иллюстрирующая то, как компоненты Windows-хоста обеспечивают работу устройства dxg в гостевой системе Linux

Если говорить об обеспечении подобной схемы работы в гостевых системах Windows, то можно сказать, что драйверы NVIDIA поддерживают GPU-PV в Windows 10 уже довольно давно. GPU NVIDIA могут быть использованы для ускорения вычислений и вывода графики во всех Windows 10-приложениях, использующих слой виртуализации Microsoft. Использование GPU-PV позволяет и работать с vGPU. Вот несколько примеров подобных приложений:


Вот как выглядит запуск DirectX-приложения в контейнере Windows Sandbox с применением видеоускорителя NVIDIA GeForce GTX 1070.


В контейнере Windows Sandbox ускорение графики выполняется средствами NVIDIA GeForce GTX 1070

Поддержка пользовательского режима


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

Библиотека dxcore предоставляет API, который позволяет получать сведения об имеющихся в системе графических адаптерах, совместимых с WDDM. Эту библиотеку задумывали как кросс-платформенную низкоуровневую замену для средства работы с DXGI-адаптерами в Windows и Linux. Библиотека, кроме того, абстрагирует доступ к сервисам dxgkrnl (IOCTL-вызовы в Linux и GDI-вызовы в Windows), используя слой API D3DKMT, который используется CUDA и другими компонентами пользовательского режима, полагающимися на поддержку WDDM в WSL.

По сведениям Microsoft, библиотека dxcore (libdxcore.so) будет доступна и в Windows, и в Linux. NVIDIA планирует добавить в драйвер поддержку DirectX 12 и API CUDA. Эти дополнения нацелены на новые возможности WSL, доступные благодаря WDDM 2.9. Обе библиотеки, представляющие API, будут подключены к dxcore для того чтобы они могли бы давать dxg указания по поводу маршалирования их запросов к KMD на хост-системе.

Попробуйте новые возможности WSL 2


Хотите использовать свой Windows-компьютер для решения настоящих задач из сфер машинного обучения и искусственного интеллекта, и при этом пользоваться всеми удобствами Linux-окружения? Если так, то поддержка CUDA в WSL даёт вам отличную возможность это сделать. Среда WSL это то место, где Docker-контейнеры CUDA показали себя как самое популярное среди дата-сайентистов вычислительное окружение.

  • Для того чтобы получить доступ к Preview-версии WSL 2 с поддержкой GPU-ускорения, вы можете присоединиться к Windows Insider Program.
  • Загрузите свежие драйверы NVIDIA, установите их и попробуйте запустить в WSL 2 какой-нибудь CUDA-контейнер.

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

А вы уже пробовали CUDA в WSL 2?

Подробнее..

Прием всего Bluetooth разом на SDR с CUDA? Легко

31.07.2020 16:11:33 | Автор: admin

В последнее время коллеги по "цеху" независимо друг от друга стали спрашивать меня: как получить c одного SDR-приемника одновременно все каналы Bluetooth? Полоса ведь позволяет, есть SDR с выходной полосой 80 МГц и более. Можно, конечно, сделать это на ПЛИС, но время такой разработки будет довольно большим. Мне давно было известно, что сделать такое на GPU довольно просто, но чтобы так!


Стандарт Bluetooth определяет физический уровень в двух версиях: Classic и Low Energy. Спецификация есть здесь. Документ ужасно большой, читать его целиком опасно для мозга. К счастью, большие компании, производящие измерительную технику, имеют средства для создания наглядных документов по теме. Tektronix и National Instruments, например. У меня совершенно нет шансов в конкуренции с ними по качеству представления материала. Интересующихся прошу изучать по ссылкам.


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


image


Таким образом, нам нужно нарезать полосу 80 МГц на 79 фильтров с шагом настройки 1 МГц и, одновременно с этим, на 40 фильтров с шагом настройки 2 МГц. Частоты следования отсчетов с выходов фильтров должны быть 1 МГц и 2 МГц, соответственно.


Таким образом, нам необходимо две гребенки фильтров.


Для начала выберем параметры этих фильтров исходя из полос сигналов Bluetooth Classic и Bluetooth Low Energy. Нам нужны их импульсные характеристики, чтобы рассчитать нагрузку на вычислительное устройство фильтра. Здесь сразу стоит оговориться, что длины импульсных характеристик мы выбрали исходя из требований "быстрого" алгоритма фильтрации. Суть от этого не меняется. И число коэффициентов импульсной характеристики не должно быть слишком большим, чтобы фильтр был реализуем на вменяемой вычислительной аппаратуре.


Для фильтров с шагом 1 МГц выберем полосу пропускания ФНЧ (половина полосы пропускания полосового фильтра) 500 кГц, длину импульсной характеристики подгоним к 480 отводам. Для фильтров с шагом 2 МГц эти параметры выберем 1 МГц и 240 отводов, соответственно. Тип окна выбираем Кайзера. Рассчитаем импульсные характеристики в filterDesigner и выгрузим их в формате С-header:


Скриншоты из filterDesigner

image
image
image
image


Можно решать задачу лобовым способом: строить соответствующий по числу фильтров массив DDC (Digital Down Converter). Такой подход хорош для ПЛИС, где возможна экономия за счет уменьшения разрядности вычислителей первых ступеней. Также ПЛИС это наиболее энергетически-эффективный способ реализации. Но трудозатраты при этом способе наиболее высоки.


При выполнении гребенки фильтров на популярных нынче GPU появляется возможность реализации более хитрого алгоритма: полифазной гребенки фильтров на основе БПФ, который на CUDA доступен из библиотеки. В заграничной литературе алгоритм называется Polyphase or WOLA (Weight, Overlap and Add) FFT Filterbank. Лень к рисованию не дает мне возможности выполнить наглядное объяснение самостоятельно. В Сети есть много материалов по теме, особенно наглядный график выполнен здесь на странице 11 (огромное спасибо уважаемым авторам), вот он:


image


Я попробую пояснить схему обработки своими словами. Слабонервных прошу не читать.

Я попробую пояснить схему обработки своими словами в рамках моих методических возможностей. БПФ есть свертка входного сигнала со всем спектром комплексных ортогональных гармоник, укладывающихся на интервале импульсной характеристики. Импульсная характеристика фильтра, на которую умножается сигнал перед входом БПФ, модулируется этим спектров гармоник. Другими словами, огибающая импульсных характеристик результирующих фильтров гребенки выносится за скобки. Далее, спектр гармоник прореживается в некоторое число раз, ввиду расширения полосы пропускания фильтра относительно фильтра в прямоугольным окном. На рисунке мы видим прореживание на четыре. Другими словами, после расширения полосы окном Кайзера (с одновременным увеличением затухания в полосе задержания), нам уже нужны не все фильтры а только четвертая их часть. Остальные избыточны, их частотные характеристики перекрываются. Из четырех точек БПФ, идущих подряд, выбираем только нулевую, вычисление которой есть суммирование четырех
входных точек, взятых через время, равное четверти длительности исходного БПФ.


Железо выберем то, которое есть под рукой. Это плата ввода компании "Инструментальные Системы" FMC126P. О ней я уже писал в одной предыдущей статье. В разъем FMC платы вставлен субмодуль той же компании с трансивером AD9371 с полосой 100 МГц. Весь поток с трансивера может быть непрерывно передан в компьютер для обработки.


Выберем видео-карту с GPU GTX 1050. (Соврал, это она нас выбрала: это все что было под рукой, была выдрана из вычислителя для расчета антенн, но тем удивительней было увидеть рабочую гребенку). Перейдем к программной части.


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


Вот ядро, которое выполняет перемножение сигнала на окно и сложение, и обертка для его вызова:


__global__ void cuComplexMultiplyWindowKernel(const cuComplex *data, const float *window, size_t windowSize, cuComplex *result) {    __shared__ cuComplex multiplicationResult[480];    multiplicationResult[threadIdx.x] = cuComplexMultiplyFloat(data[threadIdx.x + windowSize / 4 * blockIdx.x], window[threadIdx.x]);    __syncthreads();    cuComplex sum;    sum.x = sum.y = 0;    if (threadIdx.x < windowSize / 4) {        for(int i = 0; i < 4; i++) {            sum = cuComplexAdd(sum, multiplicationResult[threadIdx.x + i * windowSize / 4]);        }        result[threadIdx.x + windowSize / 4 * blockIdx.x] = sum;    }}cudaError_t cuComplexMultiplyWindow(const cuComplex *data, const float *window, size_t windowSize, cuComplex *result, size_t dataSize, cudaStream_t stream) {    size_t windowStep = windowSize / 4;    cuComplexMultiplyWindowKernel<<<dataSize / windowStep - 3, windowSize, 1024, stream>>>(data, window, windowSize, result);    return cudaGetLastError();}

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


Гребенки были проверены на выходном спектре каналов в реальном времени. На вход AD9371 подавался сигнал генератора сигналов 2450 МГц, селективность фильтров соответствовала расчетной.


image


В планах: адаптация софта на плату XRTX и реализация поиска пакетов, если это кому-нибудь окажется нужно или будет свободное время.


Всю работу по софту выполнил gaudima, слава ему!

Подробнее..

Quantization Aware Training. Или как правильно использовать fp16 inference в TensorRT

21.05.2021 12:08:14 | Автор: admin

Low-precision inference в TensorRT сегодня - мастхэв, бест практис и прочие иностранные. Сконвертить из TensorFlow легко, запустить легко, использовать fp16 легко. Да и КПД выше, чем у pruning или distillation. На первый взгляд всё работает идеально. Но на самом деле всё ли так гладко? Рассказываем, как мы в TrafficData споткнулись об fp16, встали и написали статью.

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

Что за зверь ваш low-precision?

float16

И так, low-precision inference - запуск нейронных сетей в типе пониженной точности. Но зачем это нужно? По умолчанию все фреймворки учат и сохраняют модели в типе float32. Оказывается, что количество знаков во fp32 - часто избыточно. Ну а зачем нам эти сотни знаков после запятой? Можно просто скастовать fp32 веса во fp16, чтобы получить ускорение прямого прогона и уменьшение используемой памяти в 2 раза. При этом сохранив исходную точность модели. Единственное условие - наличие тензорных ядер в вашем GPU.

int8 и прочее

Кроме fp16 с простым кастованием есть много идей по более оптимальному использованию бит в 16-битном значении. Просто чтобы напомнить:

Но этого мало. Использование нейронных сетей в высоконагруженных системах и мобильных платформах заставляет еще сильнее ужимать сети и ускорять инференс. Добро пожаловать в мир int8 и int4. Да, в них квантуют. Да, в int8 всего 256 значений. Да, это работает. Со своими сложностями, конечно - здесь уже просто так не кастанёшь, как в случае с fp16. Нужно внимательно изучать распределения значений в слоях, чтобы эффективно использовать предоставленный небольшой диапазон значений.

Объясню, почему мы не смотрим на 8/4 битные квантизации. Дело в том, что здесь не обойтись без потери точности. Например, тут говорят как оптимально юзать int4 и радуются, что потеряли не 15%, а 8% точности. Или вот красноречивая табличка от Nvidia о западении точности при использовании int8:

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

TensorRT

Если у вас мобильные решения или просто инференс на CPU, то попробуйте TensorFlow Lite. Но в основном, говоря про low-precision inference в проде, сегодня имеют ввиду TensorRT - кроссплатформенный SDK для супер-быстрой работы на GPU от Nvidia. TensorRT легко превращает ваши модели в оптимизированные Engines. Сконвертить можно из любого нейросетевого фреймворка через ONNX. Engine - очень важная сущность в TensorRT. При билде происходит оптимизация под текущий GPU - на других GPU engine либо не запустится, либо будет работать неоптимально. Короче говоря, есть ряд параметров, которые нужно знать или задать заранее:

  • GPU. На чём собрали Engine, на том пусть он и работает. Но допустим общий билд для карточек одного семейства - Turing или Ampere. Например, мы билдили Engine для RTX 2060 и он замечательно работает на RTX 2080 Super. Создание отдельного Engine для RTX 2080 Super существенного ускорения не создает.

  • BatchSize. Нужно задать максимальный - для него и будет соптимизирован Engine. В рантайме можно подавать батчи размером меньше максимального, но это будет неоптимально.

  • InputSize. Мы работаем с изображениями. И размер входного изображения иногда может меняться во время рантайма. Но TRT требует его задавать жестко, что логично. Да, есть возможность задать минимальный и максимальный размеры, а TRT создаст несколько профилей оптимизации. Но всё же это не так гибко, как в TensorFlow, а иногда нужно.

  • Precision. Собственно мы можем задать fp32/fp16/int8. Первые два различаются лишь выбором флага. С int8 я мало экспериментировал. Но судя по документации, отличие лишь в необходимости калибровочного датасета - набора картинок, на основании которого TRT определит распределения значений на разных слоях.

Ну и под конец еще добавлю, что в рантайме эти движки отжирают лишь необходимый минимум GPU RAM и замечательно работают параллельно (если правильно работать с TensorRT Context в вашем коде рантайма).

Контекст задачи

Окей, чтобы было понятнее, какие проблемы возникли, пара слов о нашем продукте. Мы пилим TrafficData - ПО для оценки трафика c камер и дронов, в рилтайме и постфактум, в дождь и снег. Нам важно, чтобы ночью детектилось вот так:

И не хуже.

На opentalks.ai2020 мы рассказывали, как, используя Pruning и физичность данных, ускорили обработку в 4 раза и не потеряли в точности. Статью про Pruning я уже выкладывал. Но сегодня давайте поговорим конкретно про low-precision inference.

Как мы запустились и потеряли нежные фичи

Скачивая либы TensorRT, бонусом вы получаете набор примеров с кодом для самых разных архитектур и ситуаций. Для билда движков мы использовали пример SampleUffSSD (UFF - универсальный формат описания сети, через который мы конвертили наши .pb), cлегка его закастомив под входной тензор от YOLO. И хотя TensorRT очень много обновляется и всё больше новых интересных слоев поддерживает, тогда мы запускались на версии, где не было реализации ResizeBilinear операции для Upsample слоя. И мы накостылили Conv2DTranspose вместо него, чтобы не писать кастомный слой. Первая сконверченная модель была радостью, как и её скорость работы.

Даже если перейти с fp32 из TF в fp32 TRT, то уже получается неслабое ускорение - на 15-20%. В конце концов TRT использует и много других оптимизаций, например горизонтальные, вертикальные и любые другие LayerFusion.

Для инференса мы закастомили пример trtExec, обернув его для использования в .NET коде. На вход - байты изображения, на выходе - нераспарсенные байты выхода YOLO. Здесь аккуратно работайте с CudaStream и ExecutionContext. Тогда ни память не утечет, ни потоки обработки не закорраптятся.

И так, мы реализовали TensorRT fp16 inference. Сбилдили движки для разных карточек. Прогнали основные тесты - колебания точности в пределах погрешности. И всё замечательно работало несколько месяцев. А дальше - история.
10:00. Звонок клиента:
- У нас тут на одном ролике TrafficData плохо работает - машинки двоятся.
- Окей, скиньте ролик разберемся.
Смотрим ролик - да, проблема есть. Ролик с тенями и на нём тени отмечаются, как второе авто.

13:00. Добрали изображения в основной датасет. Поставили доучиться с низким LR.

16:00. Тестим на версии с инференсом в TensorFlow - всё замечательно. Билдим новый Engine. Тестим на версии с инференсом в TensorRT - опять машины двоятся:

17:00. Идём домой.

Следующее утро началось с мема:

Стало очевидно, что проблема в TensorRT, а конкретно - в преобразовании весов во fp16. Мы проверили еще несколько других роликов со сложными условиями и увидели, что после преобразования во fp16 проблемы появились и в других местах. Стали появляться пропуски детекции на ночных видео, некоторые билборды стали определяться как авто. Короче вот так мы потеряли нежные, но важные фичи, про которые оригинальная сеть во fp32 знала, а вот во fp16 успешно забыла. Что делать?

Quntization Aware Training. Учи на том, на чем будет работать

Подсознательно мы сразу понимали, что если мы обучаем на fp32, а потом инференсим на fp16, то выйдет неприятная вещь. Вот эти жалкие циферки далеко после запятой потеряны и так влияют. Тогда зачем мы их учили на каждом батче? Идея Quntization Aware Training крайне проста - учи и помни о том типе, в котором будешь инференсить. Т.е. в типе fp16 должны быть все веса сверток, активаций и градиентов. Не удивляйтесь, если первые запуски в TensorFlow окажутся с NaN-лоссом. Просто внимательно инспектируйте происходящее. Мы потратили пару недель, переписали всё обучение на fp16 и проблема была решена.

Как в Tensorflow 2.0?

Тут небольшое отступление о том, как мы были рады обновлению TF2.0. Работая под TF1.15 мы кусали локти, заставляя запуститься обучение во fp16, переписывая слои. Но это заработало. А потом пришел TF2.0 - используешь tf.train.experimental.enable_mixed_precision_graph_rewrite над оптимизатором и всё заводится, как моя Lada Granta. Но всё же стоит обратить внимание на whitelist - не все ноды по умолчанию будут работать во fp16. Часть стоит поправить руками. Ну и дополнительный бонус - огромная экономия памяти, которой не получалось в TF1.15. Батч-сайз для нашей кастомной YOLOv4.5 увеличился в 2 раза - с 4 до 8. Больше батч - лучше градиенты.

Выводы

Fp16 inference - это здорово. Только не стоит забывать про Quntization Aware Training, если вы хотите сохранить точность оригинальной модели. Это позволило нам сделать еще шаг в сторону оптимизации наших и клиентских мощностей:

Что особенно важно в годы дефицита чипов и дорогих GPU. Я всё же за использование GPU в тех местах, где они приносят пользу людям, автоматизируя что-то. А не там, где они приносят прибыль, делая деньги из подогретого воздуха.

А вообще вся тематика ускорения инференса сетей сегодня - очень интересное поле для экспериментов. Хочется попробовать десятки новых способов Pruning, Distillation или квантования в int4, но всех Баксов Банни не догонишь. Пробуйте новое, но не забывайте отдыхать.

Подробнее..

Перенос молекулярной динамики на CUDA. Часть II Суммирование по Эвальду

07.07.2020 20:09:59 | Автор: admin
В предыдущей статье мы обсудили основу метода молекулярной динамики, в том числе вычисление энергии и сил взаимодействия между частицами с заданными парными потенциалами. А что, если частицы обладают некоторым электрическим зарядом? Например, в том случае, если мы моделируем кристалл поваренной соли, состоящий из ионов Na+ и Cl-. Или водный раствор, содержащий те или иные ионы. В этом случае, кроме парных потенциалов типа Леннарда-Джонса между ионами действуют силы электростатического взаимодействия, т.е. закон Кулона. Энергия такого взаимодействия для пары частиц i-j равна:

$E=C\frac{q_iq_j}{r_{ij}},$


где q заряд частицы, rij расстояние между частицами, С некоторая постоянная, зависящая от выбора единиц измерения. В системе СИ это $\frac{1}{4\pi\epsilon_0}$, в СГС 1, в моей программе (где энергия выражена в электронвольтах, расстояние в ангстремах, а заряд в элементарных зарядах) C примерно равно 14.3996.
image
Ну и что, скажете вы? Просто добавим соответствующее слагаемое в парный потенциал и готово. Однако, чаще всего в МД моделировании используют периодические граничные условия, т.е. моделируемая система со всех сторон окружена бесконечным количеством её виртуальных копий. В этом случае каждый виртуальный образ нашей системы будет взаимодействовать со всеми заряженными частицами внутри системы по закону Кулона. А поскольку Кулоновское взаимодействие убывает с расстоянием очень слабо (как 1/r), то отмахнуться от него так просто нельзя, сказав, что с такого-то расстояния мы его не вычисляем. Ряд вида 1/x расходится, т.е. его сумма, в принципе, может расти до бесконечности. И что же теперь, миску супа не солить? Убьёт электричеством?

можно не только солить суп, но и посчитать энергию Кулоновского взаимодействия в периодических граничных условиях. Такой метод был предложен Эвальдом ещё в 1921 году для расчета энергии ионного кристалла (можно также посмотреть в википедии). Суть метода заключатся в экранировании точечных зарядов и последующим вычетом функции экранирования. При этом часть электростатического взаимодействия сводится к короткойдействующему и его можно просто обрезать стандартным способом. Оставшаяся дальнодействующая часть эффективно суммируется в пространстве Фурье. Опуская вывод, который можно посмотреть в статье Блинова или в той же книге Френкеля и Смита сразу запишу решение, называемое суммой Эвальда:

$E_{real} = С\sum\limits^{N-1}_i{\sum\limits^N_{j=i+1}{ \frac{q_iq_j}{r_{ij}}\operatorname{erfc}(\alpha r_{ij}) }}, $


$E_{rec} = С\frac{2\pi}{V}\sum_{\vec{k}\ne0}(\exp(-k^2/4\alpha^2)/k^2 \sum_jq_i\exp(i\vec{k}\vec{r}_j)),$


$E_{const} = -\frac{C}{V\alpha} \sum_iq_i^2, $


где параметр, регулирующий соотношение вычислений в прямом и обратном пространствах, k вектора в обратном пространстве по которым идёт суммирование, V объём системы (в прямом пространстве). Первая часть (Ereal) является короткодействующей и вычисляется в том же цикле, что и другие парные потенциалы, смотри функцию real_ewald в предыдущей статье. Последний вклад (Eсonst) является поправкой на самовзаимодействие и часто называется постоянной частью, поскольку не зависит от координат частиц. Её вычисление тривиально, поэтому мы остановимся только на второй части Эвальдовой суммы (Erec), суммировании в обратном пространстве. Естественно, во времена вывода Эвальда молекулярной динамики не было, кто впервые использовал этот метод в МД найти мне не удалось. Сейчас любая книга по МД содержит его изложение как некий золотой стандарт. К книге Аллена даже прилагается пример кода на фортране. К счастью, у меня остался код, написанный когда-то на С для последовательной версии, осталось только его распараллелить (я позволил себе опустить некоторые объявления переменных и другие несущественные детали):
void ewald_rec(){    int mmin = 0;    int nmin = 1;    // массивы где хранятся iexp(x[i] * kx[l]),    double** elc;    double** els;    //... iexp(y[i] * ky[m]) и    double** emc;    double** ems;    //... iexp(z[i] * kz[n]),    double** enc;    double** ens;    // временные массивы для произведений iexp(x*kx)*iexp(y*ky)    double* lmc;    double* lms;    // и для q[i] * iexp(x*kx)*iexp(y*ky)*iexp(z*kz)    double* ckc;    double* cks;    // ПРЕДВАРИТЕЛЬНОЕ ЗАПОЛНЕНИЕ МАССИВОВ    eng = 0.0;    for (i = 0; i < Nat; i++)   // цикл по атомам    {        // emc/s[i][0] и enc/s[i][0] уже заполнены на этапе инициализации        // в массив elc/s нужно обновить, см. далее        elc[i][0] = 1.0;        els[i][0] = 0.0;        // iexp(kr)        sincos(twopi * xs[i] * ra, els[i][1], elc[i][1]);        sincos(twopi * ys[i] * rb, ems[i][1], emc[i][1]);        sincos(twopi * zs[i] * rc, ens[i][1], enc[i][1]);    }    // заполняем следующие элементы массива emc/s[i][l] = iexp(y[i]*ky[l]) итеративно, используя комплексное умножение    for (l = 2; l < ky; l++)        for (i = 0; i < Nat; i++)        {            emc[i][l] = emc[i][l - 1] * emc[i][1] - ems[i][l - 1] * ems[i][1];            ems[i][l] = ems[i][l - 1] * emc[i][1] + emc[i][l - 1] * ems[i][1];        }    // заполняем следующие элементы массива enc/s[i][l] = iexp(z[i]*kz[l]) итеративно, используя комплексное умножение    for (l = 2; l < kz; l++)        for (i = 0; i < Nat; i++)        {            enc[i][l] = enc[i][l - 1] * enc[i][1] - ens[i][l - 1] * ens[i][1];            ens[i][l] = ens[i][l - 1] * enc[i][1] + enc[i][l - 1] * ens[i][1];        }    // ГЛАВНЙ ЦИКЛ ПО ВСЕМ K-ВЕКТОРАМ:    for (l = 0; l < kx; l++)    {        rkx = l * twopi * ra;         // записываем exp(ikx[l]) в ikx[0] для сохранения памяти        if (l == 1)            for (i = 0; i < Nat; i++)            {                elc[i][0] = elc[i][1];                els[i][0] = els[i][1];            }        else if (l > 1)            for (i = 0; i < Nat; i++)            {                // iexp(kx[0]) = iexp(kx[0]) * iexp(kx[1])                x = elc[i][0];                elc[i][0] = x * elc[i][1] - els[i][0] * els[i][1];                els[i][0] = els[i][0] * elc[i][1] + x * els[i][1];            }        for (m = mmin; m < ky; m++)        {            rky = m * twopi * rb;            // заполняем временный массив произведением iexp(kx*x[i]) * iexp(ky*y[i])            if (m >= 0)                for (i = 0; i < Nat; i++)                {                    lmc[i] = elc[i][0] * emc[i][m] - els[i][0] * ems[i][m];                    lms[i] = els[i][0] * emc[i][m] + ems[i][m] * elc[i][0];                }            else // для отрицательных значений m используем комплексное сопряжение:                for (i = 0; i < Nat; i++)                {                    lmc[i] = elc[i][0] * emc[i][-m] + els[i][0] * ems[i][-m];                    lms[i] = els[i][0] * emc[i][-m] - ems[i][-m] * elc[i][0];                }            for (n = nmin; n < kz; n++)            {                rkz = n * twopi * rc;                rk2 = rkx * rkx + rky * rky + rkz * rkz;                if (rk2 < rkcut2) // используем радиус обрезания                {                    // вычисляем сумму (q[i]*iexp(kr[k]*r[i])) - зарядовую плотность                    sumC = 0; sumS = 0;                    if (n >= 0)                        for (i = 0; i < Nat; i++)                        {                            //считываем заряд частицы                            ch = charges[types[i]].charge;                            ckc[i] = ch * (lmc[i] * enc[i][n] - lms[i] * ens[i][n]);                            cks[i] = ch * (lms[i] * enc[i][n] + lmc[i] * ens[i][n]);                            sumC += ckc[i];                            sumS += cks[i];                        }                    else // для отрицательных индексов используем комплексное сопряжение:                        for (i = 0; i < Nat; i++)                        {                            //считываем заряд частицы                            ch = charges[types[i]].charge;                            ckc[i] = ch * (lmc[i] * enc[i][-n] + lms[i] * ens[i][-n]);                            cks[i] = ch * (lms[i] * enc[i][-n] - lmc[i] * ens[i][-n]);                            sumC += ckc[i];                            sumS += cks[i];                        }                    //наконец вычисляем энергию и силы                    akk = exp(rk2 * elec->mr4a2) / rk2;                    eng += akk * (sumC * sumC + sumS * sumS);                    for (i = 0; i < Nat; i++)                    {                        x = akk * (cks[i] * sumC - ckc[i] * sumS) * C * twopi * 2 * rvol;                        fxs[i] += rkx * x;                        fys[i] += rky * x;                        fzs[i] += rkz * x;                    }                }            } // end n-loop (over kz-vectors)            nmin = 1 - kz;        } // end m-loop (over ky-vectors)        mmin = 1 - ky;    }  // end l-loop (over kx-vectors)   engElec2 = eng * С * twopi * rvol;}

Пара пояснений к коду: функция считает комплексную экспоненту (в комментариях к коду она обозначена iexp, чтобы убрать мнимую единицу из скобок) от векторного произведения k-вектора на радиус-вектор частицы для всех k-векторов и для всех частиц. Эта экспонента домножается на заряд частицы. Далее вычисляется сумма таких произведений по всем частицам (внутренняя сумма в формуле для Erec), у Френкеля она называется зарядовой плотностью, а у Блинова структурным фактором. Ну а далее, на основании этих структурных факторов вычисляется энергия и силы, действующие на частицы. Компоненты k-векторов (2*l/a, 2*m/b, 2*n/c) характеризуются тройкой целых чисел l, m и n, которые и пробегаются в циклах до заданных пользователем пределов. Параметры a, b и c это размеры моделируемой системы в измерениях x, y и z соответственно (вывод верен для системы с геометрией прямоугольного параллелепипеда). В коде 1/a, 1/b и 1/с соответствуют переменным ra, rb и rc. Массивы под каждую величину представлены в двух экземплярах: под действительную и мнимую части. Каждый следующий k-вектор в одном измерении получается итеративно из предыдущего путем комплексного умножения предыдущего на единичный, чтобы каждый раз не считать синус с косинусом. Массивы emc/s и enc/s заполняются для всех m и n, соответственно, а массив elc/s значение для каждого l>1 помещает в нулевой индекс по l в целях экономии памяти.
В целях распараллеливания выгодно вывернуть порядок циклов так, чтобы внешний цикл пробегался по частицам. И тут мы видим проблему распараллелить эту функцию можно только до вычисления суммы по всем частицам (зарядовой плотности). Дальнейшие вычисления опираются на эту сумму, а она будет рассчитана только когда все потоки закончат работу, поэтому придётся разбить эту функцию на две. Первая вычисляет считает зарядовую плотность, а вторая энергию и силы. Замечу, что во второй функции снова потребуется величина qiiexp(kr) для каждой частицы и для каждого k-вектора, вычисленная на предыдущем этапе. И тут есть два подхода: либо пересчитать её заново, либо запомнить. Первый вариант требует больше времени, второй больше памяти (количество частиц * количество k-векторов * sizeof(float2)). Я остановился на втором варианте:
__global__ void recip_ewald(int atPerBlock, int atPerThread, cudaMD* md)// calculate reciprocal part of Ewald summ// the first part : summ (qiexp(kr)) evaluation{    int i;      // for atom loop    int ik;     // index of k-vector    int l, m, n;    int mmin = 0;    int nmin = 1;    float tmp, ch;    float rkx, rky, rkz, rk2;   // component of rk-vectors    int nkx = md->nk.x;    int nky = md->nk.y;    int nkz = md->nk.z;        // arrays for keeping iexp(k*r) Re and Im part    float2 el[2];    float2 em[NKVEC_MX];    float2 en[NKVEC_MX];    float2 sums[NTOTKVEC];          // summ (q iexp (k*r)) for each k    extern __shared__ float2 sh_sums[];     // the same in shared memory    float2 lm;     // temp var for keeping el*em    float2 ck;     // temp var for keeping q * el * em * en (q iexp (kr))    // invert length of box cell    float ra = md->revLeng.x;    float rb = md->revLeng.y;    float rc = md->revLeng.z;    if (threadIdx.x == 0)        for (i = 0; i < md->nKvec; i++)            sh_sums[i] = make_float2(0.0f, 0.0f);    __syncthreads();    for (i = 0; i < md->nKvec; i++)        sums[i] = make_float2(0.0f, 0.0f);    int id0 = blockIdx.x * atPerBlock + threadIdx.x * atPerThread;    int N = min(id0 + atPerThread, md->nAt);    ik = 0;    for (i = id0; i < N; i++)    {        // save charge        ch = md->specs[md->types[i]].charge;        el[0] = make_float2(1.0f, 0.0f);    // .x - real part (or cos) .y - imagine part (or sin)                em[0] = make_float2(1.0f, 0.0f);        en[0] = make_float2(1.0f, 0.0f);        // iexp (ikr)        sincos(d_2pi * md->xyz[i].x * ra, &(el[1].y), &(el[1].x));        sincos(d_2pi * md->xyz[i].y * rb, &(em[1].y), &(em[1].x));        sincos(d_2pi * md->xyz[i].z * rc, &(en[1].y), &(en[1].x));        // fil exp(iky) array by complex multiplication        for (l = 2; l < nky; l++)        {             em[l].x = em[l - 1].x * em[1].x - em[l - 1].y * em[1].y;             em[l].y = em[l - 1].y * em[1].x + em[l - 1].x * em[1].y;        }        // fil exp(ikz) array by complex multiplication        for (l = 2; l < nkz; l++)        {             en[l].x = en[l - 1].x * en[1].x - en[l - 1].y * en[1].y;             en[l].y = en[l - 1].y * en[1].x + en[l - 1].x * en[1].y;        }        // MAIN LOOP OVER K-VECTORS:        for (l = 0; l < nkx; l++)        {            rkx = l * d_2pi * ra;                         // move exp(ikx[l]) to ikx[0] for memory saving (ikx[i>1] are not used)            if (l == 1)                el[0] = el[1];            else if (l > 1)                {                    // exp(ikx[0]) = exp(ikx[0]) * exp(ikx[1])                    tmp = el[0].x;                    el[0].x = tmp * el[1].x - el[0].y * el[1].y;                    el[0].y = el[0].y * el[1].x + tmp * el[1].y;                }            //ky - loop:            for (m = mmin; m < nky; m++)            {                rky = m * d_2pi * rb;                //set temporary variable lm = e^ikx * e^iky                if (m >= 0)                {                        lm.x = el[0].x * em[m].x - el[0].y * em[m].y;                         lm.y = el[0].y * em[m].x + em[m].y * el[0].x;                }                else // for negative ky give complex adjustment to positive ky:                {                        lm.x = el[0].x * em[-m].x + el[0].y * em[-m].y;                        lm.y = el[0].y * em[-m].x - em[-m].x * el[0].x;                }                //kz - loop:                for (n = nmin; n < nkz; n++)                {                    rkz = n * d_2pi * rc;                    rk2 = rkx * rkx + rky * rky + rkz * rkz;                    if (rk2 < md->rKcut2) // cutoff                    {                        // calculate summ[q iexp(kr)]   (local part)                        if (n >= 0)                         {                                ck.x = ch * (lm.x * en[n].x - lm.y * en[n].y);                                ck.y = ch * (lm.y * en[n].x + lm.x * en[n].y);                         }                        else // for negative kz give complex adjustment to positive kz:                         {                                ck.x = ch * (lm.x * en[-n].x + lm.y * en[-n].y);                                ck.y = ch * (lm.y * en[-n].x - lm.x * en[-n].y);                        }                        sums[ik].x += ck.x;                        sums[ik].y += ck.y;                                                // save qiexp(kr) for each k for each atom:                        md->qiexp[i][ik] = ck;                        ik++;                    }                } // end n-loop (over kz-vectors)                nmin = 1 - nkz;            } // end m-loop (over ky-vectors)            mmin = 1 - nky;        }  // end l-loop (over kx-vectors)    } // end loop by atoms    // save sum into shared memory    for (i = 0; i < md->nKvec; i++)    {        atomicAdd(&(sh_sums[i].x), sums[i].x);        atomicAdd(&(sh_sums[i].y), sums[i].y);    }    __syncthreads();    //...and to global    int step = ceil((double)md->nKvec / (double)blockDim.x);    id0 = threadIdx.x * step;    N = min(id0 + step, md->nKvec);    for (i = id0; i < N; i++)    {        atomicAdd(&(md->qDens[i].x), sh_sums[i].x);        atomicAdd(&(md->qDens[i].y), sh_sums[i].y);    }}// end 'ewald_rec' function__global__ void ewald_force(int atPerBlock, int atPerThread, cudaMD* md)// calculate reciprocal part of Ewald summ// the second part : enegy and forces{    int i;      // for atom loop    int ik;     // index of k-vector    float tmp;    // accumulator for force components    float3 force;    // constant factors for energy and force    float eScale = md->ewEscale;    float fScale = md->ewFscale;    int id0 = blockIdx.x * atPerBlock + threadIdx.x * atPerThread;    int N = min(id0 + atPerThread, md->nAt);    for (i = id0; i < N; i++)    {        force = make_float3(0.0f, 0.0f, 0.0f);        // summ by k-vectors        for (ik = 0; ik < md->nKvec; ik++)        {            tmp = fScale * md->exprk2[ik] * (md->qiexp[i][ik].y * md->qDens[ik].x - md->qiexp[i][ik].x * md->qDens[ik].y);            force.x += tmp * md->rk[ik].x;            force.y += tmp * md->rk[ik].y;            force.z += tmp * md->rk[ik].z;        }        md->frs[i].x += force.x;        md->frs[i].y += force.y;        md->frs[i].z += force.z;    } // end loop by atoms    // one block calculate energy    if (blockIdx.x == 0)        if (threadIdx.x == 0)        {            for (ik = 0; ik < md->nKvec; ik++)                md->engCoul2 += eScale * md->exprk2[ik] * (md->qDens[ik].x * md->qDens[ik].x + md->qDens[ik].y * md->qDens[ik].y);        }}// end 'ewald_force' function

Надеюсь, вы мне простите, что я оставил комментарии на английском, код практически повторяет последовательную версию. Код даже стал читабельнее за счет того, что массивы потеряли одно измерение: elc/s[i][l], emc/s[i][m] и enc/s[i][n] превратились в одномерные el, em и en, массивы lmc/s и ckc/s в переменные lm и ck (пропала мерность по частицам, поскольку отпала необходимость хранить это для каждой частицы, промежуточный результат накапливается в shared memory). К сожалению, тут же возникла и проблема: массивы em и en пришлось задать статическими, чтобы не использовать глобальную память и не выделять память динамически каждый раз. Количество элементов в них определяется директивой NKVEC_MX (максимальное количество k-векторов по одному измерению) на этапе компиляции, а runtime используются только первые nky/z элементов. Кроме того, появился сквозной индекс по всем k-векторам и аналогичная директива, ограничивающая общее количество этих векторов NTOTKVEC. Проблема возникнет, если пользователю понадобится больше k-векторов, чем определено директивами. Для вычисления энергии предусмотрен блок с нулевым индексом, поскольку неважно какой именно блок выполнит этот расчет и в каком порядке. Тут может быть надо было использовать готовые функции FFT, раз уж метод основан на нём, но я так и не сообразил, как это сделать.
Ну а теперь попробуем что-нибудь посчитать, да тот же содиум хлорайд. Возьмём 2 тысячи ионов натрия и столько же хлора. Заряды зададим целыми, а парные потенциалы возьмём, например, из этой работы. Стартовую конфигурацию зададим случайно и слегка перемешаем её, рисунок 2а. Объём системы выберем так, чтобы он соответствовал плотности поваренной соли при комнатной температуре (2,165 г/см3). Запустим все это на небольшое время (10000 шагов по 5 фемтосекунд) с наивным учетом электростатики по закону Кулона и используя суммирование по Эвальду. Результирующие конфигурации приведены на рисунках 2б и 2в, соответственно. Вроде бы в случае с Эвальдом система чуть больше упорядочилась чем без него. Важно также, что флуктуации полной энергии с применением суммирования существенно уменьшились.

Рисунок 2. Начальная конфигурация системы NaCl (a) и после 10000 шагов интегрирования: наивным способом (б) и со схемой Эвальда (в).

Вместо заключения


Замечу, что структура, получаемая на рисунке, не соответствует кристаллической решетки NaCl, а скорее решетке ZnS, но это уже претензия к парным потенциалам. Учет же электростатики очень важен для молекулярно-динамического моделирования. Считается, что именно электростатическое взаимодействие ответственно за образование кристаллических решёток, поскольку действует на больших расстояниях. Правда с этой позиции сложно объяснить как при охлаждении кристаллизуются такие вещества как аргон.
Кроме упомянутого метода Эвальда, есть ещё и другие способы учета электростатики, смотрите, например, этот обзор.
Подробнее..

Перенос молекулярной динамики на CUDA. Часть III Внутримолекулярное взаимодействие

21.08.2020 16:19:43 | Автор: admin
До этого мы рассматривали молекулярную динамику, где законы взаимодействия между частицами зависели исключительно от типа частиц или от их заряда. Для веществ молекулярной природы взаимодействие между частицами (атомами) сильно зависит от того, принадлежат ли атомы одной молекуле или нет (точнее, связаны ли они химической связью). Например, вода:
image
очевидно, что водород с кислородом внутри одной молекулы взаимодействуют совсем по-другому, нежели тот же кислород с водородом соседней молекулы. Таким образом, разделяют ВНУТРИмолекулярное (intramolecular) и МЕЖмолекулярное (intermolecular) взаимодействие. Межмолекулярное взаимодействие можно задать короткодействующими и Кулоновскими парными потенциалами, о которых речь шла в предыдущих статьях. Здесь же сконцентрируемся на внутримолекулярном.

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

$U=\frac12k(r-r_0)^2,$


где r расстояние между частицами, k константа жесткости связи, а r0 равновесная длина связи; и потенциал Морзе:

$U=D(1-\exp(-\alpha(r-r_0)))^2,$


где D глубина потенциальной ямы, параметр характеризует ширину потенциальной ямы.
Следующий тип внутримолекулярных взаимодействий это валентные углы. Обратимся снова к заглавной картинке. Почему молекула изображена уголком, ведь силы электростатики должны были обеспечить максимальное расстояние между ионами водорода, которое соответствует углу H-O-H равному 180? Дело в том, что на рисунке нарисовано не все. Из школьного курса химии, можно вспомнить, что у кислорода есть ещё 2 неподелённые электронные пары, взаимодействие с которыми и искажает угол:
image
В классической молекулярной динамике обычно не вводят такие объекты как электроны или электронные облака, поэтому для имитации правильных углов используют потенциал валентного угла, т.е. функциональную зависимость потенциальной энергии от координат 3х частиц. Одним из наиболее удобных таких потенциалов является гармонический косинус:

$U=\frac12k(\theta-\theta_0)^2,$


где угол образованной тройкой частиц, k константа жесткости, а 0 равновесный угол.
Существуют внутримолекулярные потенциалы и более высокого порядка, например, торсионные углы, но они ещё более искусственные, чем валентные углы.
Добавить взаимодействия между частицами с заранее известными индексами дело тривиальное. Для связей храним массив, содержащий индексы связанных частиц и тип взаимодействия. Каждому потоку выдаем свой диапазон связей на обработку и готово. Аналогично с валентными углами. Поэтому мы сразу себе усложним жизнь задачу: добавим возможность создавать/удалять химические связи и валентные углы runtime. Это сразу выводит нас из плоскости классической молекулярной динамики и открывает новый горизонт возможностей. Иначе можно было просто скачать что-нибудь из существующих пакетов, например LAMMPS, DL_POLY или GROMACS , тем более что они распространяются бесплатно.
Ну а теперь немного кода. Добавим соответствующие поля в основную структуру:
    //bonds:    int nBond;      // number of bonds    int mxBond;          // maximal number of bonds    int4* bonds;    // array of bonds     int* nbonds;    // count of bond for a given atom    int* neighToBind;   // a neighbor of a given atom for binding    int* canBind;       // flags that atom[iat] can be bind    int* r2Min;         // distances for the nearest neighbor (used for binding)    int* parents;       // indexes of one of the atom bonded with a given    cudaBond* bondTypes;     int** def_bonds;    // array[nSpec][nSpec] of default bond types    int** bindBonds;    // array[nSpec][nSpec] bond types created by binding    float** bindR2;        // square of binding distance [nSpec][nSpec]    //angles:    int nAngle;    // number of angles    int mxAngle;    int4* angles;   // array of angles      int* nangles;        // number of angles for given atom    int* oldTypes;          cudaAngle* angleTypes;     int* specAngles;    // [nSp] angle type formed by given species


Количество связей и углов переменное, но практически всегда можно оценить максимально возможное и выделить память сразу под максимум, чтобы не перевыделять память, поля nBond и mxBond, соответственно, означают текущее число связей и максимальное. Массив bonds будет содержать индексы связываемых атомов, тип связи и время создания связи (если нас вдруг заинтересует такая статистика, как среднее время жизни связи). Массив angles будет хранить индексы тройки атомов, образующих валентный угол, и тип валентного угла. Массивы bondTypes и angleTypes будут содержать характеристики возможных потенциалов валентных связей и углов. Вот их структуры:
struct cudaBond{    int type;  // potential type    int spec1, spec2; // type of atoms that connected by this bond type    int new_type[2];      // bond type after mutation    int new_spec1[2], new_spec2[2];    int mxEx, mnEx;     // flags: maximum or minimum of bond length exists    float p0, p1, p2, p3, p4;    // potential parameters    float r2min, r2max;          // square of minimal and maximal bond length    float (*force_eng)(float r2, float r, float &eng, cudaBond *bond); // return energy     int count;      // quantity of such bonds    float rSumm;        // summ of lentghs (for mean length calculation)    int rCount;          // number of measured lengths (for mean length calculation)    int ltSumm, ltCount;    // for calculation of lifetime};struct cudaAngle{    int type; // potential type    float p0, p1, p2;    // potential parameters    void (*force_eng)(int4* angle, cudaAngle* type, cudaMD* md, float& eng);};

Поле type определяет функциональный форму потенциала, new_type, new_spec1 и new_spec это индексы типа связи и типов связываемых атомов, после того как связь изменится (оборвется или превратится в связь другого типа). Эти поля представлены в виде массивов с двумя элементами. Первый соответствует ситуации, когда длина станет короче r2min1/2, второй когда превысит r2max1/2. Наиболее сложная часть алгоритма это применение свойств всех связей, с учетом возможности их обрыва и превращения, а также того, что другие потоки могли оборвать соседние связи, что привело к изменению типа связанных атомов. Поясню на примере той же воды. Изначально молекула электронейтральна, химические связи образованы электронами общими для водорода и кислорода. Грубо говоря можно сказать, что заряды на атомах водорода и кислорода нулевые (на самом деле электронная плотность смещена к кислороду, поэтому на водороде небольшой плюс, +, а на кислороде 2-). Если мы будем рвать связь, кислород окончательно заберет себе электрон, а водород его отдаст. Получатся частицы H+ и O-. Итого получается 5 типов частиц, условно обозначим их: H, H+, O, O-, O2-. Последняя образуется, если мы оторвем оба водорода от молекулы воды. Соответственно, реакции:
H2O -> H+ + OH-
и
OH- -> H+ + O2-.
Знатоки химии меня поправят, что при стандартных условиях для воды и первая то стадия распада практически не реализуется (в равновесии, только одна молекула из 107 диссоциирована на ионы, да и то не совсем такие, как написано). Но для описания алгоритмов, такие схемы будут наглядными. Допустим, один поток обрабатывает одну связь в молекуле воды, а другой поток вторую связь этой же молекулы. И так получилось, что обе связи нужно разорвать. Тогда один поток должен превратить атомы в H+ и O-, а второй в H+ и O2-. Но если потоки это делают одновременно, на момент начала процедуры кислород находится в состоянии O и оба потока его превращают в O-, что неверно. Вот такие ситуации нам необходимо как-то предотвращать. Блок-схема функции, обрабатывающей химическую связь:

Проверяем соответствуют ли текущие типы атомов типу связи, если нет, то берем из таблицы дефолтных типов (она должна быть заранее составлена), далее определяем квадрат расстояния между атомами (r2) и, если связь подразумевает максимальную или минимальную длину, проверяем, не вышли ли мы за эти границы. Если вышли, то нам необходимо поменять тип связи или удалить её и в обоих случаях поменять типы атомов. Для этого будет использована функция atomicCAS сравниваем текущий тип атома с тем, который должен быть и в этом случае заменяем на новый. Если тип атома уже был изменен другим потоком, возвращаемся в начало, к переопределению типа связи. Наихудший вариант, если тип первого атома мы успели поменять, а второго нет. Откатываться назад уже поздно, ведь после того, как мы поменяли первый атом, другие потоки уже могли что-то с ним сделать. Какой же выход? Предлагаю притвориться, что мы рвём/изменяем связь другого типа, а не того, за которую взялись вначале. Находим, какой тип связи должен был быть между начальным первым атомом и измененным вторым и обрабатываем её по тем же правилам, что и изначально ожидаемую. Если и в этом случае тип атома снова успел поменяться снова используем эту же схему. Тут неявно подразумевается, что новый тип связи обладает теми же свойствами рвется при той же длине и т.д., а частицы, образуемые в ходе разрыва такие, какие нужно. Поскольку эту информацию задет пользователь, мы как бы перекладываем ответственность с нашей программы на него, он должен задать все корректно. Код:
__global__ void apply_bonds(int iStep, int bndPerBlock, int bndPerThread, cudaMD* md){    int def;    int id1, id2;       // atom indexes    int old, old_spec2, spec1, spec2, new_spec1, new_spec2;     // atom types    int new_bond_type;        int save_lt, need_r, loop;    // flags to save lifetime, to need to calculate r^2 and to be in while loop    int mnmx;   // flag minimum or maximum    int action; // flag: 0 - do nothing, 1 - delete bond, 2 - transform bond    cudaBond *old_bnd, *cur_bnd;// old bond type, current bond type    float dx, dy, dz, r2, r;    float f, eng = 0.0f;    __shared__ float shEng;#ifdef DEBUG_MODE    int cnt;    // count of change spec2 loops#endif    if (threadIdx.x == 0)    {        shEng = 0.0f;    }    __syncthreads();    int id0 = blockIdx.x * bndPerBlock + threadIdx.x * bndPerThread;    int N = min(id0 + bndPerThread, md->nBond);    int iBnd;    for (iBnd = id0; iBnd < N; iBnd++)      if (md->bonds[iBnd].z)  // the bond is not broken      {          // atom indexes          id1 = md->bonds[iBnd].x;          id2 = md->bonds[iBnd].y;          // atom types          spec1 = md->types[id1];          spec2 = md->types[id2];          old_bnd = &(md->bondTypes[md->bonds[iBnd].z]);          cur_bnd = old_bnd;          save_lt = 0;          need_r = 1;          loop = 1;#ifdef DEBUG_MODE          cnt = 0;#endif                    if ((cur_bnd->spec1 == spec1)&&(cur_bnd->spec2 == spec2))          {              //ok          }          else              if ((cur_bnd->spec1 == spec2) && (cur_bnd->spec2 == spec1))              {                  invert_bond(id1, id2, spec1, spec2, &(md->bonds[iBnd]));                  //... then ok              }              else // atom types do not correspond to bond types              {                  save_lt = 1;              }          // end initial stage          while (loop)          {             if (save_lt)                    {                  def = md->def_bonds[spec1][spec2];                  if (def == 0)     // these atom types do not form a bond                  {#ifdef DEBUG_MODE                      printf("probably, something goes wrong\n");#endif                      action = 1;   // delete                      break;                  }                  else                  {                      //! change bond type and go on                      if (def < 0)                        {                          invert_bond(id1, id2, spec1, spec2, &(md->bonds[iBnd]));                          def = -def;                      }                      md->bonds[iBnd].z = def;                      cur_bnd = &(md->bondTypes[def]);                  }             }  // end if (save_lt)             // calculate distance (only once)             if (need_r)             {                dx = md->xyz[id1].x - md->xyz[id2].x;                dy = md->xyz[id1].y - md->xyz[id2].y;                dz = md->xyz[id1].z - md->xyz[id2].z;                delta_periodic(dx, dy, dz, md);                r2 = dx * dx + dy * dy + dz * dz;                need_r = 0;             }             action = 0;   // 0 - just cultivate bond 1 - delete bond 2 - transform bond             if ((cur_bnd->mxEx) && (r2 > cur_bnd->r2max))             {                 mnmx = 1;                 if (cur_bnd->new_type[mnmx] == 0)  // delete bond                   action = 1;                else                   action = 2;   // modify bond             }             else if ((cur_bnd->mnEx) && (r2 < cur_bnd->r2min))             {                 mnmx = 0;                 action = 2;   // at minimum only bond modification possible             }             // end select action             // try to change atom types (if needed)             if (action)             {                 save_lt = 1;                 new_spec1 = cur_bnd->new_spec1[mnmx];                 new_spec2 = cur_bnd->new_spec2[mnmx];                 //the first atom                 old = atomicCAS(&(md->types[id1]), spec1, new_spec1);                 if (old != spec1)                 {                     spec1 = old;                     spec2 = md->types[id2];   // refresh type of the 2nd atom                     // return to begin of the while loop                 }                 else      // types[id1] have been changed                 {#ifdef USE_NEWANG   // save changes in atom type                     atomicCAS(&(md->oldTypes[id1]), -1, spec1);#endif                     old_spec2 = spec2;                     while ((old = atomicCAS(&(md->types[id2]), old_spec2, new_spec2)) != old_spec2)                     {                         //! the worst variant: this thread changes atom 1, other thread changes atom 2                         // imagine that we had A-old bond with the same behavior                         def = md->def_bonds[spec1][old];#ifdef DEBUG_MODE                         if (def == 0)                         {                             printf("UBEH[001]: in apply_bonds, change atom types. There are no bond types between Species[%d] and [%d]\n", spec1, old);                             break;                         }#endif                         if (def < 0)  // spec1 -> new_spec2 spec2 -> newSpec1                         {                             cur_bnd = &(md->bondTypes[-def]);                             new_spec2 = cur_bnd->new_spec1[mnmx];                         }                         else // direct order                         {                             cur_bnd = &(md->bondTypes[def]);                             new_spec2 = cur_bnd->new_spec2[mnmx];                         }                         old_spec2 = old;#ifdef DEBUG_MODE                         cnt++;                         if (cnt > 10)                         {                             printf("UBEH[002]: too many atempst to change spec2 = %d\n", spec2);                             break;                         }#endif                     }#ifdef USE_NEWANG   // save changes in atom type                     atomicCAS(&(md->oldTypes[id2]), -1, spec2);#endif                     loop = 0;                 }                 //end change types             } // end if (action)             else               loop = 0;    // action == 0, out of cycle          }  // end while(loop)          if (action == 2)          {              new_bond_type = cur_bnd->new_type[mnmx];              if (new_bond_type < 0)              {                  md->bonds[iBnd].x = id2;                  md->bonds[iBnd].y = id1;                  new_bond_type = -new_bond_type;              }              md->bonds[iBnd].z = new_bond_type;              cur_bnd = &(md->bondTypes[new_bond_type]);          }          // perform calculations and save mean bond length          if (action != 1)  // not delete          {              r = sqrt(r2);              f = cur_bnd->force_eng(r2, r, eng, cur_bnd);              atomicAdd(&(md->frs[id1].x), f * dx);              atomicAdd(&(md->frs[id2].x), -f * dx);              atomicAdd(&(md->frs[id1].y), f * dy);              atomicAdd(&(md->frs[id2].y), -f * dy);              atomicAdd(&(md->frs[id1].z), f * dz);              atomicAdd(&(md->frs[id2].z), -f * dz);                            atomicAdd(&(cur_bnd->rSumm), r);              atomicAdd(&(cur_bnd->rCount), 1);          }          else      //delete bond          {// decrease the number of bonds for atoms              atomicSub(&(md->nbonds[id1]), 1);              atomicSub(&(md->nbonds[id2]), 1);              md->bonds[iBnd].z = 0;              // change parents              exclude_parents(id1, id2, md);          }          if (save_lt)          {              keep_bndlifetime(iStep, &(md->bonds[iBnd]), old_bnd);              if (action != 1) // not delete                atomicAdd(&(cur_bnd->count), 1);              atomicSub(&(old_bnd->count), 1);          }      } // end main loop      // split energy to shared and then to global memory      atomicAdd(&shEng, eng);      __syncthreads();      if (threadIdx.x == 0)          atomicAdd(&(md->engBond), shEng);}

В коде я использовал директивы препроцессора, чтобы включить проверки на те ситуации, которые могут возникнуть по недосмотру пользователя. Для ускорения быстродействия их можно отключить. Функция реализует указанную выше схему, но завернута ещё в один цикл, который пробегает по диапазону связей, за который отвечает данный поток. Здесь и далее идентификатор типа связи может быть отрицательный, это означает, что порядок атомов в связи нужно поменять местами (например, связь O-H и H-O это одна и та же связь, но в алгоритме порядок важен, чтобы это указать я использую индексы с противоположным знаком), делает это функция invert_bond, слишком тривиальная, чтобы её описывать. Функция delta_periodic применяет периодические граничные условия к разнице координат. Если нам нужно менять не только связи, но и валентные углы (директива USE_NEWANG), то необходимо помечать атомы, у которых мы поменяли тип (об этом далее). Для исключения повторного связывания одних и тех же атомов связью, массив parents хранит индекс одного из атомов связанных с данным (эта подстраховка работает не во всех случаях, но для моих этого достаточно). Если мы рвем какую-то связь, то нужно убрать из массива parents соответствующие индексы атомов, это делает функция exclude_parents:
__device__ void exclude_parents(int id1, int id2, cudaMD* md)// exclude id1 and id2 from parents of each other (if they are)//  and seek other parents if able{    // flags to clear parent    int clear_1 = 0;        int clear_2 = 0;    int i, flag;        if (md->parents[id1] == id2)         clear_1 = 1;    if (md->parents[id2] == id1)        clear_2 = 1;    i = 0;    while ((i < md->nBond) && (clear_1 || clear_2))    {        if (md->bonds[i].z != 0)        {            flag = 0;            if (clear_1)            {                if (md->bonds[i].x == id1)                {                    md->parents[id1] = md->bonds[i].y;                    flag = 1;                }                else if (md->bonds[i].y == id1)                {                    md->parents[id1] = md->bonds[i].y;                    flag = 1;                }                if (flag)                {                    clear_1 = 0;                    i++;                    continue;                }            }            if (clear_2)            {                if (md->bonds[i].x == id2)                {                    md->parents[id2] = md->bonds[i].y;                    flag = 1;                }                else if (md->bonds[i].y == id2)                {                    md->parents[id2] = md->bonds[i].y;                    flag = 1;                }                if (flag)                {                    clear_2 = 0;                    i++;                    continue;                }            }        }        i++;    }    // be on the safe side    if (clear_1)            md->parents[id1] = -1;    if (clear_2)        md->parents[id2] = -1;}

Функция, к сожалению, пробегает по всему массиву связей. Обрабатывать и удалять связи мы научились, теперь надо научится их создавать. Следующая функция помечает атомы, пригодные для образования химической связи:
__device__ void try_to_bind(float r2, int id1, int id2, int spec1, int spec2, cudaMD *md){    int r2Int;      //  (int)r2 * const    // save parents to exclude re-linking    if (md->parents[id1] == id2)        return;    if (md->parents[id2] == id1)        return;    if (md->bindBonds[spec1][spec2] != 0)    {        if (r2 < md->bindR2[spec1][spec2])        {            r2Int = (int)(r2 * 100);            if (atomicMin(&(md->r2Min[id1]), r2Int) > r2Int)    // replace was sucessfull            {                md->neighToBind[id1] = id2 + 1;     // as 0 is reserved for no neighbour                md->canBind[id1] = 1;            }            // similar for the second atom            if (atomicMin(&(md->r2Min[id2]), r2Int) > r2Int)    // replace was sucessfull            {                md->neighToBind[id2] = id1 + 1;     // as 0 is reserved for no bind                md->canBind[id2] = 1;            }        }    }}

В матрице bindBonds хранится информация о том, могут ли данные типы атомов образовывать связь и если могут то какую. В матрице bindR2 хранится максимальное расстояние между атомами необходимое для связывания. Если все условия благоприятные, то проверяем, нет ли у атомов других соседей пригодных для связывания, но поближе. Информация об ближайшем расстоянии до соседа хранится в массиве r2Min (для удобства массив имеет тип int и значения приводятся к нему с умножением на константу, 100). Если обнаруженный сосед наиближайший, то запоминаем его индекс в массиве neighToBind и ставим флаг canBind. Есть правда опасность, что пока мы перешли к обновлению индекса, другой поток перезаписал значение минимума, но это не так критично. Данную функцию целесообразно вызывать в функциях, выполняющих обход по парам атомов, например cell_list или all_pair, описанных в первой части. Само связывание:
__global__ void create_bonds(int iStep, int atPerBlock, int atPerThread, cudaMD* md)// connect atoms which are selected to form bonds{    int id1, id2, nei;    // neighbour index    int btype, bind;    // bond type index and bond index    cudaBond* bnd;    int spec1, spec2;   // species indexes        int id0 = blockIdx.x * atPerBlock + threadIdx.x * atPerThread;    int N = min(id0 + atPerThread, md->nAt);    int iat;    for (iat = id0; iat < N; iat++)    {        nei = md->neighToBind[iat];        if (nei)    // neighbour exists        {            nei--;  // (nei = spec_index + 1)            if (iat < nei)            {                id1 = iat;                id2 = nei;            }            else            {                id1 = nei;                id2 = iat;            }                        // try to lock the first atom            if (atomicCAS(&(md->canBind[id1]), 1, 0) == 0)  // the atom is already used                continue;            // try to lock the second atom            if (atomicCAS(&(md->canBind[id2]), 1, 0) == 0)  // the atom is already used            {                // unlock the first one back                atomicExch(&(md->canBind[id1]), 1);                continue;            }            // create bond iat-nei            bind = atomicAdd(&(md->nBond), 1);#ifdef DEBUG_MODE            if (bind >= md->mxBond)            {                printf("UBEH[003]: Exceed maximal number of bonds, %d\n", md->mxBond);            }#endif            spec1 = md->types[id1];            spec2 = md->types[id2];#ifdef USE_NEWANG   // save that we have changed atom type            atomicCAS(&(md->oldTypes[id1]), -1, spec1);            atomicCAS(&(md->oldTypes[id2]), -1, spec2);#endif            btype = md->bindBonds[spec1][spec2];                        if (btype < 0)            {                // invert atoms order                md->bonds[bind].x = id2;                md->bonds[bind].y = id1;                md->bonds[bind].z = -btype;                bnd = &(md->bondTypes[-btype]);                // change atom types according the formed bond                md->types[id1] = bnd->spec2;                md->types[id2] = bnd->spec1;            }            else             {                md->bonds[bind].x = id1;                md->bonds[bind].y = id2;                md->bonds[bind].z = btype;                bnd = &(md->bondTypes[btype]);                // change atom types according the formed bond                md->types[id1] = bnd->spec1;                md->types[id2] = bnd->spec2;            }                        atomicAdd((&bnd->count), 1);            md->bonds[bind].w = iStep;  // keep time of the bond creation for lifetime calculation            atomicAdd(&(md->nbonds[id1]), 1);            atomicAdd(&(md->nbonds[id2]), 1);            // replace parents if none:            atomicCAS(&(md->parents[id1]), -1, id2);            atomicCAS(&(md->parents[id2]), -1, id1);        }    }    // end loop by atoms}

Функция блокирует один атом, затем второй и, если удалось заблокировать оба, создает связь между ними. В самом начале функции идёт упорядочивание индексов атомов по порядку, чтобы исключить ситуацию, когда один поток блокирует первый атом в паре, а другой второй атом в той же паре, оба потока успешно проходят первую проверку и заваливаются на второй, в результате ни один из них не создает связь. Ну и, наконец, нам нужно удалять те связи, которые мы пометили на удаление в функции apply_bonds:
__global__ void clear_bonds(cudaMD* md)// clear bonds with .z == 0{    int i = 0;    int j = md->nBond - 1;    while (i < j)    {        while ((md->bonds[j].z == 0) && (j > i))            j--;        while ((md->bonds[i].z != 0) && (i < j))            i++;        if (i < j)        {            md->bonds[i] = md->bonds[j];            md->bonds[j].z = 0;            i++;            j--;        }    }    if ((i == j) && (md->bonds[i].z == 0))        md->nBond = j;    else        md->nBond = j + 1;}

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

Валентные углы


С валентными углами я введу несколько допущений, чтобы облегчить алгоритмы и функции, в результате они будут даже проще, чем для валентных связей. Во-первых, параметры валентных углов должны зависеть от всех трех атомов, но здесь мы будем считать, что тип валентного угла определяет исключительно атом в его вершине. Алгоритм образования/удаления углов предлагаю простейший: всегда, когда изменяли тип атома, запоминаем этот факт в соответствующем массиве oldTypes[]. Размер массива равен числу атомов, изначально он заполнен -1. Если какая-то функция меняет тип атома, она заменяет -1 на индекс первоначального типа. У всех атомов, которые поменяли тип, удаляем все валентные углы и пробегаем по всем связям этого атома, чтобы добавить соответствующие углы:
__global__ void refresh_angles(int iStep, int atPerBlock, int atPerThread, cudaMD *md)// delete old angles and create new ones for atoms which change their type{int i, j, n, t, ang;int nei[8];// bonded neighbors of given atomint cnt;int id0 = blockIdx.x * atPerBlock + threadIdx.x * atPerThread;int N = min(id0 + atPerThread, md->nAt);int iat;for (iat = id0; iat < N; iat++)if (md->oldTypes[iat] != -1){i = 0;n = md->nangles[iat];while (n && (i < md->nAngle)){if (md->angles[i].w)if (md->angles[i].x == iat){n--;md->angles[i].w = 0;}i++;}// create new anglest = md->specAngles[md->types[iat]];// get type of angle, which formed by current atom typeif (t && (md->nbonds[iat] > 1))// atom type supports angle creating and number of bonds is enough{// search of neighbors by bondsi = 0; cnt = 0;n = md->nbonds[iat];while (n && (i < md->nBond)){if (md->bonds[i].z)// if bond isn't deleted{if (md->bonds[i].x == iat){nei[cnt] = md->bonds[i].y;cnt++;n--;}else if (md->bonds[i].y == iat){nei[cnt] = md->bonds[i].x;cnt++;n--;}}i++;}// add new angles based on found neighbors:for (i = 0; i < cnt-1; i++)for (j = i + 1; j < cnt; j++){ang = atomicAdd(&(md->nAngle), 1);md->angles[ang].x = iat;md->angles[ang].y = nei[i];md->angles[ang].z = nei[j];md->angles[ang].w = t;}n = (cnt * (cnt - 1)) / 2;}md->nangles[iat] = n;// reset flagmd->oldTypes[iat] = -1;}}

Массив specAngles содержит идентификаторы валентных углов, соответствующих данному типу атома. Следующая функция вызывает вычисление энергии и сил для всех углов:
__global__ void apply_angles(int iStep, int angPerBlock, int angPerThread, cudaMD* md)// apply valence angle potentials{cudaAngle* ang;// energies of angle potentialfloat eng;__shared__ float shEng;if (threadIdx.x == 0)shEng = 0.0f;__syncthreads();int id0 = blockIdx.x * angPerBlock + threadIdx.x * angPerThread;int N = min(id0 + angPerThread, md->nAngle);int i;for (i = id0; i < N; i++)if (md->angles[i].w){ang = &(md->angleTypes[md->angles[i].w]);ang->force_eng(&(md->angles[i]), ang, md, eng);}// split energy to shared and then to global memoryatomicAdd(&shEng, eng);__syncthreads();if (threadIdx.x == 0)atomicAdd(&(md->engAngl), shEng);}

Ну и для примера потенциала таких углов, даю функцию гармонического косинуса, на которую может указывать поле force_eng структуры cudaAngle:
__device__ void angle_hcos(int4* angle, cudaAngle* type, cudaMD* md, float& eng)// harmonic cosine valent angle potential:// U = k / 2 * (cos(th)-cos(th0))^{float k = type->p0;float cos0 = type->p1;// indexes of central atom and ligands:int c = angle->x;int l1 = angle->y;int l2 = angle->z;// vector ijfloat xij = md->xyz[l1].x - md->xyz[c].x;float yij = md->xyz[l1].y - md->xyz[c].y;float zij = md->xyz[l1].z - md->xyz[c].z;delta_periodic(xij, yij, zij, md);float r2ij = xij * xij + yij * yij + zij * zij;float rij = sqrt(r2ij);// vector ikfloat xik = md->xyz[l2].x - md->xyz[c].x;float yik = md->xyz[l2].y - md->xyz[c].y;float zik = md->xyz[l2].z - md->xyz[c].z;delta_periodic(xik, yik, zik, md);float r2ik = xik * xik + yik * yik + zik * zik;float rik = sqrt(r2ik);float cos_th = (xij * xik + yij * yik + zij * zik) / rij / rik;float dCos = cos_th - cos0; // delta cosinusfloat c1 = -k * dCos;float c2 = 1.0 / rij / rik;atomicAdd(&(md->frs[c].x), -c1 * (xik * c2 + xij * c2 - cos_th * (xij / r2ij + xik / r2ik)));atomicAdd(&(md->frs[c].y), -c1 * (yik * c2 + yij * c2 - cos_th * (yij / r2ij + yik / r2ik)));atomicAdd(&(md->frs[c].z), -c1 * (zik * c2 + zij * c2 - cos_th * (zij / r2ij + zik / r2ik)));atomicAdd(&(md->frs[l1].x), c1 * (xik * c2 - cos_th * xij / r2ij));atomicAdd(&(md->frs[l1].y), c1 * (yik * c2 - cos_th * yij / r2ij));atomicAdd(&(md->frs[l1].z), c1 * (zik * c2 - cos_th * zij / r2ij));atomicAdd(&(md->frs[l2].x), c1 * (xij * c2 - cos_th * xik / r2ik));atomicAdd(&(md->frs[l2].y), c1 * (yij * c2 - cos_th * yik / r2ik));atomicAdd(&(md->frs[l2].z), c1 * (zij * c2 - cos_th * zik / r2ik));eng += 0.5 * k * dCos * dCos;}

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

Примеры


Не претендуя на точность, я попробовал изобразить сборку молекул воды из отдельных ионов. Парные потенциалы задал произвольно в форме потенциала Букингема, а затем добавил возможность создавать связи в виде гармонического потенциала, с равновесным расстоянием равным длине связи H-O в воде, 0.96 . Кроме того, при связывании второго протона с кислородом добавлялся валентный угол с вершиной в кислороде. После 100000 шагов из случайно разбросанных ионов появились первые молекулы. На рисунке показаны начальная (слева) и конечная (справа) конфигурации:

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


Заключительные комментарии


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

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

Что такое температура и как её учитывать в молекулярном моделировании? Реализация на CUDA

06.01.2021 20:05:30 | Автор: admin

Методы численного моделирования молекулярных систем, такие как молекулярная динамика, рассматривают эти системы как механические (что-то вроде набора шариков на пружинках). Однако, в отличие от механических систем, для молекулярных существует понятие температура. Вещество не может существовать без температуры, а температура без вещества (на счет последней части утверждения есть и другое мнение). Из опыта мы знаем, что очень многие свойства вещества кардинально зависят от температуры, и, естественно, что её надо как-то учитывать. Для поддержания температуры в молекулярной динамике используются специальные алгоритмы-термостаты. Наиболее известные среди них это термостаты Андерсена, Берендсена и Нозе-Гувера. Все они основаны на молекулярно-кинетической теории газов, где температура есть просто величина пропорциональная среднекинетической энергии молекул. Соответственно, работа данных термостатов осуществляется путём умножения скоростей частиц на некоторую величину.

Что же не так?

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

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

  3. Связь температуры и кинетической энергии выводится из основных положений МКТ, которые верны только для идеального газа. Наиболее близкими к идеальному газу реальные системы это благородные газы при высоких температурах и низких давлениях. Проиллюстрировать неуниверсальность МКТ можно простым примером: при одной температуре средняя кинетическая энергия атомов в твердом теле и в газе должна быть одинаковой, что не кажется правдоподобным.

Модель

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

Соберем известные факты о температуре:

  • Температура не может быть ниже абсолютного нуля, т.е. 0 К.

  • Температура понятие интенсивное, оно не зависит от размеров системы, количества вещества и т.д.

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

  • Скорости химических реакций и коэффициенты диффузии с температурой растут экспоненциально.

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

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

  • С увеличением температуры тела обычно расширяются (газы все, твердые тела почти все, но есть исключения, см. отрицательный КТР).

  • Теплоемкость с температурой как правило растет (для некоторых жидкостей, такие как расплавленные оксиды она постоянная или чуть падает).

  • С температурой связан интересный эффект, адиабатическое расширение в пустоту (эффект Джоуля-Томсона).

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

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

  • Электропроводность металлов падает с температурой.

  • Спектр излучения абсолютно черного тела определяется исключительно его температурой.

Как мы видим, количественных характеристик температуры не так уж и много. Привязываться к каким-то ключевым точкам типа температуры кипения воды не имеет смысла, поскольку: они очень частные; зависят и от других условий, в первую очередь, давления. Тем не менее, есть характеристика, кажущаяся довольно перспективной. Это спектр излучения абсолютно черного тела. Оказывается, если тело ничего не отражает, а все излучение от него является собственным, то спектр этого излучения не зависит от природы объекта, а определяется исключительно его температурой, согласно формуле Планка. Тот факт, что все тела излучают и характеристики этого излучения зависят от температуры используется, например, в оптической пирометрии. Таким образом, температуру можно определить через спектр излучения. В условии равновесия с термостатом, т.е. средой, имеющей заданную температуру, излучение, испускаемое объектом, должно сравняться с излучением термостата. Тогда можно реализовать термостатирование следующим образом: атомы поглощают и испускают фотоны в случайном направлении, при этом их скорости изменяются согласно закону сохранения импульса. Интуитивно кажется, что если мы будем прибавлять/убавлять поправку к скорости атома в случайном направлении, то атом сначала наберет какую-то скорость, а затем эта скорость выйдет на насыщение, поскольку все выше вероятность того, что новая скорость будет направлена против одной из предыдущих добавок. Однако легко показать, что это не так, начнем с закона сохранения импульса:

m\vec{v_1}=m\vec{v_0}+\vec{p_\gamma},\qquad\qquad\qquad (1)

где m и v масса и скорость атома, индексы 1 и 0 означают состояния до и после поглощения фотона, p импульс фотона. Перейдем к скоростям:

\vec{v_1}=\vec{v_0}+\frac\epsilon{mc}\vec{r},\quad\quad\quad(2)

где энергия фотона, c скорость света в вакууме, r единичный вектор случайного направления. Из этого, квадрат скорости равен:

v_1^2 = (\vec{v_0} + \frac{mc}\vec{r})^2 = v_0^2 + 2 \frac{mc}\vec{r}\vec{v_0} + \frac{^2}{m^2c^2}, \quad\quad\quad(3)

или, используя определение скалярного произведения векторов:

v_1^2 = v_0^2 + 2 \frac{mc}v_0 \cos \phi + \frac{^2}{m^2c^2},\quad\quad(4)

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

\langle\Delta v^2\rangle = \frac{^2}{m^2c^2}\quad\quad(5)

положительное. Т.е. действуя таким образом, мы будем накачивать систему кинетической энергией, независимо от того будем излучать или поглощать фотоны, поскольку знак будет стоять перед косинусом, который в среднем нулевой. Чтобы скомпенсировать неограниченный рост скоростей в системе нужно, чтобы среднее изменение квадрата скорости атома при излучении фотона было равно среднему значению изменения квадрата скорости при поглощении фотона (5) с обратным знаком:

\langle\Delta v^2\rangle = -\frac{^2}{m^2c^2}\quad\quad(6)

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

\langle\cos\phi\rangle = -\frac{}{2mcv_0}\quad\quad(7)

Это выражение устанавливает связь между скоростями атомов и энергией фотонов. Можно задать косинус равномерной случайной величиной, так чтобы указанное выше значение было серединой интервала, а -1 его нижней границей.

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

\frac12mv_1^2 = \frac12mv_0^2 + ,\quad\quad(8)

однако, если мы попытаемся решить уравнение в системе с законом сохранения импульса (2), мы получим нефизичный результат. Например, покоящийся атом должен разогнаться до световой скорости. Причина этого заключается в том, при одном порядке кинетической энергии атома и энергии фотона их импульсы отличаются на несколько порядков. Решение напрашивается следующее: импульс сохраняем, вычисляем изменение кинетической энергии, оставшийся избыток энергии фотона добавляем к какой-нибудь другой энергии атома, назовём её тепловой (обозначим U):

U_1 + K_1 = U_0 + K_0 + ,\quad\quad(9)

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

[м] = 0.002898 / T,\quad\quad(10)

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

 = T,\quad\quad(11)

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

Кроме того, у нас есть ещё одна численная характеристика, касающаяся температуры. Мы совершенно точно знаем, какое количество теплоты (энергии/тепловой энергии) нужно сообщить телу, чтобы нагреть его до заданной температуры от нуля кельвинов. В условиях постоянного давления эта величина называется приращением энтальпии / инкрементом энтальпии / энтальпией (не путать с энтальпией образования), её значение зависит не только от температуры, но и от природы вещества. Если приводить образную аналогию, то вещество можно представить как сосуды с некой жидкостью, где её уровень это температура, а объём энтальпия. Производная энтальпии по температуре называется изобарной теплоемкостью, cp. Для идеального газа (и, с некоторым приближением, для благородных газов) cp постоянна, тогда:

H = c_p T.\quad\quad(12)

Объединяя (11) и (12) можно исключить температуру и получить энергию фотона как функцию от текущей тепловой энергии, запасенной в веществе:

 = \frac{c_p} H.\quad\quad(13)

Мы получили, что энергия излучаемых фотонов определяется энтальпией вещества. Продолжая нашу аналогию с сосудами, можно представить, что они теряют жидкость пропорционально высоте столба. Напоминаю, что выражение (13) справедливо только для систем с постоянной теплоемкостью, для реальных систем нужно находить температуру как функцию от энтальпии и подставлять её в (11). При переходе к числам меня ждал сюрприз, выразив из (10) значение и подставив его в (13), а cp взяв равным 5/2k (изобарная теплоемкость одноатомного идеального газа) получилось, что коэффициент перед энтальпией больше 1, а это означает, что система должна излучать больше энергии, чем имеется, что, конечно, не физично. Немного покопавшись, я нашел, что при переходе от длины волны к частоте (и затем к энергии) нужно использовать несколько отличное выражение, чем (10). В конечном итоге, с использованием данных по теплоемкости гелия, у меня получилось, /cp 0.9, что я и взял для своих численных экспериментов. В качестве энтальпии я взял величину U, описанную выше, формула (9).

Немного кода

Основная часть кода базируется на изложенном ранее. Однако вместо термостата Нозе-Гувера используется вышеописанная схема. Функция термостатирования:

__global__ void tstat_radi(int atPerBlock, int atPerThread, cudaMD* md){    __shared__ int indEng;    __shared__ float engTemp;    if (threadIdx.x == 0)    {        indEng = atomicAdd(&(md->curEng), 1);   // get current index in photon energies array        engTemp = 0.f;    }    __syncthreads();    uint4 randVar = md->ui4rnd;    float pe;        // photon energy    int i, e0, ei;// indexes    // atomic range    int id0 = blockIdx.x * atPerBlock + threadIdx.x * atPerThread;    int N = min(id0 + atPerThread, md->nAt);    e0 = indEng * atPerBlock + threadIdx.x * atPerThread - id0;    for (i = id0; i < N; i++)    {        ei = e0 + i;        if (ei >= md->nAt)            ei -= md->nAt;        pe = md->engPhotons[ei];        adsorb_rand_photon(&(md->vls[i]), &(md->engs[i]), md->masses[i], pe, randVar, md);        radiate_photon(&(md->vls[i]), &(md->engs[i]), md->masses[i], randVar, md);    }    atomicAdd(&engTemp, teng);    if (threadIdx.x == 0)    {        rnd_xor128(md->ui4rnd);        atomicAdd(&(md->engTemp), engTemp);    }}

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

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

__device__ void adsorb_rand_photon(float3 *vel, float *int_eng, float mass, float eng, uint4 &rand_var, cudaMD *md){    float u0 = *int_eng;    float v02 = float3_sqr(*vel);   // square of initial velocity    float3 rand_vect = rand_uvect(rand_var, md);    // momentum conservation:    float ermc = eng * revLight / mass;    vel->x += ermc * rand_vect.x;     vel->y += ermc * rand_vect.y;    vel->z += ermc * rand_vect.z;    float v12 = float3_sqr(*vel);    // energy conservation: old kinetic energy + photon energy = new kinetic energy + 'internal' energy    *int_eng += eng + 0.5f * mass * (v02 - v12);}

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

__device__ void radiate_photon(float3* vel, float* int_eng, float mass, uint4& rand_var, cudaMD* md){    float u0 = *int_eng;    float v02 = float3_sqr(*vel);   // square of initial velocity    float v0 = sqrt(v02);           // module of initial velocity    float ph_eng = 0.9f * u0;    float ermc = ph_eng * revLight / mass;    // random radiation    float3 rand_vect;    // random cosine between   (1-2a/v0) .. -1 with mean = -a/v0, where a = e/mc    float ermcv0 = ermc / v0;       // ph_eng/(m*c*v0)    float cos_phi;    if (ermcv0 >= 1.f)        cos_phi = -1.f;    else    {        int rnd = rnd_xor128(rand_var) % 2048;        float cos_phi = (float)rnd / 1024.f * (1.f - ermcv0); // 1024, because factor of 2          cos_phi -= 1.f;        rnd = rnd_xor128(rand_var) % 2048;        float theta = (float)rnd / 1024.f * numPi;  // 0 .. 2PI        rand_vect = get_angled_vector(*vel, cos_phi, theta);    }    // momentum conservation:    vel->x += ermc * rand_vect.x;       vel->y += ermc * rand_vect.y;    vel->z += ermc * rand_vect.z;    float v12 = float3_sqr(*vel);    // energy conservation:    *int_eng -= (ph_eng + 0.5f * mass * (v12 - v02));}

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

__device__ float3 get_angled_vector(float3 invec, float cos_phi, float theta){    float3 v1, v2, v3;    float leng1 = float3_length(invec);    // only for non-zero input vectors!    v1 = make_float3(invec.x / leng1, invec.y / leng1, invec.z / leng1);    // find v2 perpendicular to v1    if (v1.x != 0.f)    {        v2.y = 1.f; v2.z = 1.f; // any coordinates        v2.x = -(v1.y * v2.y + v1.z * v2.z) / v1.x;    }    else        if (v1.y != 0.f)        {            v2.x = 1.f; v2.z = 1.f; // any coordinates            v2.y = -(v1.z * v2.z) / v1.y;     // a1 = 0 !        }        else // a1=0, b1=0, c1 <> 0:        {            v2.x = 1.f; v2.y = 0.f; v2.z = 0.f;        }    // v3 is perpendicular to both v1 and v2    v3.x = v1.y * v2.z - v1.z * v2.y;    v3.y = -v1.x * v2.z + v1.z * v2.x;    v3.z = v1.x * v2.y - v1.y * v2.x;    // convert v2 and v3 into unit vectors    float leng2 = float3_length(v2);    float leng3 = float3_length(v3);    v2.x /= leng2;    v2.y /= leng2;    v2.z /= leng2;    v3.x /= leng3;    v3.y /= leng3;    v3.z /= leng3;    float sinPhi, sinTh, cosTh;    sinPhi = sqrt(1 - cos_phi * cos_phi);    sincos(theta, &sinTh, &cosTh);    // parameteric equation of circle    float x = v1.x * cos_phi + sinPhi * (cosTh * v2.x + sinTh * v3.x);    float y = v1.y * cos_phi + sinPhi * (cosTh * v2.y + sinTh * v3.y);    float z = v1.z * cos_phi + sinPhi * (cosTh * v2.z + sinTh * v3.z);    float3 res = make_float3(x, y, z);    return res;} 

Генератор случайных чисел на CUDA не тривиальная задача, спасибо пользователю @denglide за обзор генераторов и пользователю @maratyszcza за xorShift генератор, который и был использован в данной работе.

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

Результаты

Ну а теперь к численным экспериментам. В качестве модельного объекта я взял 40000 атомов аргона, взаимодействующих согласно парному потенциалу из этой работы. Размеры бокса выбраны таким образом, чтобы плотность системы соответствовали плотности аргона при стандартных условиях (T = 298 К и P = 1 атм). Все приведенные результаты даны для 1 млн шагов интегрирования с шагом интегрирования 1 фс. Вычисления проводил на видеокарте GeForce RTX 2080 Ti.

Рис. 1. Зависимость кинетической энергии от времени эволюции системы

Ещё раз напоминаю, что целевую температуру я задаю через набор энергий фотонов, излучаемых термостатом. На данном этапе я следил за изменением кинетической энергии системы (рис. 1) при различных температурах термостата и разных вариантов начальных скоростей атомов. В ряде случаев начальные скорости были нулевыми (Ekin = 0), в других случаях были для всех атомов одинаковыми по модулю, но направленными хаотично (Ekin > 0). Независимо от величины начальных скоростей, кинетическая энергия выходила на примерно постоянное значение, зависящее от температуры термостата, т.е. такой термостат умеет разгонять или замедлять атомы, в зависимости от текущего состояния системы и целевой температуры, что в первую очередь от него и требуется.

Рис. 2. Распределение атомов по скоростям после 1млн шагов эволюции системы.

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

Заключение

В качестве заключения кратко суммирую плюсы и минусы полученного способа задания температуры.

Плюсы предложенного термостата:

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

  2. Распределение скоростей частиц в системе приобретает вид распределения Максвелла.

  3. Вычислительная сложность порядка O(N), как у других термостатов.

  4. Температура определяет скорости частиц, но не сводится к ним.

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

Минусы и недоработки:

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

  2. Не удалось воспроизвести правильное давление (и его температурную зависимость), возможно решается подбором коэффициента между энтальпией и энергией фотона.

  3. Формула (7) выведена исключительно из математических соображений и имеет слабое физическое обоснование.

  4. Непонятно, откуда берется начальное распределение фотонов по энергиям, здесь оно просто постулируется на основании формулы Планка, а должно получатся "естественным образом".

Ну и напоследок, вот как выглядят 40000 атомов:

Подробнее..

Сравнение времени выполнения алгоритма на CPU и GPU

31.10.2020 16:04:39 | Автор: admin

Использование CUDA Runtime API для вычислений. Сравнение CPU и GPU вычислений.


В данной статье я решил провести сравнение выполнения алгоритма написанного на C++ на центральном и графическом процессоре(выполнение вычислений с помощью Nvidia CUDA Runtime API на поддерживаемом GPU Nvidia). CUDA API позволяет выполнение некоторых вычислений на графическом процессоре. Файл c++ использующий cuda, будет иметь расширение .cu.
Схема работы алгоритма приведена ниже.

Задача алгоритма состоит в том, что найти возможные числа X, при возведении которых в степень degree_of, будет получатся исходное число max_number. Сразу отмечу, что все числа которые будут передаваться GPU, будут хранится в массивах. Алгоритм, выполняемый каждым потоком, имеет приблизительно следующий вид:

intdegree_of=2;intdegree_of_max=Number_degree_of_max[0];//Массивхранящийзначениемаксимальнойстепеничислаintx=thread;//номервыполняемогопотокаintmax_number=INPUT[0];//Массивхранящийчисло,котороенеобходимополучитьintNumber=1;intDegree;boolBREAK=false;//Переменнаядлязавершенияwhilewhile(degree_of<=degree_of_max&&!BREAK){Number=1;for(inti=0;i<degree_of;i++){Number*=x;Degree=degree_of;}if(Number==max_number){OUT_NUMBER[thread]=X;//OUT_NUMBERМассивхранящийчислакоторыенеобходимовозвестивстепеньDegreeдляполученияисходногочислаOUT_DEGREE[thread]=Degree;//OUT_DEGREEМассивхранящийстепеньвкоторуюнужновозвестичислоXдляполученияисходногочисла}degree_of++;//Вслучаевыходазапредел:if(degree_of>degree_of_max||Number>max_number){BREAK=true;}}

Код для выполнения на CPU
#include <iostream>#include<vector>#include<string>//необходимо для getline#include<thread>#include<fstream>using namespace std;int Running_thread_counter = 0;void Upload_to_CPU(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int THREAD);void Upload_to_CPU(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int THREAD) {int thread = THREAD;Running_thread_counter++;unsigned long long  MAX_DEGREE_OF = max[0];int X = thread;unsigned long long  Calculated_number = 1;unsigned long long  DEGREE_OF = 2;unsigned long long   INP = INPUT[0];Stop[thread] = false;bool BREAK = false;if (X != 0 && X != 1) {while (!BREAK) {if (DEGREE_OF <= MAX_DEGREE_OF) {Calculated_number = 1;for (int counter = 0; counter < DEGREE_OF; counter++) {Calculated_number *= X;}if (Calculated_number == INP) {Stepn[thread] = DEGREE_OF;Number[thread] = X;Stop[thread] = true;BREAK = true;}DEGREE_OF++;}else { BREAK = true; }}}}void Parallelize_to_threads(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int size);int main(){int size = 1000;unsigned long long  *Number = new unsigned long long[size], *Degree_of = new unsigned long long[size];unsigned long long  *Max_Degree_of = new unsigned long long[1];unsigned long long  *INPUT_NUMBER = new unsigned long long[1];Max_Degree_of[0] = 7900;INPUT_NUMBER[0] = 216 * 216 * 216;ifstream inp("input.txt");if (inp.is_open()) {string t;vector<unsigned long long>IN;while (getline(inp, t)) {IN.push_back(stol(t));}INPUT_NUMBER[0] = IN[0];//исходное числоMax_Degree_of[0] = IN[1];//значение максимальной степени}else {ofstream error("error.txt");if (error.is_open()) {error << "No file " << '"' << "input.txt" << '"' << endl;error << "Please , create a file" << '"' << "input.txt" << '"' << endl;error << "One read:input number" << endl;error << "Two read:input max stepen" << endl;error << "." << endl;error.close();INPUT_NUMBER[0] = 1;Max_Degree_of[0] = 1;}}//расскометрируйте следующий код , если хотите видеть исходные значения в окне консоли //cout << INPUT[0] << endl;bool *Elements_that_need_to_stop = new bool[size];Parallelize_to_threads(Number, Degree_of, Elements_that_need_to_stop, INPUT_NUMBER, Max_Degree_of, size);vector<unsigned long long>NUMBER, DEGREEOF;for (int i = 0; i < size; i++) {if (Elements_that_need_to_stop[i]) {if (Degree_of[i] < INPUT_NUMBER[0] && Number[i] < INPUT_NUMBER[0]) {//проверка на ошибки NUMBER.push_back(Number[i]);DEGREEOF.push_back(Degree_of[i]);}}}//расскометрируйте следующий код , если хотите вывести результаты в консоль//это может замедлить программу /*for (int f = 0; f < NUMBER.size(); f++) {cout << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;}*/ofstream out("out.txt");if (out.is_open()) {for (int f = 0; f < NUMBER.size(); f++) {out << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;}out.close();}}void Parallelize_to_threads(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int size) {thread *T = new thread[size];Running_thread_counter = 0;for (int i = 0; i < size; i++) {T[i] = thread(Upload_to_CPU, Number, Stepn, Stop, INPUT, max, i);T[i].detach();}while (Running_thread_counter < size - 1);//дождаться завершения выполнения всех потоков }


Для работы алгоритма необходим текстовый файл с исходным числом и максимальной степенью.
Код для выполнения вычислений на GPU
//библиотеки cuda_runtime.h и device_launch_parameters.h//для работы с cyda#include "cuda_runtime.h"#include "device_launch_parameters.h"#include<vector>#include<string>//для getline#include <stdio.h>#include<fstream>using namespace std;__global__ void Upload_to_GPU(unsigned long long  *Number,unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT,unsigned long long  *max) {int thread = threadIdx.x;unsigned long long  MAX_DEGREE_OF = max[0];    int X = thread;unsigned long long  Calculated_number = 1;unsigned long long  Current_degree_of_number = 2;    unsigned long long   Original_numberP = INPUT[0];Stop[thread] = false;bool BREAK = false;if (X!=0&&X!=1) {while (!BREAK) {if (Current_degree_of_number <= MAX_DEGREE_OF) {Calculated_number = 1;for (int counter = 0; counter < Current_degree_of_number; counter++) { Calculated_number*=X;}if (Calculated_number == Original_numberP) {Stepn[thread] = Current_degree_of_number;Number[thread] = X;Stop[thread] = true;BREAK = true;}Current_degree_of_number++;}else { BREAK = true; }}}}cudaError_t Configure_cuda(unsigned long long *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max,unsigned int size);int main(){int size = 1000;    unsigned long long  *Number=new unsigned long long [size], *Degree_of=new unsigned long long [size];unsigned long long  *Max_degree_of = new unsigned long long [1];unsigned long long  *INPUT_NUMBER = new unsigned long long [1];   Max_degree_of[0] = 7900;ifstream inp("input.txt");if (inp.is_open()) {string text;vector<unsigned long long>IN;while (getline(inp, text)) {IN.push_back( stol(text));}INPUT_NUMBER[0] = IN[0];Max_degree_of[0] = IN[1];}else {ofstream error("error.txt");if (error.is_open()) {error<<"No file "<<'"'<<"input.txt"<<'"'<<endl;error<<"Please , create a file" << '"' << "input.txt" << '"' << endl;error << "One read:input number" << endl;error << "Two read:input max stepen" << endl;error << "." << endl;error.close();INPUT_NUMBER[0] = 1;Max_degree_of[0] = 1;}}bool *Elements_that_need_to_stop = new bool[size];    // Загрузка массивов в cudacudaError_t cudaStatus =  Configure_cuda(Number, Degree_of, Elements_that_need_to_stop, INPUT_NUMBER, Max_degree_of, size);    if (cudaStatus != cudaSuccess) {        fprintf(stderr, "addWithCuda failed!");        return 1;    }vector<unsigned long long>NUMBER, DEGREEOF;for (int i = 0; i < size; i++) {if (Elements_that_need_to_stop[i]) {NUMBER.push_back(Number[i]);//занести в вектор числоDEGREEOF.push_back(Degree_of[i]);//занести в вектор степень числа}}//раскоментируйте следующий код , чтобы вывести результаты в консоль/*for (int f = 0; f < NUMBER.size(); f++) {cout << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;}*/ofstream out("out.txt");if (out.is_open()) {for (int f = 0; f < NUMBER.size(); f++) {out << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;}out.close();}    //Очистить ресурсы связанные с устройством    cudaStatus = cudaDeviceReset();    if (cudaStatus != cudaSuccess) {        fprintf(stderr, "cudaDeviceReset failed!");        return 1;    }    return 0;}cudaError_t  Configure_cuda(unsigned long long  *Number, unsigned long long *Degree_of, bool *Stop,unsigned long long *INPUT, unsigned long long *max,unsigned int size) {unsigned long long *dev_Number = 0;unsigned long long *dev_Degree_of = 0;unsigned long long *dev_INPUT = 0;unsigned long long *dev_Max = 0;bool *dev_Elements_that_need_to_stop;cudaError_t cudaStatus;// УСТАНОВКА ИСПОЛЬЗУЕМОГО GPU cudaStatus = cudaSetDevice(0);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaSetDevice failed!  Do you have a CUDA-capable GPU installed?");goto Error;}// РЕЗЕРВИРОВАНИЕ МЕСТА В ПАМЯТИ ПОД ДАННЕcudaStatus = cudaMalloc((void**)&dev_Number, size * sizeof(unsigned long long));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!dev_Number");goto Error;}cudaStatus = cudaMalloc((void**)&dev_Degree_of, size * sizeof(unsigned long long));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!dev_Degree_of");goto Error;}cudaStatus = cudaMalloc((void**)&dev_Max, size * sizeof(unsigned long long int));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!dev_Max");goto Error;}cudaStatus = cudaMalloc((void**)&dev_INPUT, size * sizeof(unsigned long long));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!dev_INPUT");goto Error;}cudaStatus = cudaMalloc((void**)&dev_Elements_that_need_to_stop, size * sizeof(bool));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!dev_Stop");goto Error;}// ПЕРЕМЕЩЕНИЕ ДАННХ В ПАМЯТЬ GPUcudaStatus = cudaMemcpy(dev_Max, max, size * sizeof(unsigned long long), cudaMemcpyHostToDevice);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!");goto Error;}cudaStatus = cudaMemcpy(dev_INPUT, INPUT, size * sizeof(unsigned long long), cudaMemcpyHostToDevice);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!");goto Error;}Upload_to_GPU<<<1, size>>>(dev_Number, dev_Degree_of, dev_Elements_that_need_to_stop, dev_INPUT, dev_Max);// Проверка сбоев ядраcudaStatus = cudaGetLastError();if (cudaStatus != cudaSuccess) {fprintf(stderr, "addKernel launch failed: %s\n", cudaGetErrorString(cudaStatus));goto Error;}// Ожидание завершения операций , выполняемых ядромcudaStatus = cudaDeviceSynchronize();if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaDeviceSynchronize returned error code %d after launching addKernel!\n", cudaStatus);goto Error;}// Перемещение данных из памяти GPU в системную памятьcudaStatus = cudaMemcpy(Number, dev_Number, size * sizeof(unsigned long long), cudaMemcpyDeviceToHost);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!");goto Error;}cudaStatus = cudaMemcpy(Degree_of, dev_Degree_of, size * sizeof(unsigned long long), cudaMemcpyDeviceToHost);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!");goto Error;}cudaStatus = cudaMemcpy(Stop, dev_Elements_that_need_to_stop, size * sizeof(bool), cudaMemcpyDeviceToHost);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!");goto Error;}Error://Освобождение памяти GPU от данныхcudaFree(dev_INPUT);cudaFree(dev_Degree_of);cudaFree(dev_Max);cudaFree(dev_Elements_that_need_to_stop);cudaFree(dev_Number);return cudaStatus;}



Идентификатор
__global__  
в .cu файле указывает, что функция выполняется на уровне ядра GPU.
Для работы с cyda, перед вызовом функции, нужно зарезервировать память под массив и перенести элементы в память GPU. Это увеличивает объем кода, но позволяет разгрузить CPU, так как вычисления производятся на GPU.Поэтому ,cuda, дает как минимум возможность разгрузить процессор для выполнения других нагрузок, не использующих cuda.
В случае примера на cuda, задача процессора заключается лишь в загрузке инструкций на GPU и обработке результатов пришедших с GPU; В то время как в коде для CPU, процессор обрабатывает каждый поток. Стоит отметить, что cyda имеет ограничения по количеству запускаемых потоков, поэтому в обоих алгоритмах я взял одинаковое количество потоков, равное 1000. Также, в случае с CPU я использовал переменную
intRunning_thread_counter=0;

чтобы считать количество уже выполненных потоков и дожидаться, пока все потоки не выполнятся.
Тестируемая конфигурация
  • CPU :amd ryzen 5 1400(4core,8thread)
  • ОЗУ:8гбDDR4 2666
  • GPU:Nvidia rtx 2060

  • OS:windows 10 version 2004
  • Cuda:
    • Compute Capability 7.5
    • Threads per Multiprocessor 1024
    • CUDA 11.1.70

  • GPU-Z:version 2.35.0
  • Visual Studio 2017

Сведения о cyda были взяты из GPU-Z

Для тестирования алгоритма я использовал
следующий код на C#
usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;usingSystem.Diagnostics;usingSystem.IO;namespaceConsoleAppTESTSTEPEN_CPU_AND_GPU_{classProgram{staticstringUpload(Int64number,Int64degree_of){stringOUT="";string[]Chord_values=newstring[2];Int64Degree_of=degree_of;Int64Number=number;Chord_values[0]=Number.ToString();Chord_values[1]=Degree_of.ToString();File.WriteAllLines("input.txt",Chord_values);//файлвходныхданныхOUT+="inputnumber:"+Number.ToString()+"\n";OUT+="inputdegreeofnumber:"+Degree_of.ToString()+"\n";DateTimerunning_CPU_application=DateTime.Now;//записатьвремязапускапрограммыProcessproc=Process.Start("ConsoleApplication29.exe");//exeреализацияалгоритманаc++x64использующаяCPUдлявычисленийwhile(!proc.HasExited);//дождатсязавершенияпрограммыDateTimestop_CPU_application=DateTime.Now;//записатьвремяостановкипрограммыstring[]outs=File.ReadAllLines("out.txt");//получитьрезультатыFile.Delete("out.txt");OUT+="CPU:"+"\n";if(outs.Length>0){for(intj=0;j<outs.Length;j++){OUT+=outs[j]+"\n";}}else{OUT+="novalues"+"\n";}OUT+="running_CPU_application:"+running_CPU_application.ToString()+"\n";OUT+="stop_CPU_application:"+stop_CPU_application.ToString()+"\n";OUT+="GPU:"+"\n";//альтернативныедействиядляреализацииалгоритмаkorenXN.exex64использующегодлявычисленийGPUDateTimerunning_GPU_application=DateTime.Now;ProcessprocGPU=Process.Start("korenXN.exe");while(!procGPU.HasExited);DateTimestop_GPU_application=DateTime.Now;string[]outs2=File.ReadAllLines("out.txt");File.Delete("out.txt");if(outs2.Length>0){for(intj=0;j<outs2.Length;j++){OUT+=outs2[j]+"\n";}}else{OUT+="novalues"+"\n";}OUT+="running_GPU_application:"+running_GPU_application.ToString()+"\n";OUT+="stop_GPU_application:"+stop_GPU_application.ToString()+"\n";returnOUT;//возвратитьрезультат}staticvoidMain(){Int64start=36*36;//начальноезначениевходногочислаInt64degree_of_strat=500;//начальноезначениемаксимальнойстепениintsize=20-5;//количествоэлементоввмассивеInt64[]Number=newInt64[size];//массиввходныхчиселInt64[]Degree_of=newInt64[size];//массивмаксимальныхстепенейstring[]outs=newstring[size];//масссиврезультатовfor(intn=0;n<size;n++){if(n%2==0){Number[n]=start*start;}else{Number[n]=start*degree_of_strat;Number[n]-=n+n;}start+=36*36;Degree_of[n]=degree_of_strat;degree_of_strat+=1000;}for(intn=0;n<size;n++){outs[n]=Upload(Number[n],Degree_of[n]);Console.WriteLine(outs[n]);}System.IO.File.WriteAllLines("result.txt",outs);//записатьрезультатывфайлresult.txt}}}


, который создавал файл с исходными данными, затем последовательно запускал exe файлы алгоритмов использующих CPU или GPU и замерял время их работы, затем заносил это время и результаты работы алгоритмов в файл result.txt. Для замера загруженности процессора использовался диспетчер задач windows.
Результаты теста превидены в таблице:

Как видно из таблицы, время выполнения алгоритма на GPU немного больше, чем на CPU.
Однако, отмечу, что вовремя работы алгоритма использующего для вычислений GPU загрузка им CPU, в Диспетчере задач, не превышала 30%, в то время как алгоритм использующий для вычислений CPU, загружал его на 68-85%, что в свою очередь иногда приводило к замедлению других приложений. Также, ниже приведен график, показывающий различие во
времени выполнения (по оси Y)CPU и GPU в зависимости от входного числа(по оси X).
график


Далее я решил провести тестирование при процессоре нагруженном другими приложениями. Процессор был нагружен так, что запущенный в приложение тест, не занимал больше 55% ресурсов процессора. Результаты теста приведены ниже:

График


Как видно из таблицы, в случае с нагруженным CPU, выполнение вычислений на GPU, дает прирост производительности, так как загруженность процессора в 30% укладывается в лимит 55%, а в случае использования CPU для вычислений, его загрузка составляет 68-85% , что тормозит работу алгоритма, если CPU нагружен другими приложениями.

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


Подробнее..

Software ecosystems принципы построения

11.12.2020 14:05:39 | Автор: admin
image
У этой статьи тяжелая судьба. Пару месяцев назад меня попросили написать обзор на предмет построения программных экосистем для разных архитектур. Я поначалу отнекивался да отшучивался в том духе что, экосистема это не биология. Это даже не технология. Это исключительно про деньги. И иногда про политику. Потом собрал волю в кулак, мысли в кучу, cел и написал все буквально за один день. На английском. Затем обзор перевели на китайский и опубликовали. По дороге переводчик существенно улучшил текст и добавил пару интересных мыслей. Потом я решил, что текст может быть небезынтересен аудитории Хабра, а также полезен мне, чтобы ссылаться на него в дальнейшем. И начал ваять русский вариант, вооружившись английским оригиналом и китайским переводом. Это была та еще борьба со специфичными английскими терминами (SW ecosystem ?= программная экосистема, enabling ?= продвижение, application engineer ?= инженер по приложениям) и малопонятными пока иероглифами. В итоге русский текст занял больше времени, чем английский и китайский вместе взятые Так бывает.

Преамбула.

За последние четыре или пять десятилетий мы наблюдали много усилий по выводу на рынок новых процессорных и микроконтроллерных архитектур. Очень немногие из них оказались успешными в долгосрочной перспективе. Два наиболее показательных примера: архитектура х86 для серверов и ПК и ARM для мобильных телефонов и микроконтроллеров. Остальные либо занимают (занимали) небольшую нишу, либо существовали недолго. Безусловно, одним из ключевых компонентов успеха вычислительной архитектуры в целом является её способность удовлетворять наиболее важные запросы клиентов в соответствующем сегменте. Для серверов таким запросом является производительность, для мобильных устройств низкое энергопотребление/длительное время автономной работы. Но еще одним важным фактором является наличие богатой программной экосистемы, которая позволяет эффективно разрабатывать новый софт, а также портировать существующий. Создание экосистемы является очень длительным и дорогостоящим процессом, поскольку необходимо разработать все необходимое прикладное программное обеспечение. Ho только таким образом архитектура может занять целевой сегмент рынка. Однако, как только экосистема разработана, она начинает служить в качестве естественного защитного механизма, предотвращая проникновение конкурирующих архитектур на рынок. В этой статье автор обобщает ключевые выводы из опыта, в применении к задаче построения ARM-экосистемы в серверном сегменте рынка.

Сегодняшний вызов:

Сегодня мы, возможно, наблюдаем самое интересное время на рынке микроэлекторники. В отличие от того, что было 5 лет назад, когда Intel контролировал 95% корпоративного рынка, сегодня у нас больше архитектурных инноваций, чем когда-либо. Первым упомянем чипы серии Ryzen, которыe составляют серьезную конкуренцию Intel. Однако эта инновация не является архитектурной: процессоры AMD используют разработанную экосистему х86. Их преимущество заключается в эффективной имплементации. Другие инновации интересннее, так как они требуют построения новой программной экосистемы.
Революция искусственного интеллекта, начатая и возглавляемая NVidia, кардинально изменила ландшафт серверного рынка. Она создала совершенно новый сегмент, почти исключительно контролируемый его создателем. В основе его платформа параллельного программирования CUDA. CUDA (впервые представленная в 2007 году) является одним из интересных примеров построения экосистемы, мы рассмотрим его более подробно позднее. В свою очередь, Intel пытается представить свой собственный графический процессор (частично основанный на графической системе Gen), чтобы конкурировать в области искусственного интеллекта. Для создания среды программирования Intel представил набор инструментов OneAPI. Это амбициозная концепция универсального метода программирования ускорителей (GPU, AI, FPGA и т.д.). Кроме того, Intel использует для OneAPI открытый исходный код, что делает интерфейс более привлекательным, чем у моделей с закрытым исходным кодом. В концепции OneAPI центральную роль играет Data Parallel C++. Это набор C++ расширений на основе стандарта CYCL. Мне кажется, успех этой интересной попытки будет зависеть от включения данных расширений в будущие стандарты C++. А также от эффективной реализации интеловского GPU. Последним примером является проникновение архитектуры ARM в область искусственного интеллекта, облачных и высокопроизводительных вычислений. Архитектура ARM известна своим успехом на рынках низкого энергопотребления и имеет значительную экосистему ПО. Поэтому попытка проникнуть в более высокие сегменты рынка выглядит естественно. Это самый интересный для нас случай, и мы рассмотрим его более подробно с различных точек зрения.

Технология.

Технологические предпосылки я приводил тут.

Экономика.

Усиление конкуренции Intel со стороны AMD привело к существенной ценовой войне между процессорными гигантами. Это урезает доходы производителей, но облечгает жизнь конечным пользователям. Напротив, в области искусственного интеллекта мы наблюдаем доминирование NVidia, которая диктует цену рынку. И здесь потребители активнее ищут альтернативные системы.

Политика.

В связи с усилением геополитической напряженности Китай и Россия взяли курс на развитие 100% независимой экосистемы. Однако это означает не только независимость аппаратного обеспечения. Не менее важную роль играет софт. Многие приложения должны быть перенесены или заменены для обеспечения истинной независимости. Нижеприведенная картина иллюстрирует ситуацию в России.
image
Политический фактор создает сильный спрос на ресурсы для устранения пробелов в ПО и долгосрочную поддержку для этих усилий. Существует несколько мощных сил, стоящих за созданием экосистемы ARM. Но есть и очень серьезные проблемы. Позвольте мне назвать несколько из них:

Типичная для ранних стадий проблема курицы и яйца:

Некоторые коммерческие приложения с закрытым исходным кодом становятся де-факто стандартом для важных сегментов. Например, SAP-Hana, Oracle для баз данных, VMWare для виртуализации и т.д. Обычно разработчики не заинтересованы в переносе и сопровождении своих продуктов на архитектуре, которая не имеет достаточной доли рынка. С другой стороны, конечные пользователи не покупают оборудование, которое не поддерживает это ПО. Существует несколько способов разорвать этот порочный круг. Первый заплатить (огромные) деньги компаниям-разработчикам ПО за портирование. Второй создать конкурентное решение, которое либо вытеснит продукт с рынка либо вынудить желательное портирование. Третий убедить кого-то из больших кастомеров разработчика надавить на него. Однако ни один путь не является ни простым, ни дешевым.

Legacy Software.

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

На горизонте стоит немало проблем, и для их решения нам следует изучить

Уроки прошлого.

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

В самом начале.

Когда Intel выпускала свой первый 8-битный процессор 8080 в середине 70-х годов, у нее не было особых архитектурных преимуществ по сравнению со своими конкурентами Motorola 6800 или Zilog Z80. Тем не менее Intel впервые осознал важность программного обеспечения для продвижения железа. Она также первым представил собственный компилятор для ускорения, упрощения и удешевления разработки софта. Именно инструменты создали важное конкурентное преимущество для Intel, и позволили ей добиться успеха на ранних этапах. Позже Intel представила MKL для линейной алгебры, VTune для оптимизации программ и многие другие инструменты, которые играли важную роль в формировании х86 экосистемы. Более того, Intel гарантировала совместимость своих инструментов. Позже Intel была осознана важность ОС, которая управляет работой железа. Её тандем с Microsoft (Wintel) стал доминирующей силой на рынке ПК в течение примерно трех десятилетий. Однако с развитием серверного рынка ОС Linux и открытый исходный код в целом, стали очень актуальны. Таким образом, Intel стал главным контрибьютором в ядро Linux, и до сих пор занимает позицию номер 1 (угадаете кто номер 2?). Самое главное Intel всегда строго придерживалcя бинарной совместимости (Backward compatibility) своих платформ. Тот факт, что бинарные файлы, созданные для любого предыдущего поколения архитектуры, работают на более позднем поколении из коробки, и позволил х86 стать доминирующей силой. Создание экосистемы требует времени, но оно того cтоит.

Восход и закат Солнца.

Это единственный контрпример, когда развитая экосистема сыграла против своего создателя. В начале 2000-х Sun Microsystems занимала значительную нишу на рынке серверов с архитектурой SPARC. Компания инициировала и развила экосистему Java. Проблема однако в том, что Java-машина основана на интерпретаторе, а не на компиляторе. Так что, когда обстоятельства заставили Sun открыть Javа, это стало началом конца. Реализовав интерпретатор Java для х86, Intel постепенно вытеснил Sun с серверного рынка. Урок, который можно извлечь интерпретированные языки (столь популярные сегодня) не являются эффективной защитой рыночной доли.

Успех ARM на рынке низкого энергопотребления.

Архитектура АРМ лидирует на рынке сотовых телефонов и планшетов с начала 2000-х годов. Характер его доминирования сильно отличается от х86 в ПК и серверах. ARM это открытое сообщество лицензия стоит относительно недорого, что создает предпосылки для вступления в игру большого числа игроков. А открытая операционная система Android, дополнительно снижает входной барьер. Так же свою роль сыграло то, что ведущие игроки (Samsung, Qualcomm, Huawei и т.д.) осознали необходимость индустриального альянса для создания экосистемы. Это позволило построить ее в очень сжатые сроки.

Попытка корпорации Intel вторгнуться на мобильный рынок.

Примерно в 2007 году Intel (или чуть раньше) осознал потенциал рынка мобильных телефонов и планшетов и предприняла попытку его завоевания. В основе проекта лежали архитектура Atom (х86 с низким энергопотреблением) и бинарная трансляция с ARM на х86 (Houdini). Этот механизм позволяет использовать существующую экосистему программного обеспечения. Приложения были запущены на устройствах Intel в рекордно короткие сроки. Однако против Intel сыграли два факта. Первый неспособность догнать ARM в области энергопотребления. Второе сложность бинарной трансляции из RISC в более сложный набор инструкций CISC. Такая трансляция предполагает значительные накладные расходы, которые могут быть неприемлемы для некоторых классов приложений. Таким образом, можно рассматривать бинарную трансляцию как инструмент входа на рынок, но он вряд ли может служить как долгосрочное решение.

Экосистема CUDA.

Хотя успех Nvidia в сегменте искусственного интеллекта неоспорим, развитие собственной экосистемы CUDA по-прежнему вызывает у меня вопросы. Например, в области высокопроизводительных вычислителений доля приложений, использующих CUDA, по-прежнему остается низкой. Причиной является высокая стоимость портирования и сопровождения (maintenance) таких приложений. Некоторые разработчики портировали (со значительной помощью инженеров NVidia) свои коды, но позже отказались от них из-за стоимости поддержки. Напротив, в сегменте искусственного интеллекта позиция NМidia чрезвычайно сильна. Тем не менее исследователи в основном используют структуры более высокого уровня (TensorFlow, PyTorch и т.д.) и не занимаются непосредственным программированием на CUDA. Это подводит нас к выводу, что правильный выбор уровня абстракции софтовой обвязки имеет большое значение для продвижения железа.

Назад в будущее.

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

Индустриальный альянс.

Самая большая проблема экосистемы ARM в серверном сегменте, в отличие от мобильного, заключается в том, что она все еще очень фрагментирована. Нужен какой-то центр притяжения для согласования усилий. Альянс представляется вполне естественным, учитывая, что доля всех ARM-вендоров на серверном рынке составляет около 1%. У них просто нет причин конкурировать. Задача совместного построения экосистемы должна иметь приоритет над дифференциацией между собой. Был предпринят ряд попыток создания подобного альянса. Можно упомянуть Open Green Computing Consortium (openGCC для программиста трудно придумать более дурацкое название), и наши недавние усилия в рамках ARM Advanced Technology Committee в рамках АПКИТ. Это может быть хорошим началом, однако оба эти альянса являются локальными. Open GCC является китайской организацией, а АПКИТ российской. Индустрия диктует необходимость глобального альянса, который может служить нескольким целям:

Синхронизация и влияние на регулирующие органы.

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

Работа с поставщиками операционных систем и приложений.

Как мы видели из опыта, операционные системы играют центральную роль в создании программной экосистемы. Можно также заметить, что ОС, разрабатываемые поставщиками железа, исторически не были очень успешными (за исключением Apple). Так что работа с поставщиками ОС стратегически очень важна. Частично это происходит сейчас патчи, обновления, появляющиеся в последних версиях ядра Linux, компиляторов и glibc, повышают производительность ARM-систем для самых разнообразных ворклоадов. Но эти усилия все еще очень фрагментированы, каждый поставщик делает это сам по себе, что обычно занимает довольно много времени. Альянс может помочь синхронизировать эти усилия и ускорить принятие обновлений.
Мы также увидели, что разрешение проблемы курицы и яйца на ранних стадиях продвижения архитектуры может быть очень сложным. Альянс сможет предоставить дополнительный вес в переговорах с поставщиками ПО и помочь разделить бремя раннего продвижения между производителями железа.

Система дистрибуции софта

Третьим важным моментом здесь является распространение программного обеспечения. Сейчас серверное программное обеспечение для ARM как правило собирается из исходных кодов, что требует времени и аппаратных ресурсов. В то время как для х86 есть удобная схема на основе RPM, используемая для дистрибуции. Создание чего-то подобного потребует огромной работы с независимыми поставщиками приложений и операционныx систем. И альянс может быть очень полезным для содействия в этом направлении.

Создание эффективного инсрументария.

В прошлом программные инструменты зарекомендовали себя надежным средством развития экосистемы. Поэтому сегодня внимание к ним имеет критическое значение. Один из лучших примеров это корпорация Intel, которая объединила разработчиков программных средств и инженеров по приложениям (application engineers) в одну организацию, как показано ниже. Красным отмечено критическое взаимодействие, зеленым -регулярное, желтым -спорадическое.
image
Это обеспечивает тесное сотрудничество между разработчиками инструментов. Это дает возможность аппликейшн инженерам (AE) использовать лучшие инструменты в отрасли. AE, работающие с одинаковыми ворклоадами обмениватются знаниями друг с другом и предоставляют консолидированную обратную связь архитекторам железа. И тд. И тп. Все это привело к интересным явлениям: инженеры, работающие в такой организации, начали думать не только о своей продукции, но и о широкой экосистемной картине. Они учитывают взаимозависимость между собой, а также влияние на экосистему в целом
Но чтобы создать такую организацию, нужен некий общий язык, который все стороны смогут использовать для общения. В какой-то момент метод анализа микроархитектуры TMAM, стал таковым.
image
ТМАМ обеспечивает способ сбора, интерпретации и использования событий микроархитектуры для профилировки и оптимизации приложений.
image
Наиболее важным преимуществом такого метода является то, что он дает объективную картину того, что происходит в железе, и позволяет улучшать как его, так и софт. Но, возможно, еще более важно то, что он является уникальным средством для создания эффективной организации по продвижению железа.

Вывод.

Построение экосистемы ARM на серверном рынке выглядит перспективно с экономической, политической и технологической точки зрения. Однако, оно происходит на фоне серьезной конкуренции с доминирующей архитектурой x86. К тому же ранние проблемы продвижения архитектуры еще далеки от того, чтобы назвать их решенными. Однако я верю, что в будущем ARM сможет стать серьезной силой на серверном рынке. Создание глобального индустриального альянса и эффективной организации по построению экосистемы представляется очень желательным.
Построение экосистемы ARM на серверном рынке выглядит перспективно с экономической, политической и технологической точки зрения. Однако, оно происходит на фоне серьезной конкуренции с доминирующей архитектурой x86. К тому же ранние проблемы продвижения архитектуры еще далеки от того, чтобы назвать их решенными. Однако я верю, что в будущем ARM сможет стать серьезной силой на серверном рынке. Создание глобального индустриального альянса и эффективной организации по построению экосистемы представляется очень желательным.
Россия видится одной из самых перспективных арен для построения ARM экосистемы со многих точек зрения. Правительственный курс на информационную независимость обеспечивает долгосрочную поддержку этой инициативы. Однако потенциал ARM-экосистемы еще даже полностью не осознан на правительственном уровне. Нужна серьезная работа для того, чтобы наш голос был услышан. Еще одна причина сравнительно большой рынок и разнообразие кастомеров как частных, так и государственных. Например, здесь можно назвать ресурсодобывающих гигантов (Газпром, Лукойл, Роснефть), ведущие банки (Сбер, ВТБ, Aльфа), провайдеров мобильных услуг (MTC, Meгафон, Билайн, Tеле2). Растет осознание необходимости альтернативы существующей инфраструктуре (x86, IBM, Oracle) как с точки зрения безопасности, так и с точки ценообразования.
Ну и последнее по счету, но не по значимости человеческий ресурс. В России много программистов, имеющих опыт решения экосистемных задач и думающих в этих терминах.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru