Россия, г. Москва |
Самостоятельная работа 1: Оптимизация вычислительно трудоемкого программного модуля для архитектуры Intel Xeon Phi. Метод Монте-Карло
Оптимизация 2: отказ от использования дублирующих массивов
Основной недостаток текущей версии программы состоит в том, что мы не можем использовать все возможности сопроцессора – число потоков, на которых мы можем запустить программу, ограничено объемом памяти сопроцессора. И если при работе на CPU использование дублирующих массивов – вполне нормальная и часто применяемая практика, то при переходе на Intel Xeon Phi эта техника не всегда себя оправдывает.
В нашем случае дублирование выполнено для двух типов результатов: общей фотонной карты траекторий и фотонных карт для каждого из детекторов. Объем операций доступа к этим массивам существенно различен. Общая фотонная карта траекторий обновляется каждым фотоном, в то время как фотонные карты детекторов обновляются реже, так как далеко не каждый фотон попадает в тот или иной детектор. С другой стороны, суммарный размер фотонных карт для всех детекторов на порядок превосходит размер общей фотонной карты (в примере это 40 и 4 МБ соответственно).
Отказ от дублирования общей фотонной карты с одной стороны приводит к существенным затратам на синхронизацию, а с другой – выигрыш по памяти в этом случае не так велик.
Таким образом, наиболее эффективным выглядит отказ от дублирующих массивов только для фотонных карт детекторов. В этом случае существенно уменьшается размер дополнительно используемой памяти, а затраты на синхронизацию будут не так велики. Вероятность, что в одно и то же время фотоны из разных потоков попадут в детекторы, невелика.
Заметим, что в данной версии мы отказываемся от дублирования еще одного массива – массива сигналов на детекторах (output->weightInDetector). Целесообразность такого решения предлагается оценить слушателю в рамках дополнительных заданий.
Для реализации предложенной идеи прежде всего следует избавиться от дублирования данных. Основные изменения коснутся функции параллельного запуска процесса моделирования LaunchOMP() (./Opt2/xmcmlLauncher/launcher_omp.cpp):
void LaunchOMP(InputInfo* input, OutputInfo* output, MCG59* randomGenerator, int numThreads) { … #pragma omp parallel { int threadId = omp_get_thread_num(); InitThreadOutput(&(threadOutputs[threadId]), output); PhotonTrjectory trajectory; for (uint64 i = threadId; i < input->numberOfPhotons; i += numThreads) { ComputePhoton(specularReflectance, input, &(threadOutputs[threadId]), &(randomGenerator[threadId]), &trajectory); } } output->specularReflectance = specularReflectance; for (int i = 0; i < numThreads; ++i) { for (int j = 0; j < output->gridSize; ++j) { output->commonTrajectory[j] += threadOutputs[i].commonTrajectory[j]; } } for (int i = 0; i < numThreads; ++i) { FreeThreadOutput(threadOutputs + i); } delete[] threadOutputs; }
Финальное суммирование осталось только для общей фотонной карты, вместо функций InitOutput() и FreeOutput() используются (./Opt2/xmcmlLauncher/launcher_omp.cpp):
void InitThreadOutput(OutputInfo* dst, OutputInfo* src) { dst->gridSize = src->gridSize; dst->detectorTrajectory = src->detectorTrajectory; dst->numberOfDetectors = src->numberOfDetectors; dst->numberOfPhotons = src->numberOfPhotons; dst->specularReflectance = src->specularReflectance; dst->weightInDetector = src->weightInDetector; dst->commonTrajectory = new uint64[dst->gridSize]; memset(dst->commonTrajectory, 0, dst->gridSize*sizeof(uint64)); } void FreeThreadOutput(OutputInfo* output) { if (output->commonTrajectory != NULL) { delete[] output->commonTrajectory; } }
Избавившись от дублирования, необходимо позаботиться о синхронизации. Для этого прежде нужно изменить функцию UpdateDetectorTrajectory() (./Opt2/xmcml/mcml_kernel.cpp):
void UpdateDetectorTrajectory(OutputInfo* output, Area* area, PhotonTrjectory* trajectory, int detectorId) { int index; int ny = area->partitionNumber.y; int nz = area->partitionNumber.z; uint64* detectorTrajectory = output->detectorTrajectory[detectorId].trajectory; for (int i = 0; i < trajectory->size; ++i) { index = trajectory->x[i]*ny*nz + trajectory->y[i]*nz + trajectory->z[i]; #pragma omp atomic ++(detectorTrajectory[index]); } #pragma omp atomic ++(output->detectorTrajectory[detectorId]. numberOfPhotons); }
Также изменению подвергнется функция UpdateWeightInDetector() (./Opt2/xmcml/mcml_kernel.cpp):
void UpdateWeightInDetector(OutputInfo* output, double photonWeight, int detectorId) { #pragma omp atomic output->weightInDetector[detectorId] += photonWeight; }
Время работы программы на центральном процессоре после применения оптимизации практически не изменилось. Время работы программы на сопроцессоре в 120 потоков уменьшилось до 36 секунд. Более того, появилась возможность запуска программы в 240 потоков. Однако время моделирования в этом случае составило 40 секунд. Итого, данная оптимизация позволила сократить время вычислений на сопроцессоре еще на 35%.