- •Курс лекций
- •Оглавление
- •1. Архитектура и принципы работы обычных эвм с центральным процессором (cpu) 9
- •2. Методы повышения производительности традиционных эвм 27
- •3. Типы архитектур высокопроизводительных вычислительных систем 45
- •4. Потоковые параллельные вычисления для физического моделирования 62
- •5. Применение графических процессоров на примерах сложения матриц и решения дифференциальных уравнений 82
- •6. Молекулярная динамика на графическом процессоре 100
- •7. Высокоскоростное моделирование систем с дальнодействием 125
- •8. Восстановление потенциалов межчастичных взаимодействий по температурной зависимости периода решетки методами высокоскоростного мдм на графических процессорах 145
- •9. Базовые особенности программирования графических процессоров шейдерной модели 4.0 160
- •Введение
- •1.Архитектура и принципы работы обычных эвм с центральным процессором (cpu)
- •1.1.Структура традиционной эвм
- •1.2.Организация работы эвм
- •1.3.Иерархия памяти компьютера
- •1.4. Выполнение команд
- •1.5.Требования к коммуникационным линиям
- •1.6.Устройства ввода-вывода
- •2.Методы повышения производительности традиционных эвм
- •2.1. Распараллеливание расчетов
- •2.2.Конвейерная обработка данных и команд
- •2.3.Высокопроизводительные процессоры
- •2.3.1.Суперскалярные процессоры
- •2.3.2.Процессоры risc с сокращенным набором команд
- •2.3.3.Процессоры со сверхдлинным командным словом
- •2.3.4.Векторные процессоры
- •2.3.5.Процессоры для параллельных компьютеров
- •2.3.6.Процессоры с многопоточной архитектурой
- •2.3.7.Технология Hyper-Threading
- •2.4.Требования к памяти высокопроизводительных эвм
- •2.5.Коммуникационная сеть высокопроизводительных эвм
- •2.5.1.Статические и динамические топологии и маршрутизация коммуникационных систем
- •2.5.2.Многокаскадные сети и методы коммутации
- •2.6.Классификация архитектур параллельных компьютеров
- •3.Типы архитектур высокопроизводительных вычислительных систем
- •3.1.Simd архитектура (с разделяемой и распределенной памятью)
- •3.2. Mimd архитектура с разделяемой и распределенной памятью
- •3.3. Комбинированные системы
- •3.4. Мультипроцессорные и мультикомпьютерные системы
- •3.5.Кластеры пэвм и рабочих станций
- •3.6.Особенности параллельного программирования
- •4.Потоковые параллельные вычисления для физического моделирования
- •4.1.Общие принципы распараллеливания расчётов
- •4.2.Обмен данными между процессором и памятью
- •4.3.Графические процессоры как вычислительные системы для поточно-параллельных расчётов
- •4.3.1.Вычислительные возможности центральных процессоров общего назначения и графических процессоров
- •4.3.2.Графический конвейер
- •4.3.3.История программируемости графических процессоров
- •4.3.4.Требования к алгоритмам для gpu, поддерживающих шейдерную модель 3.0
- •4.3.5.Возможности gpu в рамках шейдерной модели 3.0 и взаимодействие gpu с памятью
- •4.3.6.Проблема одинарной точности
- •4.4.Средства программирования графических процессоров
- •4.4.1.Общая структура программы для физического моделирования на графическом процессоре
- •4.4.2.Необходимое программное обеспечение
- •4.5.Области использования графических процессоров
- •5.Применение графических процессоров на примерах сложения матриц и решения дифференциальных уравнений
- •5.1.Распараллеливание независимых вычислений
- •5.2.Используемый графический процессор
- •5.3.Представление данных для графического процессора
- •5.4.Программирование вычислительного ядра
- •5.5.Взаимодействие центрального и графического процессоров
- •5.5.1.Функции центрального процессора
- •5.5.2.Пример программы
- •6.Молекулярная динамика на графическом процессоре
- •6.1.Принципы моделирования ионных кристаллов методом молекулярной динамики
- •6.2.Программирование графического процессора для расчёта действующих на ионы результирующих сил
- •6.2.1.Исходные данные
- •6.2.2.Представление исходных данных для gpu
- •6.2.3.Алгоритм расчёта результирующих сил с использованием графического процессора
- •6.2.4.Шейдер для расчёта результирующей силы
- •6.3.Исполнение шейдера из программы мд-моделирования на c#
- •6.3.1.Этапы алгоритма моделирования, исполняемые на cpu
- •6.3.2.Процедуры на c#, обеспечивающие работу с графическим процессором
- •6.4.Постановка граничных условий и стабилизация макросостояния молекулярно-динамической системы
- •6.4.1.Компенсация импульса и момента импульса
- •6.4.2.Стабилизация температуры
- •7.Высокоскоростное моделирование систем с дальнодействием
- •7.1.Актуальность моделирования
- •7.2.Высокоскоростные алгоритмы моделирования систем с дальнодействующими силами
- •7.3.Методика высокоскоростного молекулярно-динамического моделирования диоксида урана
- •7.4.Экспериментальные результаты и их обсуждение
- •7.5.Анализ зависимостей среднего квадрата смещений ионов кислорода от времени
- •8.Восстановление потенциалов межчастичных взаимодействий по температурной зависимости периода решетки методами высокоскоростного мдм на графических процессорах
- •8.1.Задача восстановления потенциалов межчастичных взаимодействий в кристаллах
- •8.2.Исходные данные и метод восстановления потенциалов
- •8.3.Модель и детали реализации
- •9.Базовые особенности программирования графических процессоров шейдерной модели 4.0
- •9.1.Предпосылки появления новой шейдерной модели
- •9.2.Архитектура gpu шейдерной модели 4.0. Преимущества этой модели
- •9.2.1.Иерархия вычислительных блоков и памяти в шейдерной модели 4.0
- •9.2.2.Конвейерная обработка данных на gpu sm4
- •9.2.3.Логическая структура вычислений на gpu sm4
- •9.2.4.Преимущества gpu шейдерной модели 4.0
- •9.3.Средства высокоуровневого программирования gpu шейдерной модели 4.0
- •9.3.1.Совместимость с шейдерной моделью 3.0
- •9.3.2.Специальные средства программирования gpu sm4. Cuda
- •9.3.3.Средства для написания и компиляции программ на cuda
- •9.3.4.Структура программы на cuda
- •9.4.Перемножение матриц на cuda
- •9.4.1.Алгоритм перемножения матриц
- •9.4.2.Процедура перемножения матриц на gpu sm4
- •9.4.3.Вызов процедуры перемножения матриц из программы на c
- •9.5.Молекулярная динамика на cuda
- •9.5.1.Алгоритм с использованием разделяемой памяти
- •9.5.2.Расчёт сил на gpu с использованием 3-го закона Ньютона
- •Библиографический список
- •Приложение 1 Операторы и функции языка hlsl, использованные в курсе лекций п.1.2. Типы данных
3.6.Особенности параллельного программирования
При написании программы под параллельную обработку возникает ряд проблем, которые, мягко говоря, неочевидны. Далее описаны возможные варианты уменьшения такого рода ошибок.
Итак, ваша программа работает в обычном режиме, вдруг она вылетает с ошибкой, что произошло несвоевременное обращение к оперативной памяти по какому-то невоспроизводимому адресу. А в чем же собственно проблема? Для этого представим, что наша программа обрабатывает много разных величин, по большому количеству формул, но, мало того, обрабатываемые величины связаны между собой. При классическом подходе к параллельному программированию всю программу разложить на несколько процессоров мы должны вручную. Очень показателен рис. 3.11.
Рис.3.11. Распараллеливание одного процесса на несколько потоков [10]
На рисунке все это выглядит очень просто. На самом деле материнский поток при помощи различных вызовов должен сформировать несколько новых потоков (например, в Unix-системах после порождения новых потоков основной процесс приостанавливается ОС, а в ОС возникает два объекта – новый поток, запущенный по команде материнского, и последующий, продолжающий работу главного, но не являющийся собственно процессом). Вместо
…
Выполнить Действие1();
Выполнить Действие2();
…
Мы получим:
…
Запустить Поток (Действие1);
Запустить Поток (Действие2);
…
Т.е. обсчет объектов происходит одновременно. На самом деле, чаще встречается конструкция Запустить Поток (действие для Объекта1); Запустить Поток (действие для Объекта2), т.к. проделывается много однотипных операций над несколькими объектами.
Но такая организация может свести на нет все преимущества параллельного кода. Это связано с тем, что запуск нового потока и переключение между потоками (если процессоры не могут обработать все потоки одновременно) очень дорогие операции. Тогда пользуются следующим решением. Запускают необходимые (именно необходимые, тщательно подобранные по количеству, чтобы не было лишних переключений) потоки заранее, а после, главный поток в указанных местах передает текущее задание этим потокам. В результате получаем код вида:
…
Запустить Поток(Поток1);
Запустить Поток(Поток2);
…
Отправить на Обработку Потоку(Поток1, Действие над Объектом1);
Отправить на Обработку Потоку(Поток2, Действие над Объектом2);
…
В такой ситуации уже на этапе разработки нужно оперировать очень громоздкими и сложными конструкциями. Во-первых, их очень сложно написать. Во-вторых, их еще сложнее отладить.
На помощь может прийти такой компилятор как OpenMP. Это достаточно универсальное средство. OpenMP не привязывается к особенностям ОС. В случае непонимания другими компиляторами его можно просто игнорировать. Недостатком является очень жесткая модель программирования. В основе OpenMP лежит идея использования специальных компиляторов, ориентированных на параллельное программирование. В коде программы вставляются специальные метки, которые указывают какую часть кода выполнять последовательно, а какую параллельно. В итоге код программы приобретает следующий вид (рис. 3.12):
…
#Выполнить Код Параллельно
Действие1;
…
Рис.3.12. Пример работы программы [10]
Но OpenMP не поможет при таких проблемах, как, например, балансировка загрузки потоков. Вполне возможны ситуации, когда два из трех потоков справятся со своей задачей гораздо раньше, чем третий, и будут простаивать, ожидая его результаты. Вот и получается, что если 80% кода можно распараллелить, а 20% - нет. То получить прирост в производительности при подключении второго ядра более чем в 40% не получится. И это только процессор, а если взять такие ресурсы как оперативная память, которые вообще разделить нельзя, то ускорить программу относительно одноядерного процессора на двуядерном в два раза вряд ли удастся.
Возвращаясь от аппаратной части к программной, обратим внимание еще на один момент. Допустим, у нас есть одна величина, которую используют два потока. Первый поток изменяет ее, но в этот же момент в другом потоке она тоже должна быть изменена. Что же получим на выходе? Все зависит от того, какой из потоков запишет в память последним «верное» значение. А представим себе, что один из потоков удалил нашу злосчастную величину, сократив на нее что-нибудь другое (или сделав любую другую операцию, приводящую к стиранию объекта из памяти). Как в такой ситуации должен вести себя другой поток, который объект обсчитал и хочет внести изменения, а вносить некуда, объект удален. Конечно же, громко заявить об ошибке, попутно убив всю программу.
Разрешить такого рода недоразумения помогают объекты синхронизации. Они позволяют временно заблокировать изменение одного из объектов, которые могут быть использованы двумя потоками одновременно (рис. 3.13). Т.е. отдаем объект в пользование одному из потоков, а всех остальных претендентов на него ставим в очередь. Если этого не сделать, то, как было сказано ранее, программа может обрушиться. Причем обрушиться она может не в момент конфликта, а через некоторое время, когда «неудачный» код уже не отловить. Причем смерть программы может быть каждый раз в другом месте и в другое время.
Рис. 3.13. Взаимодействие потоков [10]
Как мы выяснили в предыдущем абзаце, мало объектов синхронизации – программа сломается. Но кто бы мог подумать, что большое количество объектов синхронизации тоже приведет к краху нашей бедной программы? А связано это вот с чем. Пусть у нас есть какой-нибудь объект, который хотят заполучить все потоки. Т.к. всем потокам сразу принадлежать объект не может, то он отдается одному из них. Остальные терпеливо выстраиваются в очередь. И что же тогда останется от параллельности в таком коде? Тогда мы выдаем блокировку только на время, в течение которого будет происходить расчет. Вот мы и подошли к очередной проблеме. Нам необходимо изменить состояние двух объектов. Нам удалось просчитать как оно изменится, нам даже удалось изменить первый объект. Но при подходе ко второму объекту программа зависла. Почему? Потому что второй объект заблокирован, потому что он должен внести изменения в первый объект. Блокировку на себя он не отдаст, пока не изменит тот самый первый объект, обрабатываемый основным кодом. Вот так и будет стоять программа, потому что заблокированы все необходимые объекты. Или вот подобная ситуация. Первый поток заблокировал объект, обработал, и забыл снять блокировку. После этого другому потоку понадобилось проверить состояние объекта, он натыкается на блокировку и отказывается работать дальше. Поэтому важно помнить, что никогда не стоит пытаться обладать двумя объектами одновременно. А также, очень важно всегда проверять, что все однажды взятые объекты своевременно разблокированы.
Одной из проблем при отладке параллельного кода, является то, что один шаг процесса может сопровождаться десятками шагов другого процесса. Ситуации, возникающие в таких шагах зачастую уникальны. Они связаны со случайным совпадением в слабосвязанных между собой потоков. Такие проблемы могут больше никогда не появится в программе. И, как это бывает во всех измерениях, наличие наблюдателей (отладочных средств) вносит коррективы в результат, т.к. немного изменяет внешнюю среду.
Несмотря на все проблемы, связанные с параллельным программированием, отказаться от многоядерных процессоров не удастся. Тем более, что все производители процессоров заваливают нас недорогими продуктами. Скоро параллельное программирование станет таким же обычным, каким сейчас стало непараллельное.