Relatório Trabalho 1 Programação Paralela Gustavo Rissetti 1 Rodrigo Exterckötter Tjäder 1 1 Acadêmicos do Curso de Ciência da Computação Universidade Federal de Santa Maria (UFSM) {rissetti,tjader@inf.ufsm.br 1. Introdução Como trabalho parcial da disciplina de Programação Paralela ELC139, foi proposto um trabalho relacionado à paralelização de um método bastante conhecido no meio cientifico, o método de Monte Carlo. O termo método de Monte Carlo refere-se a uma abordagem para calcular soluções aproximadas para problemas de diversas áreas, tendo como principal característica o emprego de números aleatórios, ou pseudo-aleatórios, para representar valores de variáveis envolvidas em algum fenômeno a ser estudado ou acompanhado. A aplicação escolhida para o presente trabalho utiliza o método de Monte Carlo para simular incêndios em florestas, utilizando um modelo simples em que o fogo se propaga de uma árvore para outra com uma dada probabilidade. A cada execução da aplicação, realizamse vários experimentos com diferentes probabilidades de propagação do fogo. Na saída, temse o percentual de árvores queimadas para cada probabilidade de propagação considerada. 2. Objetivos e metodologia O objetivo principal deste trabalho consiste em analisar o código sequencial do programa fornecido e buscar regiões em que seja possibilitada a aplicação de uma paralelização, visando obter um maior rendimento na execução do programa. Na versão sequencial do código é possível observar que são feitos diversos experimentos usando alguns laços de repetição, sendo possível, a partir de uma análise, paralelizar a execução desses experimentos dividindo em diferentes threads porções das operações a serem executadas dentro dos laços de repetição. Para a concretização desse trabalho foram usadas duas abordagens de paralelização, uma utilizando o OpenMP e a outra utilizando os recursos do MPI. Na versão que foi paralelizada com o OpenMP, foi abordada a paralelização do primeiro laço de repetição. Porém com esta abordagem não foram obtidos resultados muito significativos, como pode ser visto na seção 3, pois com essa divisão em threads, algumas threads ficam com muito pouco trabalho para fazer, no caso de probabilidades baixas, e outras ficam com muito trabalho para fazer, no caso de probabilidades altas. Isso acarreta no atraso da execução da aplicação, pois as threads responsáveis por calcular as probabilidades mais baixas são executadas quase que imediatamente, enquanto as demais demoram a terminar sua execução. O trecho do código onde foi aplicada essa abordagem de paralelização pode ser visto abaixo.
... try { Forest* forest; Random rand; prob_step = (prob_max prob_min)/(double)(n_probs 1); printf( Probabilidade,PercentualQueimado\n ); // Indicação de que a partir daqui o código será paralelizado. #pragma omp parallel shared(forest_size,n_trials) private(it,ip,percent_burned,prob_spread,forest) { // Cada thread terá sua própria floresta. forest = new Forest(forest_size); prob_spread = new double[n_probs]; percent_burned = new double[n_probs]; // Para cada probabilidade, calcula o percentual de árvores queimadas. // Paralelizando o primeiro laço de repetição. #pragma omp for schedule (dynamic) for (ip = 0; ip < n_probs; ip++){ prob_spread[ip] = prob_min + (double) ip * prob_step; percent_burned[ip] = 0.0; rand.setseed(base_seed+ip); // Nova sequência de números aleatórios // Executa vários experimentos for (it = 0; it < n_trials; it++){ // Queima floresta até o fogo apagar forest >burnuntilout(forest >centraltree(), prob_spread[ip], rand); percent_burned[ip] += forest >getpercentburned(); // Calcula média dos percentuais de árvores queimadas percent_burned[ip] /= n_trials; // Mostra resultado para esta probabilidade printf( %lf,%lf\n, prob_spread[ip], percent_burned[ip]); delete[] prob_spread; delete[] percent_burned; // Fim da região paralela do código... Já na versão que foi paralelizada com o MPI, foi abordada a paralelização do segundo laço de repetição. Como pode ser visto na seção 3, essa abordagem ofereceu uma distribuição mais uniforme da carga de trabalho. Isso acarreta em uma melhora no despenho da aplicação, pois cada processo faz uma quantidade igual de trabalho, e nenhum fica ocioso esperando os outros. O trecho do código onde foi aplicada essa abordagem de paralelização pode ser visto abaixo.... // Inicialização do MPI int task_id, n_tasks; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &task_id); MPI_Comm_size(MPI_COMM_WORLD, &n_tasks); // Parâmetros dos experimentos
int params[3] = {30, 000, 101; int &forest_size = params[0]; int &n_trials = params[1]; int &n_probs = params[2]; double* percent_burned; // Percentuais queimados (saída) double* prob_spread; // Probabilidades (entrada) double prob_min = 0.0; double prob_max = 1.0; double prob_step; int base_seed = 100; // A primeira tarefa lê os argumentos da linha de comando e passa eles para as outras tarefas if (task_id == 0) checkcommandline(argc, argv, forest_size, n_trials, n_probs); MPI_Bcast(¶ms, 3, MPI_INT, 0, MPI_COMM_WORLD); try { Forest* forest = new Forest(forest_size); Random rand; prob_spread = new double[n_probs]; percent_burned = new double[n_probs]; prob_step = (prob_max prob_min)/(double)(n_probs 1); // Para cada probabilidade, calcula o percentual de árvores queimadas for (int ip = 0; ip < n_probs; ip++) { prob_spread[ip] = prob_min + (double) ip * prob_step; percent_burned[ip] = 0.0; rand.setseed(base_seed+ip); // Nova sequência de números aleatórios // Calcula o número de testes que cada tarefa terá que realizar int n_trials_per_task = n_trials / n_tasks; // A tarefa 0 calcula o resto da divisão para fechar o número total. if (task_id == 0) n_trials_per_task += n_trials % n_tasks; // Executa vários experimentos for (int it = 0; it < n_trials_per_task; it++) { // Queima floresta até o fogo apagar forest >burnuntilout(forest >centraltree(), prob_spread[ip], rand); percent_burned[ip] += forest >getpercentburned(); // Vetor para reunir os resultados de todos na tarefa 0 double *percent_burned_all = NULL; if (task_id == 0) percent_burned_all = new double[n_probs]; // Usa a Reduce para somar as porcentagens queimadas em cada tarefa e armazenar em percent_burned_all MPI_Reduce(percent_burned, percent_burned_all, n_probs, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (task_id == 0) { printf( Probabilidade,PercentualQueimado\n ); // Calcula as médias e imprime os resultados for (int ip = 0; ip < n_probs; ip++) printf( %lf,%lf\n, prob_spread[ip], percent_burned_all[ip] / n_trials); delete[] prob_spread; delete[] percent_burned; if (task_id == 0) { delete[] percent_burned_all;... 3. Resultados Foram executados diversos testes com as duas versões do programa paralelizado para poder obtermos alguns resultados e determinar se houve ganho de desempenho com a utilização da nova abordagem. Os testes foram realizados nas duas versões com os seguintes argumentos de entrada:./firesim 30 1000 30,./firesim 30 000 10,./firesim 60 6000 10 e./firesim 0 8000. Os resultados das execussões, assim como o cálculo do speedup e desenpenho obtido podem ser observados abaixo, em forma tabular e em forma gráfica. Os primeiros dados apresentados são referentes à verão paralelizada com o OpenMP e os demais referentes à versão paralelizada com o MPI. 3.1. OpenMP Com essa abordagem não foi obtido um bom desempenho com a paralelização, pois a partir de duas threads o desempenho começou a piorar gradativamente. A seguir estão expostos os tempos de execução e cálculo de speedup e desempenho para a versão paralelizada usando o OpenMP. 3.1.1. Uso:./firesim 30 1000 30 Threads Tempo ideal real Desempenho 1 0m11.810s 2 0m10.32s 2 1.14 7.00% 4 0m11.232s 4 1.0 26.2% 8 0m13.278s 8 0.89 11.12% 16 0m14.622s 16 0.80 0.00% 32 0m1.81s 32 0.76 02.37%
3 30 2 20 1 10 0 0 10 1 20 2 30 3 Número de threads 3.1.2. Uso./firesim 30 000 10 Threads Tempo ideal real Desempenho 1 0m19.99s 2 0m17.010s 2 1.1 7.0% 4 0m19.348s 4 1.01 2.2% 8 0m19.147s 8 1.02 12.7% 16 0m18.78s 16 1.0 06.6% 32 0m19.749s 32 0.99 03.09% 3 30 2 20 1 10 0 0 10 1 20 2 30 3 Número de threads
3.1.3. Uso./firesim 60 6000 10 Threads Tempo ideal real Desempenho 1 2m27.06s 2 1m47.82s 2 1.37 68.0% 4 1m41.481s 4 1.4 36.2% 8 1m36.10s 8 1.3 19.12% 16 1m34.873s 16 1. 09.69% 32 1m34.749s 32 1. 04.84% 3 30 2 20 1 10 0 0 10 1 20 2 30 3 Número de threads 3.1.4. Uso./firesim 0 8000 Threads Tempo ideal real Desempenho 1 1m3.901s 2 0m47.114s 2 1.36 68.00% 4 0m39.011s 4 1.64 41.00% 8 0m38.63s 8 1.6 20.62% 16 0m38.447s 16 1.66 10.37% 32 0m38.838s 32 1.64 0.12%
3 30 2 20 1 10 0 0 10 1 20 2 30 3 Número de threads 3.2. MPI Com essa abordagem foram obtidos resultados melhores, e o desempenho teve uma melhora constante com o aumento do número de processos. Porém, os resultados só foram analisados até seis processos simultâneos, pois quando se tentava rodar com mais do que isso no laboratório, a biblioteca começava a gerar erros. A seguir estão expostos os tempos de execução e cálculo de speedup e desempenho para a versão paralelizada usando o MPI. 3.2.1. Uso:./firesim 30 1000 30 Processos Tempo ideal real Desempenho 1 0m20.007s 2 0m13.87s 2 1.47 73.0% 3 0m8.109s 3 2.47 82.33% 4 0m6.7s 4 2.96 74.00% 0m.667s 3.3 70.60% 6 0m.841s 6 3.42 7.00%
6 4 3 2 1 1 2 3 4 6 Número de processos 3.2.2. Uso./firesim 30 000 10 Processos Tempo ideal real Desempenho 1 0m39.488s 2 0m14.92s 2 2.64 132.0% 3 0m14.67s 3 2.68 89.33% 4 0m10.929s 4 3.61 90.2% 0m8.89s 4.60 92.00% 6 0m6.804s 6.80 96.66% 6 4 3 2 1 1 2 3 4 6 Número de processos
3.2.3. Uso./firesim 60 6000 10 Processos Tempo ideal real Desempenho 1 1m41.744s 2 1m1.08s 2 0.91 4.0% 3 1m.84s 3 1. 1.66% 4 0m42.26s 4 2.41 60.2% 0m34.207s 2.97 9.40% 6 0m28.917s 6 3.2 8.66% 6 4 3 2 1 0 1 2 3 4 6 Número de processos 3.2.4. Uso./firesim 0 8000 Processos Tempo ideal real Desempenho 1 0m46.791s 2 0m41.274s 2 1.13 6.0% 3 0m30.864s 3 1.2 0.66% 4 0m2.891s 4 1.81 4.2% 0m20.27s 2.31 46.20% 6 0m16.28s 6 2.88 48.00%
6 4 3 2 1 1 2 3 4 6 Número de processos 4. Conclusão Em geral, as aplicações do método de Monte Carlo são computacionalmente intensivas, pois é necessário repetir experimentos com diversas amostras de números para se fazer alguma análise estatística. Com a execução deste trabalho foi observado que é possível ter um ganho de desempenho em aplicações desse tipo com o uso de paralelização. Porém, para se obter esse ganho é necessário analisar com cuidado o código a ser paralelizado para que a solução seja adequada. Foi perceptível que a versão paralelizada com o MPI teve um maior desempenho, pois nessa versão o código foi paralelizado de uma maneira melhor, que distribui as tarefas igualitariamente entre os processos, permitindo que a execução se torne mais rápida. Referências Argonne National Laboratory/Mississippi State University. Manual MPICH. Disponível em <http://www.mcs.anl.gov/research/projects/mpi/www/>. Acesso em 11 mai. 2009. MPI Forum. The Message Passing Interface (MPI) standard. Disponível em <http:// www-unix.mcs.anl.gov/mpi/>. Acesso em 11 mai. 2009. OpenMP. OpenMP: Simple, Portable, Scalable SMP Programming. Disponível em <http: //openmp.org/>. Acesso em 11 mai. 2009. Wikipedia. Monte Carlo method. Disponível em <http://en.wikipedia.org/wiki/ Monte_Carlo_method>. Acesso em 11 mai. 2009.