-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy path25-Intro_HPC.Rmd
233 lines (174 loc) · 8.62 KB
/
25-Intro_HPC.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# Introducción al procesamiento en paralelo en R {#intro-hpc}
<!-- bookdown::preview_chapter("25-Intro_HPC.Rmd") -->
```{r , child = '_global_options.Rmd'}
```
En este apéndice se pretenden mostrar las principales herramientas para el procesamiento en paralelo disponibles en R y dar una idea de su funcionamiento.
Para más detalles se recomienda ver [CRAN Task View: High-Performance and Parallel Computing with R](https://cran.r-project.org/view=HighPerformanceComputing)
(además de HPC, High Performance Computing, también incluye herramientas
para computación distribuida)^[También puede ser de interés la presentación [R y HPC (uso de R en el CESGA)](https://www.r-users.gal/sites/default/files/10_aurelio_rodriguez.pdf) de Aurelio Rodríguez en las [VI Xornada de Usuarios de R en Galicia](https://www.r-users.gal/pagina/programa-2018)].
## Introducción
Emplearemos la siguiente terminología:
- **Núcleo**: término empleado para referirse a un procesador lógico de un equipo
(un equipo puede tener un único procesador con múltiples núcleos que pueden
realizar operaciones en paralelo). También podría referirse aquí a un
equipo (nodo) de una red (clúster de equipos; en la práctica cada uno puede tener
múltiples núcleos). *Un núcleo puede ejecutar procesos en serie*.
- **Clúster**: colección de núcleos en un equipo o red de equipos.
*Un clúster puede ejecutar varios procesos en paralelo*.
Por defecto la versión oficial de R emplea un único núcleo, aunque se puede compilar
de forma que realice cálculos en paralelo (e.g. librería LAPACK).
También están disponibles otras versiones de R que ya implementan por defecto
procesamiento en paralelo ([multithread](https://mran.revolutionanalytics.com/documents/rro/multithread)):
- [Microsoft R Open](https://mran.revolutionanalytics.com): versión de R con rendimiento mejorado.
Métodos simples de paralelización^[Realmente las herramientas estándar son
*OpenMP* para el procesamiento en paralelo con memoria compartida en un único equipo
y *MPI* para la computación distribuida en múltiples nodos.]:
- *Forking*: Copia el proceso de R a un nuevo núcleo (se comparte el entorno de trabajo).
Es el más simple y eficiente pero **no está disponible en Windows**.
- *Socket*: Lanza una nueva versión de R en cada núcleo, como si se tratase de un cluster
de equipos comunicados a traves de red (hay que crear un entorno de trabajo en cada núcleo).
Disponible en todos los sistemas operativos.
## Paquetes en R
Hay varios paquetes que se pueden usar para el procesamiento paralelo en R,
entre ellos podríamos destacar:
- `parallel`: forma parte de la instalación base de R y fusiona los paquetes
`multicore` (forking) y `snow` (sockets; Simple Network of Workstations).
Además incluye herramientas para la generación de números aleatorios en paralelo
(cada proceso empleara una secuencia y los resultados serán reproducibles).
Incluye versiones "paralelizadas" de la familia `*apply()`:
`mclapply()` (forking), `parLapply()` (socket), ...
- `foreach`: permite realizar iteraciones y admite paralelización con el operador `%dopar%`,
aunque requiere paquetes adicionales como `doSNOW` o `doParallel` (recomendado).
- `rslurm`: permite la ejecución distribuida en clústeres Linux que implementen
SLURM (Simple Linux Utility for Resource Management),
un gestor de recursos de código abierto muy empleado.
## Ejemplos
Si se emplea el paquete `parallel` en sistemas tipo Unix (Linux, Mac OS X, ...), se podría
evaluar en paralelo una función llamando directamente a `mclapply()`.
Por defecto empleará todos los núcleos disponibles, pero se puede especificar un número menor
mediante el argumento `mc.cores`.
```{r}
library(parallel)
ncores <- detectCores()
ncores
func <- function(k) {
i_boot <- sample(nrow(iris), replace = TRUE)
lm(Petal.Width ~ Petal.Length, data = iris[i_boot, ])$coefficients
}
RNGkind("L'Ecuyer-CMRG") # Establecemos Pierre L'Ecuyer's RngStreams...
set.seed(1)
system.time(res.boot <- mclapply(1:100, func)) # En Windows llama a lapply() (mc.cores = 1)
# res.boot <- mclapply(1:100, func, mc.cores = ncores - 1) # En Windows genera un error si mc.cores > 1
```
En Windows habría que crear previamente un cluster, llamar a una de las funciones
`par*apply()` y finalizar el cluster:
```{r}
cl <- makeCluster(ncores - 1, type = "PSOCK")
clusterSetRNGStream(cl, 1) # Establecemos Pierre L'Ecuyer's RngStreams con semilla 1...
system.time(res.boot <- parSapply(cl, 1:100, func))
# stopCluster(cl)
str(res.boot)
```
Esto también se puede realizar en Linux (`type = "FORK"`),
aunque podríamos estar trabajando ya en un cluster de equipos...
También podríamos emplear balance de carga si el tiempo de computación es variable
(e.g. `parLapplyLB()` o `clusterApplyLB()`) pero no sería recomendable si se emplean
números pseudo-aleatorios (los resultados no serían reproducibles).
Además, empleando las herramientas del paquete `snow` se puede representar el uso
del cluster (*experimental* en Windows):
```{r eval=FALSE}
# library(snow)
ctime <- snow::snow.time(snow::parSapply(cl, 1:100, func))
ctime
plot(ctime)
```
Hay que tener en cuenta la sobrecarga adicional debida a la comunicación entre nodos
al paralelizar (especialmente con el enfoque de socket).
### Procesamiento en paralelo con la función `boot()`
La función `boot::boot()` incluye parámetros para el procesamiento en paralelo:
`parallel = c("no", "multicore", "snow")`, `ncpus`, `cl`.
Si `parallel = "snow"` se crea un clúster en la máquina local durante la ejecución,
salvo que se establezca con el parámetro `cl`.
Veamos un ejemplo empleando una muestra simulada:
```{r}
n <- 100
rate <- 0.01
mu <- 1/rate
muestra <- rexp(n, rate = rate)
media <- mean(muestra)
desv <- sd(muestra)
library(boot)
statistic <- function(data, i){
remuestra <- data[i]
c(mean(remuestra), var(remuestra)/length(remuestra))
}
B <- 2000
set.seed(1)
system.time(res.boot <- boot(muestra, statistic, R = B))
# system.time(res.boot <- boot(muestra, statistic, R = B, parallel = "snow"))
system.time(res.boot <- boot(muestra, statistic, R = B, parallel = "snow", cl = cl))
```
### Estudio de simulación {#estudio-sim-boot}
Si se trata de un estudio más complejo, como por ejemplo un estudio de simulación
en el que se emplea bootstrap, la recomendación sería tratar de paralelizar
en el nivel superior para minimizar la sobrecarga debida a la comunicación
entre nodos.
Por ejemplo, a continuación se realiza un estudio similar al mostrado en la Sección \@ref(estudio-sim-exp)
pero comparando las probabilidades de cobertura y las longitudes de los
intervalos de confianza implementados en la función `boot.ci()`.
```{r}
t.ini <- proc.time()
nsim <- 500
getSimulation <- function(isim, B = 2000, n = 30, alfa = 0.1, mu = 100) {
rate <- 1/mu # 0.01
resnames <- c("Cobertura", "Longitud")
# intervals <- c("Normal", "Percentil", "Percentil-t", "Percentil-t simetrizado")
intervals <- c("Normal", "Basic", "Studentized", "Percentil", "BCa")
names(intervals) <- c("normal","basic", "student", "percent", "bca")
intervals <- intervals[1:4]
resultados <- array(dim = c(length(resnames), length(intervals)))
dimnames(resultados) <- list(resnames, intervals)
# for (isim in 1:nsim) { # isim <- 1
muestra <- rexp(n, rate = 0.01)
media <- mean(muestra)
desv <- sd(muestra)
# boot()
library(boot)
statistic <- function(data, i){
remuestra <- data[i]
c(mean(remuestra), var(remuestra)/length(remuestra))
}
res.boot <- boot(muestra, statistic, R = B)
res <- boot.ci(res.boot, conf = 1 - alfa)
# Intervalos
res <- sapply(res[names(intervals)], function(x) {
l <- length(x)
x[c(l-1, l)]
})
# resultados
resultados[1, ] <- apply(res, 2,
function(ic) (ic[1] < mu) && (mu < ic[2])) # Cobertura
resultados[2, ] <- apply(res, 2, diff) # Longitud
resultados
}
parallel::clusterSetRNGStream(cl)
result <- parLapply(cl, 1:nsim, getSimulation)
# stopCluster(cl)
# result
t.fin <- proc.time() - t.ini
print(t.fin)
resnames <- c("Cobertura", "Longitud")
intervals <- c("Normal", "Basic", "Studentized", "Percentil", "BCa")
names(intervals) <- c("normal","basic", "student", "percent", "bca")
intervals <- intervals[1:4]
resultados <- sapply(result, function(x) x)
dim(resultados) <- c(length(resnames), length(intervals), nsim)
dimnames(resultados) <- list(resnames, intervals, NULL)
res <- t(apply(resultados, c(1, 2), mean))
res
knitr::kable(res, digits = 3)
```
El último paso es finalizar el cluster:
```{r}
stopCluster(cl)
```