Государственное образовательное учреждение высшего профессионального образования «Нижегородский государственный университет им. Н.И. Лобачевского» Факультет вычислительной математики и кибернетики Лабораторная работа по курсам «Параллельные численные методы» и «Технологии параллельного программирования» Тема: «Метод Монте-Карло и техника понижения дисперсии для него – метод контрольных переменных» Выполнили: Студенты группы 85М21 Гнатюк Д.В. Збруев Д.А. Нижний Новгород 2011 Оглавление 1. Постановка задачи................................................................................................................... 3 2. Описание алгоритмов ............................................................................................................. 4 2.1. Метод Монте-Карло ........................................................................................................ 4 2.1.1. Обычный алгоритм интегрирования Монте-Карло................................................ 4 2.1.2. Геометрический алгоритм интегрирования Монте-Карло .................................... 4 2.2. 3. 4. Control variates ................................................................................................................. 5 Программные реализации ..................................................................................................... 6 3.1. Реализация на CPU .......................................................................................................... 6 3.2. Реализация на CUDA........................................................................................................ 8 3.3. Реализация на TBB .........................................................................................................10 Вычислительные эксперименты ..........................................................................................12 4.1. CUDA ...............................................................................................................................12 4.2. TBB...................................................................................................................................13 2 1. Постановка задачи Необходимо реализовать вычисление определенного интеграла для многомерной функции методом Монте-Карло, а также технику понижения дисперсии для него – метод контрольных переменных (control variate). Функция должна быть задана в виде обычной функции языка C. Предлагается провести эксперименты с одномерными и пятимерными функциями. Можно выбрать любую функцию, включая ту, для которой решение известно. Аналитическое решение можно использовать для анализа сходимости и погрешности. Кроме того, необходимо провести эксперименты по оценке эффективности реализации. 3 2. Описание алгоритмов 2.1. Метод Монте-Карло 2.1.1. Обычный алгоритм интегрирования Монте-Карло Предположим, требуется вычислить определённый интеграл 𝑏 ∫ 𝑓(𝑥)𝑑𝑥. 𝑎 Рассмотрим случайную величину u, равномерно распределённую на отрезке интегрирования [a,b]. Тогда f(u) так же будет случайной величиной, причём её математическое ожидание выражается как 𝑏 𝑓(𝑢) = ∫ 𝑓(𝑥)ϕ(x)𝑑𝑥, 𝑎 где ϕ(x) - плотность распределения случайной величины u, равная 1 𝑏−𝑎 на участке [a,b]. Таким образом, искомый интеграл выражается как 𝑏 ∫ 𝑓(𝑥)𝑑𝑥 = (𝑏 − 𝑎)Ε𝑓(𝑢). 𝑎 Но математическое ожидание случайной величины f(u) можно легко оценить, смоделировав эту случайную величину и посчитав выборочное среднее. Итак, бросаем N точек, равномерно распределённых на [a,b], для каждой точки ui вычисляем f(ui). Затем вычисляем выборочное среднее: 1 𝑁 𝑁 ∑ 𝑓(𝑢𝑖 ). 𝑖=1 В итоге получаем оценку интеграла: 𝑏 ∫𝑎 𝑓(𝑥)𝑑𝑥 ≈ 𝑏−𝑎 𝑁 ∑𝑁𝑖=1 𝑓(𝑢𝑖 ). Точность оценки зависит только от количества точек N. 2.1.2. Геометрический алгоритм интегрирования Монте-Карло Для определения площади под графиком функции (вычисления определенного интеграла функции) можно использовать следующий стохастический алгоритм: 1) ограничим функцию прямоугольником (n-мерным параллелепипедом в случае многих измерений), площадь которого Spar можно легко вычислить; 2) «набросаем» в этот прямоугольник (параллелепипед) некоторое количество точек (N штук), координаты которых будем выбирать случайным образом; 3) определим число точек (K штук), которые попадут под график функции; 4) площадь области, ограниченной функцией и осями координат, S даётся выражением 𝑆 = 𝐾 𝑆𝑝𝑎𝑟 𝑁. 4 2.2. Control variates Предположим X это случайная величина и мы хотим оценить Мы можем оценить A сгенерировав L независимых значений X и посчитав Таким образом ошибка будет составлять Таким образом, количество значений, необходимое для достижения заданной точности обратно пропорционально дисперсии. Control variate - это такая случайная величина W(X), что B=E[W(X)] может быть легко вычислена. Если W(X) коррелирует с V(X) тогда случайная величина может иметь меньшую дисперсию, чем V(X). Теперь мы можем оценить A более точно Можно выбрать такое α, чтобы минимизировать дисперсию Z Оптимальным 𝛼 будет Таким образом, качество метода напрямую зависит от корреляции V и W. На практике мы не можем сразу знать оптимальное α, но можем его оценить по данным, полученным в результате работы Монте-Карло. По значениям случайной величины Xk можно вычислить Vk=V(Xk) и Wk=W(Xk), тогда 5 3. Программные реализации 3.1. Реализация на CPU Был реализован классический метод Монте-Карло и метод контрольных переменных на примере вычисления определенного интеграла двумерной функции. Аргументами методов являются пределы интегрирования и количество значений случайной величины. #define PI 3.14159265358979323846264338327950288419716939937510 #define a 124367 #define c 57634 #define m 2147483648 unsigned int x=1234; float rnd() { x=(a*x+c)%m; return ((float) x)/m; //return ((float)rand())/RAND_MAX; } double f(double x, double y) { //функция (3) из лабораторной работы №2 «Вычисление определенного интеграла» return (exp( sin(PI * x) * cos(PI * y) ) + 1.0) / 256.0; } //Monte Carlo without control variates float MC(int x1, int y1, int x2, int y2, int nsamples) { int x=1234; int good_points = 0; float denom = (x2-x1)*(y2-y1); float rnd_x = 0, rnd_y = 0, rnd_z = 0; float Fxy = 0, Fxy_max = 0; float result = 0; float accuracy = 0.1f; for (float i = x1; i < x2; i += accuracy) { for (float j = y1; j < y2; j += accuracy) { Fxy = f(i,j); if (Fxy > Fxy_max) Fxy_max = Fxy; } } for (int i = { rnd_x rnd_y rnd_z 0; i < nsamples; i++) = rnd() * (x2-x1)+x1; = rnd() * (y2-y1)+y1; = rnd() * Fxy_max; Fxy = f(rnd_x, rnd_y); if (rnd_z < Fxy) good_points++; } result = denom * Fxy_max * good_points / nsamples; return result; } //Monte Carlo with control variates float MCCV(int x1, int y1, int x2, int y2, int nsamples) 6 { float *valV = new float[nsamples], *valW = new float[nsamples]; float float float float float denom = (x2-x1)*(y2-y1); rnd_x = 0, rnd_y = 0; Fxy = 0; result = 0.0f; corVW=0.0f, meanV=0.0; //just W(X1, X2) = (X1 + X2), Mean(W) = (X1max-X1min)/2 + (X2max-X2min)/2 //float meanW = (x2-x1)/2.0f + (y2-y1)/2.0f, sigmaW2=0.0f; float meanW = 0.5f*(x1+x2+y2+y1), sigmaW2=0.0f; for (int i = { rnd_x rnd_y Fxy = 0; i < nsamples; i++) = rnd() * (x2-x1)+x1; = rnd() * (y2-y1)+y1; f(rnd_x, rnd_y); valV[i] = Fxy; valW[i] = rnd_x + rnd_y; sigmaW2 += (valW[i] - meanW)*(valW[i] - meanW); meanV += valV[i]; } meanV /= nsamples; for (int i = 0; i < nsamples; i++) corVW += (valV[i] - meanV) * (valW[i] - meanW); float alpha = corVW / sigmaW2; for (int i = 0; i < nsamples; i++) { result += (valV[i] - alpha*(valW[i] - meanW)) * denom; } result /= nsamples; return result; } 7 3.2. Реализация на CUDA Идея распараллеливания заключается в том, чтобы разделить исходную последовательность значений случайной величины на куски заданного размера и распределить их между потоками. Каждый поток автономно вычисляет свою оценку и записывает результат в выходной массив. После синхронизации эти значения обрабатываются и находится результирующая оценка интеграла. Важным моментом является параллельная генерация случайных величин потоками, которая являлась основным местом для оптимизации алгоритма и получения ускорения. #define PI 3.14159265358979323846264338327950288419716939937510 #define a 124367 #define c 57634 #define m 2147483648 __device__ float rnd2(unsigned int idx) { unsigned int x=1234; int aa=a; int res = 1; int t = idx+1; while (t) { if (t & 1) res *= aa; t >>= 1; } x = (res*x)%m; return ((float) x)/m; } __device__ float f_device(float x, float y) { return (exp( sin(PI * x) * cos(PI * y) ) + 1.0) / 256.0; } __global__ void MCCVKernel(int x1, int y1, int x2, int y2, int chunkSize, float *S) { long ind = blockIdx.x * blockDim.x + threadIdx.x; __shared__ float valV[CHUNKSIZE]; __shared__ float valW[CHUNKSIZE]; float float float float float denom = (x2-x1)*(y2-y1); rnd_x = 0, rnd_y = 0; Fxy = 0; result = 0.0f; corVW=0.0f, meanV=0.0; //just W(X1, X2) = (X1 + X2), Mean(W) = (X1max-X1min)/2 + (X2max-X2min)/2 //float meanW = (x2-x1)/2.0f + (y2-y1)/2.0f, sigmaW2=0.0f; float meanW = 0.5f*(x1+x2+y2+y1), sigmaW2=0.0f; unsigned int x=1234;/////, aN=0, nsamples = blockDim.x * CHUNKSIZE; unsigned int num = ind*chunkSize*2; for (int i = { rnd_x rnd_y Fxy = 0; i < chunkSize; i++) = rnd2(ind*chunkSize+2*i) * (x2-x1)+x1; = rnd2(ind*chunkSize+2*i+1) * (y2-y1)+y1; f_device(rnd_x, rnd_y); valV[i] = Fxy; valW[i] = rnd_x + rnd_y; sigmaW2 += (valW[i] - meanW)*(valW[i] - meanW); 8 meanV += valV[i]; } meanV /= chunkSize; for (int i = 0; i < chunkSize; i++) corVW += (valV[i] - meanV) * (valW[i] - meanW); float alpha = corVW / sigmaW2; for (int i = 0; i < chunkSize; i++) { result += (valV[i] - alpha*(valW[i] - meanW)) * denom; } result /= chunkSize; S[ind] = result; } int MCCVCuda(int x1, int y1, int x2, int y2, int nsamples, int chunkSize, float *output, LARGE_INTEGER &time) { float *S; int chunksCount = nsamples / chunkSize; cudaMalloc(&S, chunksCount * sizeof(float)); MCCVKernel<<<8, chunksCount/8>>>(x1, y1, x2, y2, chunkSize, S); cudaThreadSynchronize(); QueryPerformanceCounter(&time); cudaMemcpy(output, S, chunksCount*sizeof(float), cudaMemcpyDeviceToHost); cudaFree(S); return 0; } 9 3.3. Реализация на TBB Идея распараллеливания с использованием TBB совпадает с предыдущей реализацией на CUDA и заключается в том, чтобы разделить исходную последовательность значений случайной величины на куски заданного размера и распределить их между потоками. В имеющемся инструментарии TBB для этих целей идеально подходит parallel_for. // Класс-функтор TBB class integrationClass { private: int x1; int y1; int x2; int y2; float Fxy_max; int chunkSize; float *resultS; int denom; public: // Конструктор integrationClass (int tx1, int ty1, int tx2, int ty2, int tchunkSize, float *tres) { x1=tx1; y1=ty1; x2=tx2; y2=ty2; chunkSize=tchunkSize; denom=(x2-x1)*(y2-y1); resultS=tres; } // Оператор () выполняется над диапазоном из пространства итераций void operator()(const blocked_range<int> &range) const { float rnd_x=0, rnd_y=0; float Fxy=0; for (int i = range.begin(); i != range.end(); i++) { float valW[CHUNK_SIZE], valV[CHUNK_SIZE]; int ind = i; float result = 0.0f; float corVW=0.0f, meanV=0.0; //just W(X1, X2) = (X1 + X2), Mean(W) = (X1max-X1min)/2 + (X2maxX2min)/2 //float meanW = (x2-x1)/2.0f + (y2-y1)/2.0f, sigmaW2=0.0f; float meanW = 0.5f*(x1+x2+y2+y1), sigmaW2=0.0f; unsigned int x=1234;/////, aN=0, nsamples = blockDim.x * CHUNKSIZE; unsigned int num = ind*chunkSize*2; for (int i2 = 0; i2 < CHUNK_SIZE; i2++) { rnd_x = rnd2(ind*chunkSize+2*i2) * (x2-x1)+x1; rnd_y = rnd2(ind*chunkSize+2*i2+1) * (y2-y1)+y1; //rnd_x = rnd(x) * (x2-x1)+x1; //rnd_y = rnd(x) * (y2-y1)+y1; Fxy = f(rnd_x, rnd_y); valV[i2] = Fxy; valW[i2] = rnd_x + rnd_y; ////meanWExper += valW[i]; 10 sigmaW2 += (valW[i2] - meanW)*(valW[i2] - meanW); meanV += valV[i2]; } meanV /= chunkSize; for (int i2 = 0; i2 < chunkSize; i2++) corVW += (valV[i2] - meanV) * (valW[i2] - meanW); float alpha = corVW / sigmaW2; for (int i2 = 0; i2 < chunkSize; i2++) { result += (valV[i2] - alpha*(valW[i2] - meanW)) * denom; } result /= chunkSize; resultS[ind] = result; } } }; //вызов parallel_for int main(int argc, char *argv[]) { … task_scheduler_init init; int chunkSize = CHUNK_SIZE; int chunkCount = nsamples / chunkSize; float *resultTBB = new float[chunkCount]; parallel_for(blocked_range<int>(0, chunkCount), integrationClass(x1, y1, x2, y2, chunkSize, resultTBB)); result = 0; for (int i = 0; i < chunkCount; i++) result += resultTBB[i]; result /= chunkCount; … return 0; } 11 4. Вычислительные эксперименты 4.1. CUDA Тестовая конфигурация: Intel Core2 Duo 2.27 GHz, 3Gb RAM, NVIDIA GeForce 9200M GS. 0.18 0.16 Время, сек 0.14 0.12 MC simple 0.1 0.08 MCCV serial 0.06 0.04 MCCV CUDA 8 blocks, chunksCount/8 threads 0.02 0 Количество точек Рис.1 Время работы алгоритмов 3.5 3 Ускорение 2.5 2 1.5 1 0.5 0 Количество точек Рис. 2 Оценка эффективности Реализация на GPU оправдывает затраты на реализацию и показывает достаточно хорошее ускорение. Стоит особо отметить, что в специфике данной задаче большую часть времени работы CUDA-нитей составляет именно генерация случайных чисел, а не непосредственно вычисления. Мы опробовали различные способы многопоточной генерации случайных чисел – и leapfrog и разделение выборки с последовательным пролистыванием до нужного начального значения, они дают замедление работы по сравнению с версией для CPU! На наш взгляд, самым оптимальным вариантом была бы схема, при которой сначала на CPU вычисляются 12 инициализации генератора СЧ для разных потоков, а затем этот массив передается на GPU; эта версия позволила бы получить еще большее ускорение по сравнению с текущей реализацией. 4.2. TBB Тестовая конфигурация: Intel Core i7 2.8 GHz, 3Gb RAM. 0.12 Время, сек 0.1 0.08 0.06 MC simple 0.04 MCCV serial MCCV TBB 0.02 0 Количество точек Рис.1 Время работы алгоритмов 3 Ускорение 2.5 2 1.5 1 0.5 0 Количество точек Рис. 2 Оценка эффективности TBB версия показывает достаточно хороший показатель эффективность/затраты, т.к. реализация алгоритма с использованием данной технологии оказалась гораздо проще, чем, например, на CUDA. 13