R en paralelo (pero ahora, con futuros)

Esta entrada extiende y mejora una homónima de 2014.

El problema de entonces consistía en calcular por separado y en paralelo objetos A, B y C para combinarlos después. Cuando, por supuesto, el cálculo de A, B y C es pesado.

El muy reciente paquete future incorpora a R un mecanismo disponible en otros lenguajes de programación: un cierto tipo de datos, los futuros, que contienen promesas de valores que se calculan fuera del hilo principal del programa. Se usan, por ejemplo, para realizar llamadas a APIs, operaciones de IO o (y esto es más pertinente para usuarios de R) cálculos que llevan su tiempico.

Mirad el código de entonces y comparadlo con:

library(future)

plan(multiprocess)

a0 <- future({
  Sys.sleep(3)
  10
})

b0 <- future({
  Sys.sleep(3)
  11
})

system.time(
  res <- list(value(a0), value(b0))
)  

Para más detalles, las viñetas.

14 comentarios sobre “R en paralelo (pero ahora, con futuros)

  1. Iñaki 4 noviembre, 2016 11:44

    Supongo que viene de aquí. Puse un comentario ayer allí, pero no se ha publicado. Lo que venía a decir es que no veo ninguna ventaja frente a un `mclapply ` en el tipo de aplicación que se da (y desde luego en la aplicación que das en esta entrada, que es la misma).

    Me repito con respecto a tu entrada pasada, pero al estilo de esta:

    «`r
    library(parallel)

    a0 <- function() {
    Sys.sleep(3)
    10
    }

    b0 <- function() {
    Sys.sleep(3)
    11
    }

    system.time(
    res <- mclapply(c(a0, b0), do.call, list())
    )
    «`

    ¿Diferencias? Yo no veo ninguna, ni en código ni en rendimiento (y si me apuras, los ejemplos de la entrada enlazada se pueden hacer más cortos con parallel). La clave está en el objetivo final: combinar los resultados para usarlos después. Esto hace que haya que bloquear se quiera o no, ya sea con parallel o con future.

    Computación en paralelo no es lo mismo que asincronismo. Lo primero ya está bien cubierto por parallel (y otras herramientas), mientras que el potencial de future se encuentra en lo segundo. Lo que en Javascript sería un hazme_esto().then(esto_otro()) (que, por cierto, no he encontrado la manera de hacerlo con future…).

    En definitiva, que no entiendo por qué tanto revuelo con future del autor del artículo que enlazo ("a game-changing R package") cuando todos los ejemplos que da son triviales con parallel.

  2. Iñaki 4 noviembre, 2016 11:45

    Perdón… debería haber utilizado la etiqueta «code». La costumbre, ya sabes…

  3. Carlos J. Gil Bellosta 4 noviembre, 2016 14:47

    Reconozco que gran parte de mi entusiasmo es consecuencia de ver trasladadas a R funcionalidades disponibles en Scala. Estoy de acuerdo en que, bajo cierto punto de vista, future no aporta gran cosa.

    Efectivamente, si cuentas el número de caracteres de las soluciones que usan future y parallel, el beneficio, es mínimo. En términos de lo que ocurra bajo el capó, seguro, tampoco. O de rendimiento. Convengo.

    Sin embargo, conceptualmente, desde un punto de vista lógico, me parece muy limpia la solución de empaquetar objetos que en algún momento aparecerán (porque son el resultado de operaciones complejas, o que incluyen IO pesado, o llamadas a servicios externos) en un objeto-promesa, un futuro. Cuando necesitas el valor, lo desempaquetas con value (bloquees o no).

    Tu código (que es similar al que creé en su día con parallel y que enlazo en la entrada) es un truco contraintuitivo: creas unas funciones y luego las corres más abajo en paralelo. Es una chocolatada. Es más limpio (y anuncia exactamente lo que se quiere hacer) envolver tu código asíncrono no en function (que está pensado para otra cosa) sino en future.

    Como en tantas cosas, la diferencia radica en pasar de trucos a principios.

  4. Iñaki 5 noviembre, 2016 0:25

    Yo me entusiasmé con el entusiasmo del post que apareció en R-bloggers porque enseguida pensé en future({})$then({}). Luego ese «then» no estaba por ningún lado, por lo que me dio el bajón y me puse en modo gruñón.

    Supongo que ya llegará, que al paquete le queda mucho recorrido. Aunque hay que solucionar algunos problemillas de arquitectura para ello.

  5. daniel 5 noviembre, 2016 14:09

    Hola, me extraña el comentario de Iñaki porque leyendo la entrada (1) leo que future no bloquea al programa, lo que permite por ejemplo lanzar una serie de threads que requieren mucho tiempo y al mismo tiempo seguir haciendo otras cosas con la consola, como descargar más datos etc. Es decir future permite continuar con otras acciones que no requieran el resultado que estamos esperando, esto lo veo muy práctico ya que mientras espero los resultados de un cálculo puedo seguir explorando otras opciones con el programa.

    A propósito, veo en (*) que el comentario de Iñaki ya está publicado.

    https://alexioannides.com/2016/11/02/asynchronous-and-distributed-programming-in-r-with-the-future-package

  6. Iñaki 5 noviembre, 2016 16:40

    @Daniel: Definir un future no bloquea, al igual que no bloquea definir una tarea, una function, en el ejemplo con parallel. Lo que bloquea es value y mclapply respectivamente.

    Y no puede ser de otra manera, porque el objetivo aquí no es el asincronismo, sino la computación en paralelo de diversas tareas pesadas: en ambos casos es necesario bloquear hasta que todas las tareas terminen y juntar los resultados. El paquete future proporciona value para bloquear y mclapply ya directamente bloquea sin más ceremonias porque para eso está.

  7. daniel 6 noviembre, 2016 0:27

    @Iñaki, leyendo con más detenimiento los paquetes future, y parallel parece que future es una interfaz para unificar los distintos tareas tanto sincronizadas como no sincronizadas:

    The purpose of the future package is to provide a very simple and uniform way of evaluating R expressions asynchronously.

    probablemente llama a mcparallel y mccollect (*).

    Por ejemplo, usando mccollect con la opcion wait=FALSE se pueden obtener los resultados que ya estén listos y descartar el resto, lo que permite mayor flexibilidad.

    En definitiva lo que parece ofrecer el paquete future es cierta uniformidad en el tratamiento del paralelismo, pero se podrá obtener algo similar usando otros paquetes.

    https://stat.ethz.ch/R-manual/R-devel/library/parallel/html/mcparallel.html

  8. Iñaki 6 noviembre, 2016 20:36

    Sí, es muy interesante que puedas resolver esas tareas en multiproceso o en un clúster de manera transparente con la misma sintaxis. Ese es su punto fuerte, diría yo, ahora mismo: unificar.

    Pero mi crítica iba más a que se vende como asincronismo unos ejemplos que claramente son síncronos. Me parece necesario diferenciar esto bien, porque asincronismo no es paralelismo y paralelismo no es asincronismo.

    Lamentablemente, a pesar de lo que la descripción del paquete pueda decir, el asincronismo no será posible con future hasta que no se puedan asignar event handlers.

  9. Carlos J. Gil Bellosta 6 noviembre, 2016 22:11

    Ya, asincronía no es paralelismo (y a la inversa). Pero, ¿asincronía son «callbacks»? Entiendo que quieres hacer cosas del tipo: «haz A; cuando acabes, haz B» en un hilo asíncrono. De alguna, manera, se puede escribir así: future(A) %% then(B). ¿Qué diferencia hay con respecto a future(A;B) corriendo todo en su propio hilo independiente del principal?

    Me consta que en algunas implementaciones de librerías para programación asíncrona hay «callbacks». Me consta que en algunas basadas en futuros también. Pero lo que no me consta es que sean necesarias para la asincronía.

  10. Iñaki 7 noviembre, 2016 11:14

    Si tienes un core por cada future, no hay ninguna diferencia, no, pero entonces es paralelismo, no necesariamente asincronismo. En general, no los vas a tener. O, poniéndonos más estrictos, supón que tienes un solo core. La programación asíncrona debe funcionar igual independientemente del número de cores. Si solo hay un thread, tu future(A; B) se convierte en un programa síncrono (ya lo era, pero los múltiples cores nos formaban la ilusión de que no) que no deja ejecutar otro future hasta que no acabe.

    Para que haya asincronismo tiene que haber un scheduler, eventos y pequeñas unidades de trabajo que poder intercalar (llámalos «callbacks», llámalos «x»). Lee un archivo; cuando acabes (evento), haz esto otro, etc. Con future(A; B), la secuencia ACB solo es posible si tienes suficientes cores para paralelizar. Con future(A) %% then(B), la secuencia ACB siempre es posible (si hay un scheduler y una gestión de eventos implementada por debajo).

  11. Iñaki 7 noviembre, 2016 12:00

    Un artículo muy interesante (sobre JavaScript, eso sí) sobre todo esto de promesas, asincronismo y callbacks. Y el «then», el bendito «then».

  12. Carlos J. Gil Bellosta 8 noviembre, 2016 1:25

    Yo tengo 4 cores, pero:


    library(future)

    plan(multiprocess, workers = 10)

    system.time({
    a1 < - future({Sys.sleep(5); 1}) a2 <- future({Sys.sleep(5); 1}) a3 <- future({Sys.sleep(5); 1}) a4 <- future({Sys.sleep(5); 1}) a5 <- future({Sys.sleep(5); 1}) a6 <- future({Sys.sleep(5); 1}) res <- sapply(list(a1, a2, a3, a4, a5, a6), value) }) # user system elapsed # 0.060 0.108 5.031

    Supongo que si tuviese un solo núcleo, los tiempos serían los mismos. Es el sistema operativo el que gestiona los procesos (que pueden ser superior al número de cores) y les asigna recursos. Aunque tengas multithreading, que yo sepa, siempre hay un número máximo de hilos (32000, creo, en Scala). Supongo que habrá un número máximo de "forks" que tolerará el sistema operativo. Pero no está limitado al número de núcleos. Otra cosa es que tengan que compartir recursos (pero con los hilos sucede lo mismo).

  13. Iñaki 8 noviembre, 2016 10:59

    Puede que no me haya explicado bien metiendo la palabra «core» también por ahí. Cambia «core» por «thread». Decía que «si solo tienes un thread», ergo


    library(future)

    plan(multiprocess, workers = 1)

    system.time({
    a1 <- future({Sys.sleep(5); print("Soy a1"); 1})
    a2 <- future({Sys.sleep(4); print("Soy a2"); 1})
    a3 <- future({Sys.sleep(3); print("Soy a3"); 1})
    a4 <- future({Sys.sleep(2); print("Soy a4"); 1})
    a5 <- future({Sys.sleep(1); print("Soy a5"); 1})
    res <- sapply(list(a1, a2, a3, a4, a5), value)
    })
    [1] "Soy a1"
    [1] "Soy a2"
    [1] "Soy a3"
    [1] "Soy a4"
    [1] "Soy a5"

    Adiós «asincronismo» (nunca lo hubo). Node.js es single-threaded y es perfectamente asíncrono. ¿Por qué? Porque tiene un manejador de eventos, un event loop. Lo encontrarás también en la Event Machine de Ruby y en el core de Python Twisted.

Los comentarios están desabilitados.