diff --git a/DESCRIPTION b/DESCRIPTION index d3f28e323..801977397 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: mirai Type: Package Title: Minimalist Async Evaluation Framework for R -Version: 0.11.3.9004 +Version: 0.11.3.9005 Description: Lightweight parallel code execution and distributed computing. Designed for simplicity, a 'mirai' evaluates an R expression asynchronously, on local or network resources, resolving automatically upon completion. @@ -22,7 +22,7 @@ Encoding: UTF-8 Depends: R (>= 3.5) Imports: - nanonext (>= 0.11.0.9010) + nanonext (>= 0.12.0) Enhances: parallel, promises diff --git a/NEWS.md b/NEWS.md index 1e1d032dd..8144301b0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,11 +1,11 @@ -# mirai 0.11.3.9004 (development) +# mirai 0.11.3.9005 (development) * More minimal print methods for 'mirai' and 'miraiCluster'. * Adds `local_url()` helper to construct a random inter-process communications URL for local daemons (thanks @noamross #90). * `daemon()` argument 'autoexit' now accepts a signal value such as `tools::SIGINT` in order to raise it upon exit. * `daemon()` now records the state of initial global environment objects (e.g. those created in .Rprofile) for cleanup purposes (thanks @noamross #91). * Eliminates potential memory leaks along certain error paths. -* Requires nanonext >= [0.11.0.9010]. +* Requires nanonext >= 0.12.0. # mirai 0.11.3 diff --git a/README.Rmd b/README.Rmd index d41a26454..103c1c45f 100644 --- a/README.Rmd +++ b/README.Rmd @@ -112,6 +112,14 @@ Refer to the [mirai vignette](https://shikokuchuo.net/mirai/articles/mirai.html) vignette("mirai", package = "mirai") ``` +### Integrations + +{mirai} enhances the [{parallel}](https://shikokuchuo.net/mirai/articles/parallel.html) package by providing an alternative communications backend for R, implementing a low-level feature request by R-Core at [R Project Sprint 2023](https://contributor.r-project.org/r-project-sprint-2023/). + +{mirai} also supplies its own `as.promise()` method, allowing it to be used as a promise from the [{promises}](https://shikokuchuo.net/mirai/articles/promises.html) package. + +Further example integrations are provided for [{plumber}](https://shikokuchuo.net/mirai/articles/plumber.html), [{shiny}](https://shikokuchuo.net/mirai/articles/shiny.html), and [{torch}](https://shikokuchuo.net/mirai/articles/torch.html). + ### Powering Crew and Targets The [`crew`](https://cran.r-project.org/package=crew) package is a distributed worker-launcher extending {mirai} to different distributed computing platforms, from traditional clusters to cloud services. @@ -127,47 +135,6 @@ The [`crew`](https://cran.r-project.org/package=crew) package is a distributed w [`targets`](https://cran.r-project.org/package=targets), a Make-like pipeline tool for statistics and data science, has integrated and adopted [`crew`](https://cran.r-project.org/package=crew) as its default high-performance computing backend. -### Parallel Clusters - -{mirai} provides an alternative communications backend for R's 'parallel' base package, implementing a low-level feature request by R-Core at [R Project Sprint 2023](https://contributor.r-project.org/r-project-sprint-2023/). - -```{r cluster} -cl <- make_cluster(4) -cl -``` - -A 'miraiCluster' is fully compatible with all 'parallel' functions such as `parallel::clusterApply()` [[further details](https://shikokuchuo.net/mirai/articles/parallel.html)]. - -### Asynchronous Shiny and Plumber Applications - -{mirai} serves as an asynchronous backend for scaling enterprise {shiny} or {plumber} applications. - -A 'mirai' plugs in directly to Shiny's reactive framework without the need to use promises [[see example](https://shikokuchuo.net/mirai/articles/shiny.html#shiny-example-usage)]. - -Alternatively, 'mirai' may be used interchangeably with 'promises' by using the promise pipe `%...>%`, or explictly by `promises::as.promise()`, allowing side-effects to be performed upon asynchronous resolution of a 'mirai'. - -The following example outputs "hello" to the console after one second when the 'mirai' resolves. - -```{r promises} -library(promises) -p <- mirai({Sys.sleep(1); "hello"}) %...>% cat() -p -``` - -Example usage is provided for [shiny](https://shikokuchuo.net/mirai/articles/shiny.html) and for [plumber](https://shikokuchuo.net/mirai/articles/plumber.html). - -### Torch Parallelization - -The custom serialization interface in {mirai} is accessed via `serialization()`. - -In the case of {torch}, this requires just the following call at the head of your session: - -```{r torch} -serialization(refhook = list(torch::torch_serialize, torch::torch_load)) -``` - -This allows tensors, including complex objects such as models, optimizers etc. to be used seamlessly across local and remote processes in the same way as other R objects [[further details](https://shikokuchuo.net/mirai/articles/torch.html)]. - ### Thanks We would like to thank in particular: diff --git a/README.md b/README.md index 9967895ad..cb80efdde 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ result. ``` r m$data -#> [1] -1.43344968 -0.02044095 -0.54523216 3.66578804 5.10911257 -#> [6] 0.19572871 0.27279264 -1.83408111 -48.92140310 -0.69761779 +#> [1] -1.8371814 -0.6623062 -0.4788542 1.2682884 0.6763480 1.4785289 +#> [7] 0.7884642 -2.0883185 -1.5098757 -0.5443121 ``` Alternatively, explicitly call and wait for the result using @@ -96,8 +96,8 @@ Alternatively, explicitly call and wait for the result using ``` r call_mirai(m)$data -#> [1] -1.43344968 -0.02044095 -0.54523216 3.66578804 5.10911257 -#> [6] 0.19572871 0.27279264 -1.83408111 -48.92140310 -0.69761779 +#> [1] -1.8371814 -0.6623062 -0.4788542 1.2682884 0.6763480 1.4785289 +#> [7] 0.7884642 -2.0883185 -1.5098757 -0.5443121 ``` ### Daemons @@ -124,6 +124,24 @@ package functionality. This may be accessed within R by: vignette("mirai", package = "mirai") ``` +### Integrations + +{mirai} enhances the +[{parallel}](https://shikokuchuo.net/mirai/articles/parallel.html) +package by providing an alternative communications backend for R, +implementing a low-level feature request by R-Core at [R Project Sprint +2023](https://contributor.r-project.org/r-project-sprint-2023/). + +{mirai} also supplies its own `as.promise()` method, allowing it to be +used as a promise from the +[{promises}](https://shikokuchuo.net/mirai/articles/promises.html) +package. + +Further example integrations are provided for +[{plumber}](https://shikokuchuo.net/mirai/articles/plumber.html), +[{shiny}](https://shikokuchuo.net/mirai/articles/shiny.html), and +[{torch}](https://shikokuchuo.net/mirai/articles/torch.html). + ### Powering Crew and Targets The [`crew`](https://cran.r-project.org/package=crew) package is a @@ -147,68 +165,6 @@ pipeline tool for statistics and data science, has integrated and adopted [`crew`](https://cran.r-project.org/package=crew) as its default high-performance computing backend. -### Parallel Clusters - -{mirai} provides an alternative communications backend for R’s -‘parallel’ base package, implementing a low-level feature request by -R-Core at [R Project Sprint -2023](https://contributor.r-project.org/r-project-sprint-2023/). - -``` r -cl <- make_cluster(4) -cl -#> < miraiCluster | ID: `0` nodes: 4 active: TRUE > -``` - -A ‘miraiCluster’ is fully compatible with all ‘parallel’ functions such -as `parallel::clusterApply()` \[[further -details](https://shikokuchuo.net/mirai/articles/parallel.html)\]. - -### Asynchronous Shiny and Plumber Applications - -{mirai} serves as an asynchronous backend for scaling enterprise {shiny} -or {plumber} applications. - -A ‘mirai’ plugs in directly to Shiny’s reactive framework without the -need to use promises \[[see -example](https://shikokuchuo.net/mirai/articles/shiny.html#shiny-example-usage)\]. - -Alternatively, ‘mirai’ may be used interchangeably with ‘promises’ by -using the promise pipe `%...>%`, or explictly by -`promises::as.promise()`, allowing side-effects to be performed upon -asynchronous resolution of a ‘mirai’. - -The following example outputs “hello” to the console after one second -when the ‘mirai’ resolves. - -``` r -library(promises) -p <- mirai({Sys.sleep(1); "hello"}) %...>% cat() -p -#> -``` - -Example usage is provided for -[shiny](https://shikokuchuo.net/mirai/articles/shiny.html) and for -[plumber](https://shikokuchuo.net/mirai/articles/plumber.html). - -### Torch Parallelization - -The custom serialization interface in {mirai} is accessed via -`serialization()`. - -In the case of {torch}, this requires just the following call at the -head of your session: - -``` r -serialization(refhook = list(torch::torch_serialize, torch::torch_load)) -``` - -This allows tensors, including complex objects such as models, -optimizers etc. to be used seamlessly across local and remote processes -in the same way as other R objects \[[further -details](https://shikokuchuo.net/mirai/articles/torch.html)\]. - ### Thanks We would like to thank in particular: diff --git a/vignettes/mirai.Rmd b/vignettes/mirai.Rmd index a271ef0ea..217041f97 100644 --- a/vignettes/mirai.Rmd +++ b/vignettes/mirai.Rmd @@ -31,7 +31,6 @@ Use `mirai()` to evaluate an expression asynchronously in a separate, clean R pr A 'mirai' object is returned immediately. - ```r library(mirai) @@ -47,34 +46,27 @@ m <- mirai( m #> < mirai | $data > ``` - Above, all specified `name = value` pairs are passed through to the 'mirai'. The 'mirai' yields an 'unresolved' logical NA whilst the async operation is ongoing. - ```r m$data #> 'unresolved' logi NA ``` - Upon completion, the 'mirai' resolves automatically to the evaluated result. - ```r m$data |> str() -#> num [1:100000000] 0.528 1.653 -0.151 0.256 0.452 ... +#> num [1:100000000] -0.419 0.59 -3.401 0.892 -2.036 ... ``` - Alternatively, explicitly call and wait for the result using `call_mirai()`. - ```r call_mirai(m)$data |> str() -#> num [1:100000000] 0.528 1.653 -0.151 0.256 0.452 ... +#> num [1:100000000] -0.419 0.59 -3.401 0.892 -2.036 ... ``` - For easy programmatic use of `mirai()`, '.expr' accepts a pre-constructed language object, and also a list of named arguments passed via '.args'. So, the following would be equivalent to the above: @@ -89,7 +81,7 @@ args <- list(m = runif(1), n = 1e8) m <- mirai(.expr = expr, .args = args) call_mirai(m)$data |> str() -#> num [1:100000000] -0.23 2.545 1.844 0.382 -0.754 ... +#> num [1:100000000] -0.236 8434.42 -0.406 0.51 4.426 ... ``` [« Back to ToC](#table-of-contents) @@ -104,7 +96,6 @@ Cache data in memory and use `mirai()` to perform periodic write operations conc Below, '.args' is used to pass a list of objects already present in the calling environment to the mirai by name. This is an alternative use of '.args', and may be combined with `...` to also pass in `name = value` pairs. - ```r library(mirai) @@ -113,14 +104,12 @@ file <- tempfile() m <- mirai(write.csv(x, file = file), .args = list(x, file)) ``` - A 'mirai' object is returned immediately. `unresolved()` may be used in control flow statements to perform actions which depend on resolution of the 'mirai', both before and after. This means there is no need to actually wait (block) for a 'mirai' to resolve, as the example below demonstrates. - ```r # unresolved() queries for resolution itself so no need to use it again within the while loop @@ -134,7 +123,6 @@ while (unresolved(m)) { cat("Write complete:", is.null(m$data)) #> Write complete: TRUE ``` - Now actions which depend on the resolution may be processed, for example the next write. [« Back to ToC](#table-of-contents) @@ -147,7 +135,6 @@ As part of a data science / machine learning pipeline, iterations of model train Running each iteration in a 'mirai' isolates this potentially-problematic code such that even if it does fail, it does not bring down the entire pipeline. - ```r library(mirai) @@ -175,12 +162,11 @@ for (i in 1:10) { #> iteration 5 successful #> iteration 6 successful #> iteration 7 successful +#> Error: random error #> iteration 8 successful #> iteration 9 successful -#> Error: random error #> iteration 10 successful ``` - Further, by testing the return value of each 'mirai' for errors, error-handling code is then able to automate recovery and re-attempts, as in the above example. Further details on [error handling](#errors-interrupts-and-timeouts) can be found in the section below. The end result is a resilient and fault-tolerant pipeline that minimises downtime by eliminating interruptions of long computes. @@ -197,17 +183,13 @@ This is potentially more efficient as new processes no longer need to be created Call `daemons()` specifying the number of daemons to launch. - ```r daemons(6) #> [1] 6 ``` - - To view the current status, `status()` provides the number of active connections along with a matrix of statistics for each daemon. - ```r status() #> $connections @@ -215,68 +197,57 @@ status() #> #> $daemons #> i online instance assigned complete -#> abstract://389d1b697878f987f687e37f 1 1 1 1 0 -#> abstract://7fb26d9f95e11b3e75001696 2 1 1 1 0 -#> abstract://7a148fd3243d8d9559aadd70 3 1 1 1 0 -#> abstract://6dac5528ae1909eb5a778609 4 1 1 1 0 -#> abstract://9c1ad061914433f060f918ff 5 1 1 1 0 -#> abstract://abf8c29588a01fd28354b911 6 1 1 1 0 +#> abstract://4e708eaf3aaf9a8870774097 1 1 1 0 0 +#> abstract://c4e8fc96c7ea92aa610144ef 2 1 1 0 0 +#> abstract://ab284735f99387a3b3440ba3 3 1 1 0 0 +#> abstract://c430cdb1c267b8d81cb041ca 4 1 1 0 0 +#> abstract://1c71b0343df4aa3720daac87 5 1 1 0 0 +#> abstract://ed66fbfa1689233f9b42984f 6 1 1 0 0 ``` - The default `dispatcher = TRUE` creates a `dispatcher()` background process that connects to individual daemon processes on the local machine. This ensures that tasks are dispatched efficiently on a first-in first-out (FIFO) basis to daemons for processing. Tasks are queued at the dispatcher and sent to a daemon as soon as it can accept the task for immediate execution. Dispatcher uses synchronisation primitives from [`nanonext`](https://doi.org/10.5281/zenodo.7903429), waiting upon rather than polling for tasks, which is efficient both in terms of consuming no resources while waiting, and also being fully synchronised with events (having no latency). - ```r daemons(0) #> [1] 0 ``` - Set the number of daemons to zero to reset. This reverts to the default of creating a new background process for each 'mirai' request. #### Without Dispatcher Alternatively, specifying `dispatcher = FALSE`, the background daemons connect directly to the host process. - ```r daemons(6, dispatcher = FALSE) #> [1] 6 ``` - Requesting the status now shows 6 connections, along with the host URL at `$daemons`. - ```r status() #> $connections #> [1] 6 #> #> $daemons -#> [1] "abstract://712946bc701bf260e1980f5a" +#> [1] "abstract://32352ff17d4ac83434ceab75" ``` - This implementation sends tasks immediately, and ensures that tasks are evenly-distributed amongst daemons. This means that optimal scheduling is not guaranteed as the duration of tasks cannot be known *a priori*. As an example, tasks could be queued at a daemon behind a long-running task, whilst other daemons are idle having already completed their tasks. The advantage of this approach is that it is low-level and does not require an additional dispatcher process. It is well-suited to working with similar-length tasks, or where the number of concurrent tasks typically does not exceed available daemons. `everywhere()` may be used to evaluate an expression on all connected daemons and persist the resultant state, regardless of a daemon's 'cleanup' setting. - ```r everywhere(library(parallel)) ``` - The above keeps the `parallel` package loaded for all evaluations. Other types of setup task may also be performed such as making a common resource available, etc. - ```r daemons(0) #> [1] 0 ``` - Set the number of daemons to zero to reset. #### Everywhere @@ -303,22 +274,17 @@ It is recommended to use a websocket URL starting `ws://` instead of TCP in this Supplying a vector of URLs allows the use of arbitrary port numbers / paths. 'n' does not need to be specified if it can be inferred from the length of the 'url' vector, for example: - ```r daemons(url = c("ws://10.75.32.70:5566/cpu", "ws://10.75.32.70:5566/gpu", "ws://10.75.32.70:7788/1")) ``` - Alternatively, below a single URL is supplied, along with `n = 4` to specify that the dispatcher should listen at 4 URLs. In such a case, an integer sequence is automatically appended to the path `/1` through `/4` to produce the URLs. - ```r daemons(n = 4, url = host_url(port = 5555)) #> [1] 4 ``` - Requesting status on the host machine: - ```r status() #> $connections @@ -331,7 +297,6 @@ status() #> tcp://hostname:5557 3 0 0 0 0 #> tcp://hostname:5558 4 0 0 0 0 ``` - As per the local case, `$connections` shows the single connection to dispatcher, however `$daemons` now provides a matrix of statistics for the remote daemons. - `i` index number. @@ -344,48 +309,40 @@ Dispatcher automatically adjusts to the number of daemons actually connected. He To reset all connections and revert to default behaviour: - ```r daemons(0) #> [1] 0 ``` - Closing the connection causes the dispatcher to exit automatically, and in turn all connected daemons when their respective connections with the dispatcher are terminated. #### Connecting to Remote Daemons Directly By specifying `dispatcher = FALSE`, remote daemons connect directly to the host process. The host listens at a single URL, and distributes tasks to all connected daemons. - ```r daemons(url = host_url(), dispatcher = FALSE) -#> [1] "tcp://hostname:38447" +#> [1] "tcp://hostname:34149" ``` - Note that above, calling `host_url()` without a port value uses the default of '0'. This is a wildcard value that will automatically cause a free ephemeral port to be assigned. The actual assigned port is provided in the return value of the call, or it may be queried at any time via `status()`. The number of daemons connecting to the host URL is not limited and network resources may be added or removed at any time, with tasks automatically distributed to all connected daemons. `$connections` will show the actual number of connected daemons. - ```r status() #> $connections #> [1] 0 #> #> $daemons -#> [1] "tcp://hostname:38447" +#> [1] "tcp://hostname:34149" ``` - To reset all connections and revert to default behaviour: - ```r daemons(0) #> [1] 0 ``` - This causes all connected daemons to exit automatically. [« Back to ToC](#table-of-contents) @@ -400,7 +357,6 @@ To launch remote daemons, supply a remote launch configuration to the 'remote' a The first example below launches 4 daemons on the machine 10.75.32.90 (using the default SSH port of 22 as this was not specified), connecting back to the dispatcher URLs: - ```r daemons( n = 4, @@ -410,7 +366,6 @@ daemons( ``` The second example below launches one daemon on each of 10.75.32.90 and 10.75.32.91 using the custom SSH port of 222: - ```r daemons( n = 2, @@ -430,7 +385,6 @@ Tunnelling requires the hostname for 'url' specified when setting up daemons to The below example launches 2 nodes on the remote machine 10.75.32.90 using SSH tunnelling over port 5555 ('url' hostname is specified as 'localhost'): - ```r daemons( url = "tcp://localhost:5555", @@ -445,22 +399,20 @@ daemons( As an alternative to automated launches, calling `launch_remote()` without specifying 'remote' may be used to return the shell commands for deploying daemons manually. The printed return values may be copy / pasted directly to a remote machine. - ```r daemons(n = 2, url = host_url()) #> [1] 2 launch_remote(1:2) #> [1] -#> Rscript -e "mirai::daemon('tcp://hostname:40227',rs=c(10407,-1161792096,-944287455,-1911839634,-82165225,-1418127508,-1919664771))" +#> Rscript -e "mirai::daemon('tcp://hostname:37615',rs=c(10407,298433903,983192804,415862421,-1539719598,792301995,1328174320))" #> #> [2] -#> Rscript -e "mirai::daemon('tcp://hostname:41663',rs=c(10407,-2043862129,185544717,1701915152,-966466064,1809908748,1857730719))" +#> Rscript -e "mirai::daemon('tcp://hostname:35183',rs=c(10407,1174764898,527188777,1261951900,-1315404600,57492514,820772616))" daemons(0) #> [1] 0 ``` - Note that `daemons()` should be set up on the host machine before launching `daemon()` on remote resources, otherwise the daemon instances will exit if a connection is not immediately available. Alternatively, specifying the argument `autoexit = FALSE` will allow daemons to wait (indefinitely) for a connection to become available. [« Back to ToC](#table-of-contents) @@ -473,57 +425,52 @@ TLS is available as an option to secure communications from the local machine to An automatic zero-configuration default is implemented. Simply specify a secure URL of the form `wss://` or `tls+tcp://` when setting daemons, or use `host_url(tls = TRUE)`, for example: - ```r daemons(n = 4, url = host_url(ws = TRUE, tls = TRUE)) #> [1] 4 ``` - Single-use keys and certificates are automatically generated and configured, without requiring any further intervention. The private key is always retained on the host machine and never transmitted. The generated self-signed certificate is available via `launch_remote()`. This function conveniently constructs the full shell command to launch a daemon, including the correctly specified 'tls' argument to `daemon()`. - ```r launch_remote(1) #> [1] -#> Rscript -e "mirai::daemon('wss://hostname:44221/1',tls=c('-----BEGIN CERTIFICATE----- +#> Rscript -e "mirai::daemon('wss://hostname:34161/1',tls=c('-----BEGIN CERTIFICATE----- #> MIIFNzCCAx+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAzMREwDwYDVQQDDAhrdW1h #> bW90bzERMA8GA1UECgwITmFub25leHQxCzAJBgNVBAYTAkpQMB4XDTAxMDEwMTAw #> MDAwMFoXDTMwMTIzMTIzNTk1OVowMzERMA8GA1UEAwwIa3VtYW1vdG8xETAPBgNV #> BAoMCE5hbm9uZXh0MQswCQYDVQQGEwJKUDCCAiIwDQYJKoZIhvcNAQEBBQADggIP -#> ADCCAgoCggIBAKnvbLWqfZjbFWrRyrwOu2HLmsXJ98OlmY8DGb6VP6knHqqM5QAt -#> IBB40AHs2okVTbS2qNQ/8a3IusouEIDpDZgHFsCaixNUDqJZqROJKqy1sTz42yuf -#> WKR8tCS53YXR1r49lIcbJx0hU2+BFcTHmy8slAB3V6boJI9mxoK5Fl548ogJggEQ -#> 4KHOIF6cG+vhIDwD+QP+5L7Di/5vgtOKIBMO0FHZvkRZ7Q/hPU39TeyFmXoGwbfl -#> GXOUENorsyULLnYLMC833uNPHjq/9W64ITiTZDPp44pMtmlH2xjPjbjXy1zER47q -#> w8Niwib+zBHWWYJZ0eq0CGm0z6yLYQF+gATzBYuh8Qut8zT7iEfZuTM2eCwYaW3W -#> 2UHH19tTpcOc/dtrjSvbc/iteWUhj7IljPNqXkwQlUfvNUpaGD+vNc2mcqzhGhzo -#> JltfBbXoQUjQKdKnSVJkaJ9qMXARG/hce/ZkzNV3cJ/oysXWbj/JlSFK60VfAxqE -#> RrBVhLuBSTGSor4eI7p2DhSOw/7jOOc28xYYdpkBxGDyU9yTCoYT3kbBOkmlt9Zd -#> YqOv81NMeqhvXOeSEntRYn0TnCap7uQe0ZJOHOsM3DEC19bK6YkiLvPwVu0LjU0d -#> FWb9D8LC2oMMDRZTO2FffRtaLgLn39iaF+E+LiUzUv/Q1SMArGLgntu3AgMBAAGj -#> VjBUMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAk9DxNWGYxq1IxWu5eH -#> tW4H0iNiMB8GA1UdIwQYMBaAFAk9DxNWGYxq1IxWu5eHtW4H0iNiMA0GCSqGSIb3 -#> DQEBCwUAA4ICAQALVWdomOAx9lb/OlcgOllAjhIa3fOrfNEUg4idzWfjmM4YHp09 -#> kMsuDXjcnvzMe8La5mRTFeOLS9+N8n9+x9MWydWb3hjcvrx1UfSU81SdSAARahhm -#> 1qlO3pmmOBqUITWiShx2AJioBcZVWsYCvQN8qeNhBXonJDyeKkMupB8H/SiEb6fN -#> WvpjnmmxwT9b8k883zFFqVtgOEyGHCPfkmZfj2paOxuIYf3EdbqljzKjQ+HpXyhi -#> Iw1XaTq3YQKYJXQnXRwvHJcjO3vLW1tW8nsFfa7b/Um+rmljFU33D3lTpDGD2rOZ -#> 3enux5SjJdDVAhCHs7Mob8/HuSce6fotXZ1mn2j7K+hXL4uDcgvMxMsqy+hGVQfd -#> RRv4lT7CsX+pVRLSD32rkODrTBp0lm4GLLY9B9K4S+eFbYl6iqh8z41+5evpwn+4 -#> sazOqN1iaeUWkDnC8hiBYaqtW0k+HCPgUql+dy+MmlHQG+iDLa3CL4ft7Kz9s4ss -#> dc21foCg1s2lu9kOkH8lB261qsBU9e4nnRwzDDAHXBPH8MmUdzg0v3AFI6nagKpk -#> DWba+9VbMt05hkaQ67M5srKEqmU/LIVRG8ISd7Pt10twhQQZ5WQMfhKWARcbqved -#> 0UdJcWm3apYm1LkCWihv/0CVpEaOVZCOmjA9huCUAX98G+JMyYpgXDxVlg== +#> ADCCAgoCggIBAL4AotUEXxbcXJDjkhXCfkaZv4ju4nXMVuZfyKzinR5hmo1W4Xnb +#> uZTbkpoO+45ZvTB2X/B1Smy2WByInJiJTht/ttagRKMRWUSJne2lhUkThXw8E6fQ +#> tj1V5peXMn38RdTG6trfMGOXmab31p2zXrsyFMCH7C0lx0bjfG7PepIlbU/nlXXy +#> it38meB2+Y1C1FnngUXeP8zV/DLkqlOfknhQ9uL6lFFps0LDkhgGebp0E5U95y2p +#> kwzqUqUboJVmTGm/rkCi7SsR0mSd763lR7ZQtKiL7gOBNEMOgB6Fj4EWeRH5+2DE +#> t4eDnCYewAj2DyufVwJsJuXATtCjZQ1MH592wNvUrX6N1d+pETJZbBKFdJ2nlE2J +#> HM4ukhWODg8RMJlTpr5tR4IgvFm4GBqdTIokWUHRoGsyEzUqxuptnKXQoC3R4N5M +#> itP5ZOUEw3UjB6FG5QBjz3owKqOftbQDVYoWNXF/OG/VKakqvmGc10bQB7BRkxEu +#> TIHwp54oHNWvoXOVWA1EzKwIkWFCv8qbXdFINFtAmEkqkA9u+6nnIgd3ghkRDMfz +#> 4okFmnjQUlMlB8IuE9JaJmrQbvGMbTjIEqs0178DsdVhQtV4qwjGMdIxGmYFNA4I +#> /bCnnPaMVbb4RlmaZBo9WDrTN9UDM5lJtBNOfKgxhTzvvo/SC183k5L3AgMBAAGj +#> VjBUMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEm/rcHWiHl253YRUpdI +#> abs0fNWBMB8GA1UdIwQYMBaAFEm/rcHWiHl253YRUpdIabs0fNWBMA0GCSqGSIb3 +#> DQEBCwUAA4ICAQCbOzDTh4dHNhN+IctvMbz8OQdzBxxTkez+GNVN270x0il0m6jO +#> Ug+UU/K7PkDKb2a/23gsHi6A1DcJGWK6/aPk6LbBcrdSvJzM2+gcdrgTs87CXbkq +#> getMX2rOA75XSoja+1sLyBEQj4j1J6TVqrMapjT+9LaK+a2VM2+kafLr9uYLGsi0 +#> DLJbt35ThbsTHmFV8EDljfw2L+jlyJPJRHTf0GGq+ZDmKlN6Az34xhTGXY+CLCZG +#> 7YmlwMuC/cfVcKnIdjCeqNKbfW1LMaFfMxj4K2KO1HmAHINogOxwrn5BFE0Uz0Nk +#> VxbH5pXNJaZpT/U+AKg2Q/f7iMCSSA8Arnb9rz++qbmSUHiAV79ErXxri4924k17 +#> mClo7vivZ+SDInTXqWVswMJYgqviFIu79IiYMM0y+hf4cs4V2dtXyfn4EllMOAp5 +#> /Nk5yRg/QmVnWOjNA2TvK53HcBw2Sg5Cu5xwkid/HCZwdOjJ148VVTPoKijFpfks +#> 8kw7TmLHgSspmyVFpmYr1XR/0YmqLrBNZyBEfTbFrdR0jVdE0c16jWaVjfzKBI8R +#> UC0CW8MO5oCYmX66Qi129zKrfZtKBViDrLmr2/dyV8wJDwHkYuhILm5Gnvy3nUSS +#> s/VQyQdu/0FYW2XunJu63GgxLorXhInPeTGXMX5FiezeBT0JFR4L3J7liQ== #> -----END CERTIFICATE----- -#> ',''),rs=c(10407,2104974505,-453851050,1928121055,-657799276,-1374136955,-78037886))" +#> ',''),rs=c(10407,1582156224,1095323329,1333058958,1835245751,1218968972,-1275976419))" ``` - The printed value may be deployed directly on a remote machine. - [« Back to ToC](#table-of-contents) #### CA Signed Certificates @@ -574,7 +521,6 @@ Similarly, functions such as `status()`, `launch_local()` or `launch_remote()` s If execution in a mirai fails, the error message is returned as a character string of class 'miraiError' and 'errorValue' to facilitate debugging. `is_mirai_error()` may be used to test for mirai execution errors. - ```r m1 <- mirai(stop("occurred with a custom message", call. = FALSE)) call_mirai(m1)$data @@ -589,18 +535,14 @@ is_mirai_error(m2$data) is_error_value(m2$data) #> [1] TRUE ``` - If a daemon instance is sent a user interrupt, the mirai will resolve to an empty character string of class 'miraiInterrupt' and 'errorValue'. `is_mirai_interrupt()` may be used to test for such interrupts. - ```r is_mirai_interrupt(m2$data) #> [1] FALSE ``` - If execution of a mirai surpasses the timeout set via the '.timeout' argument, the mirai will resolve to an 'errorValue'. This can, amongst other things, guard against mirai processes that have the potential to hang and never return. - ```r m3 <- mirai(nanonext::msleep(1000), .timeout = 500) call_mirai(m3)$data @@ -613,7 +555,6 @@ is_mirai_interrupt(m3$data) is_error_value(m3$data) #> [1] TRUE ``` - `is_error_value()` tests for all mirai execution errors, user interrupts and timeouts. [« Back to ToC](#table-of-contents) diff --git a/vignettes/mirai.Rmd.orig b/vignettes/mirai.Rmd.orig index 301179e3f..03ef6b604 100644 --- a/vignettes/mirai.Rmd.orig +++ b/vignettes/mirai.Rmd.orig @@ -36,7 +36,6 @@ Multiple long computes (model fits etc.) can be performed in parallel on availab Use `mirai()` to evaluate an expression asynchronously in a separate, clean R process. A 'mirai' object is returned immediately. - ```{r exec} library(mirai) @@ -51,30 +50,23 @@ m <- mirai( m ``` - Above, all specified `name = value` pairs are passed through to the 'mirai'. The 'mirai' yields an 'unresolved' logical NA whilst the async operation is ongoing. - ```{r do} m$data ``` ```{r dowhile, echo=FALSE} call_mirai(m) ``` - Upon completion, the 'mirai' resolves automatically to the evaluated result. - ```{r resolv} m$data |> str() ``` - Alternatively, explicitly call and wait for the result using `call_mirai()`. - ```{r call} call_mirai(m)$data |> str() ``` - For easy programmatic use of `mirai()`, '.expr' accepts a pre-constructed language object, and also a list of named arguments passed via '.args'. So, the following would be equivalent to the above: ```{r equiv} @@ -101,7 +93,6 @@ High-frequency real-time data cannot be written to file/database synchronously w Cache data in memory and use `mirai()` to perform periodic write operations concurrently in a separate process. Below, '.args' is used to pass a list of objects already present in the calling environment to the mirai by name. This is an alternative use of '.args', and may be combined with `...` to also pass in `name = value` pairs. - ```{r exec2} library(mirai) @@ -110,13 +101,11 @@ file <- tempfile() m <- mirai(write.csv(x, file = file), .args = list(x, file)) ``` - A 'mirai' object is returned immediately. `unresolved()` may be used in control flow statements to perform actions which depend on resolution of the 'mirai', both before and after. This means there is no need to actually wait (block) for a 'mirai' to resolve, as the example below demonstrates. - ```{r call2} # unresolved() queries for resolution itself so no need to use it again within the while loop @@ -128,7 +117,6 @@ while (unresolved(m)) { cat("Write complete:", is.null(m$data)) ``` - Now actions which depend on the resolution may be processed, for example the next write. [« Back to ToC](#table-of-contents) @@ -140,7 +128,6 @@ Use case: isolating code that can potentially fail in a separate process to ensu As part of a data science / machine learning pipeline, iterations of model training may periodically fail for stochastic and uncontrollable reasons (e.g. buggy memory management on graphics cards). Running each iteration in a 'mirai' isolates this potentially-problematic code such that even if it does fail, it does not bring down the entire pipeline. - ```{r exec3r} library(mirai) @@ -163,7 +150,6 @@ for (i in 1:10) { } ``` - Further, by testing the return value of each 'mirai' for errors, error-handling code is then able to automate recovery and re-attempts, as in the above example. Further details on [error handling](#errors-interrupts-and-timeouts) can be found in the section below. The end result is a resilient and fault-tolerant pipeline that minimises downtime by eliminating interruptions of long computes. @@ -179,64 +165,49 @@ This is potentially more efficient as new processes no longer need to be created #### With Dispatcher (default) Call `daemons()` specifying the number of daemons to launch. - ```{r daemons} daemons(6) ``` - ```{r daemons2, include=FALSE} -Sys.sleep(1) +Sys.sleep(1L) ``` - To view the current status, `status()` provides the number of active connections along with a matrix of statistics for each daemon. - ```{r daemons3} status() ``` - The default `dispatcher = TRUE` creates a `dispatcher()` background process that connects to individual daemon processes on the local machine. This ensures that tasks are dispatched efficiently on a first-in first-out (FIFO) basis to daemons for processing. Tasks are queued at the dispatcher and sent to a daemon as soon as it can accept the task for immediate execution. Dispatcher uses synchronisation primitives from [`nanonext`](https://doi.org/10.5281/zenodo.7903429), waiting upon rather than polling for tasks, which is efficient both in terms of consuming no resources while waiting, and also being fully synchronised with events (having no latency). - ```{r daemons4} daemons(0) ``` - Set the number of daemons to zero to reset. This reverts to the default of creating a new background process for each 'mirai' request. #### Without Dispatcher Alternatively, specifying `dispatcher = FALSE`, the background daemons connect directly to the host process. - ```{r daemonsq} daemons(6, dispatcher = FALSE) ``` ```{r daemonsq2, include=FALSE} Sys.sleep(0.5) ``` - Requesting the status now shows 6 connections, along with the host URL at `$daemons`. - ```{r daemonsqv} status() ``` - This implementation sends tasks immediately, and ensures that tasks are evenly-distributed amongst daemons. This means that optimal scheduling is not guaranteed as the duration of tasks cannot be known *a priori*. As an example, tasks could be queued at a daemon behind a long-running task, whilst other daemons are idle having already completed their tasks. The advantage of this approach is that it is low-level and does not require an additional dispatcher process. It is well-suited to working with similar-length tasks, or where the number of concurrent tasks typically does not exceed available daemons. `everywhere()` may be used to evaluate an expression on all connected daemons and persist the resultant state, regardless of a daemon's 'cleanup' setting. - ```{r everywhere} everywhere(library(parallel)) ``` - The above keeps the `parallel` package loaded for all evaluations. Other types of setup task may also be performed such as making a common resource available, etc. - ```{r daemons5} daemons(0) ``` - Set the number of daemons to zero to reset. #### Everywhere @@ -262,23 +233,17 @@ The default `dispatcher = TRUE` creates a background `dispatcher()` process on t It is recommended to use a websocket URL starting `ws://` instead of TCP in this scenario (used interchangeably with `tcp://`). A websocket URL supports a path after the port number, which can be made unique for each daemon. In this way a dispatcher can connect to an arbitrary number of daemons over a single port. Supplying a vector of URLs allows the use of arbitrary port numbers / paths. 'n' does not need to be specified if it can be inferred from the length of the 'url' vector, for example: - ```{r vectorqueue, eval=FALSE} daemons(url = c("ws://10.75.32.70:5566/cpu", "ws://10.75.32.70:5566/gpu", "ws://10.75.32.70:7788/1")) ``` - Alternatively, below a single URL is supplied, along with `n = 4` to specify that the dispatcher should listen at 4 URLs. In such a case, an integer sequence is automatically appended to the path `/1` through `/4` to produce the URLs. - ```{r localqueue} daemons(n = 4, url = host_url(port = 5555)) ``` - Requesting status on the host machine: - ```{r remotev2} status() ``` - As per the local case, `$connections` shows the single connection to dispatcher, however `$daemons` now provides a matrix of statistics for the remote daemons. - `i` index number. @@ -290,37 +255,29 @@ As per the local case, `$connections` shows the single connection to dispatcher, Dispatcher automatically adjusts to the number of daemons actually connected. Hence it is possible to dynamically scale up or down the number of daemons according to requirements (limited to the 'n' URLs assigned). To reset all connections and revert to default behaviour: - ```{r reset2} daemons(0) ``` - Closing the connection causes the dispatcher to exit automatically, and in turn all connected daemons when their respective connections with the dispatcher are terminated. #### Connecting to Remote Daemons Directly By specifying `dispatcher = FALSE`, remote daemons connect directly to the host process. The host listens at a single URL, and distributes tasks to all connected daemons. - ```{r remote} daemons(url = host_url(), dispatcher = FALSE) ``` - Note that above, calling `host_url()` without a port value uses the default of '0'. This is a wildcard value that will automatically cause a free ephemeral port to be assigned. The actual assigned port is provided in the return value of the call, or it may be queried at any time via `status()`. The number of daemons connecting to the host URL is not limited and network resources may be added or removed at any time, with tasks automatically distributed to all connected daemons. `$connections` will show the actual number of connected daemons. - ```{r remotev} status() ``` - To reset all connections and revert to default behaviour: - ```{r reset} daemons(0) ``` - This causes all connected daemons to exit automatically. [« Back to ToC](#table-of-contents) @@ -334,7 +291,6 @@ To launch remote daemons, supply a remote launch configuration to the 'remote' a #### SSH Direct Connection The first example below launches 4 daemons on the machine 10.75.32.90 (using the default SSH port of 22 as this was not specified), connecting back to the dispatcher URLs: - ```{r ldmn, eval=FALSE} daemons( n = 4, @@ -343,7 +299,6 @@ daemons( ) ``` The second example below launches one daemon on each of 10.75.32.90 and 10.75.32.91 using the custom SSH port of 222: - ```{r ldmnd, eval=FALSE} daemons( n = 2, @@ -362,7 +317,6 @@ In these cases SSH tunnelling offers a solution by creating a tunnel once the in Tunnelling requires the hostname for 'url' specified when setting up daemons to be either '127.0.0.1' or 'localhost'. This is as the tunnel is created between 127.0.0.1:port or equivalently localhost:port on each machine. The host listens to its localhost:port and the remotes each dial into localhost:port on their own respective machines. The below example launches 2 nodes on the remote machine 10.75.32.90 using SSH tunnelling over port 5555 ('url' hostname is specified as 'localhost'): - ```{r sshrevtun, eval=FALSE} daemons( url = "tcp://localhost:5555", @@ -376,7 +330,6 @@ daemons( #### Manual Deployment As an alternative to automated launches, calling `launch_remote()` without specifying 'remote' may be used to return the shell commands for deploying daemons manually. The printed return values may be copy / pasted directly to a remote machine. - ```{r launchremotereal} daemons(n = 2, url = host_url()) @@ -384,7 +337,6 @@ launch_remote(1:2) daemons(0) ``` - Note that `daemons()` should be set up on the host machine before launching `daemon()` on remote resources, otherwise the daemon instances will exit if a connection is not immediately available. Alternatively, specifying the argument `autoexit = FALSE` will allow daemons to wait (indefinitely) for a connection to become available. [« Back to ToC](#table-of-contents) @@ -396,21 +348,16 @@ TLS is available as an option to secure communications from the local machine to #### Zero-configuration An automatic zero-configuration default is implemented. Simply specify a secure URL of the form `wss://` or `tls+tcp://` when setting daemons, or use `host_url(tls = TRUE)`, for example: - ```{r tlsremote} daemons(n = 4, url = host_url(ws = TRUE, tls = TRUE)) ``` - Single-use keys and certificates are automatically generated and configured, without requiring any further intervention. The private key is always retained on the host machine and never transmitted. The generated self-signed certificate is available via `launch_remote()`. This function conveniently constructs the full shell command to launch a daemon, including the correctly specified 'tls' argument to `daemon()`. - ```{r launch_remote} launch_remote(1) ``` - The printed value may be deployed directly on a remote machine. - ```{r tlsclose, include=FALSE} daemons(0) ``` @@ -464,7 +411,6 @@ Similarly, functions such as `status()`, `launch_local()` or `launch_remote()` s ### Errors, Interrupts and Timeouts If execution in a mirai fails, the error message is returned as a character string of class 'miraiError' and 'errorValue' to facilitate debugging. `is_mirai_error()` may be used to test for mirai execution errors. - ```{r errorexample} m1 <- mirai(stop("occurred with a custom message", call. = FALSE)) call_mirai(m1)$data @@ -475,15 +421,11 @@ call_mirai(m2)$data is_mirai_error(m2$data) is_error_value(m2$data) ``` - If a daemon instance is sent a user interrupt, the mirai will resolve to an empty character string of class 'miraiInterrupt' and 'errorValue'. `is_mirai_interrupt()` may be used to test for such interrupts. - ```{r interruptexample} is_mirai_interrupt(m2$data) ``` - If execution of a mirai surpasses the timeout set via the '.timeout' argument, the mirai will resolve to an 'errorValue'. This can, amongst other things, guard against mirai processes that have the potential to hang and never return. - ```{r timeouts} m3 <- mirai(nanonext::msleep(1000), .timeout = 500) call_mirai(m3)$data @@ -492,7 +434,6 @@ is_mirai_error(m3$data) is_mirai_interrupt(m3$data) is_error_value(m3$data) ``` - `is_error_value()` tests for all mirai execution errors, user interrupts and timeouts. [« Back to ToC](#table-of-contents) diff --git a/vignettes/parallel.Rmd b/vignettes/parallel.Rmd index 3d4aac3e3..51d94095d 100644 --- a/vignettes/parallel.Rmd +++ b/vignettes/parallel.Rmd @@ -7,17 +7,17 @@ vignette: > --- - ### Parallel Integration -`mirai` provides an (experimental) alternative communications backend for R. This functionality was developed to fulfil a request by R Core at R Project Sprint 2023. +`mirai` provides an alternative communications backend for R. This functionality was developed to fulfil a request by R Core at R Project Sprint 2023. -`make_cluster()` creates a cluster object of class 'miraiCluster', which may be used for any function in the `parallel` base package such as `parallel::clusterApply()`, `parallel::parLapply()` or the load-balanced version `parallel::parLapplyLB()`. +`make_cluster()` creates a cluster object of class 'miraiCluster', which is fully-compatible with `parallel` cluster types. + Specify 'n' to launch nodes on the local machine. + Specify 'url' for receiving connections from remote nodes. + Optionally, specify 'remote' to launch remote daemons using a remote configuration generated by `remote_config()` or `ssh_config()`. +Created clusters may be used for any function in the `parallel` base package such as `parallel::clusterApply()` or `parallel::parLapply()`, or the load-balanced versions such as `parallel::parLapplyLB()`. ```r library(mirai) @@ -45,33 +45,27 @@ parallel::parLapply(cl, iris, mean) `status()` may be called on a 'miraiCluster` to query the number of connected nodes at any time. - ```r status(cl) #> $connections #> [1] 4 #> #> $daemons -#> [1] "abstract://9d3563c19de6181b72e407a5" +#> [1] "abstract://3d4a4402496bfa98f98d2c2c" stop_cluster(cl) ``` -Specifying 'url' without 'remote' causes the shell commands for manual deployment of nodes to be printed to the console. - +Making a cluster specifying 'url' without 'remote' causes the shell commands for manual deployment of nodes to be printed to the console. ```r cl <- make_cluster(n = 2, url = host_url()) #> Shell commands for deployment on nodes: #> #> [1] -#> Rscript -e "mirai::daemon('tcp://hostname:37197',rs=c(10407,872660769,-1764378002,1577836567,-906322580,354061181,-892072934))" +#> Rscript -e "mirai::daemon('tcp://hostname:46385',rs=c(10407,1330589964,-1191976291,1617653946,461158131,253482904,1523997881))" #> #> [2] -#> Rscript -e "mirai::daemon('tcp://hostname:37197',rs=c(10407,1038611314,2079194929,-852266189,494315566,1464442287,-73856997))" +#> Rscript -e "mirai::daemon('tcp://hostname:46385',rs=c(10407,749396609,-578276799,2131323632,1725456233,-1056170017,453431426))" stop_cluster(cl) ``` - -Created clusters are fully compatible with `parallel` cluster types. - -They may be registered by package [`doParallel`](https://cran.r-project.org/package=doParallel) for use with the [`foreach`](https://cran.r-project.org/package=foreach) package. diff --git a/vignettes/parallel.Rmd.orig b/vignettes/parallel.Rmd.orig index 41b3499ce..248881a3d 100644 --- a/vignettes/parallel.Rmd.orig +++ b/vignettes/parallel.Rmd.orig @@ -13,17 +13,17 @@ knitr::opts_chunk$set( out.width = "100%" ) ``` - ### Parallel Integration -`mirai` provides an (experimental) alternative communications backend for R. This functionality was developed to fulfil a request by R Core at R Project Sprint 2023. +`mirai` provides an alternative communications backend for R. This functionality was developed to fulfil a request by R Core at R Project Sprint 2023. -`make_cluster()` creates a cluster object of class 'miraiCluster', which may be used for any function in the `parallel` base package such as `parallel::clusterApply()`, `parallel::parLapply()` or the load-balanced version `parallel::parLapplyLB()`. +`make_cluster()` creates a cluster object of class 'miraiCluster', which is fully-compatible with `parallel` cluster types. + Specify 'n' to launch nodes on the local machine. + Specify 'url' for receiving connections from remote nodes. + Optionally, specify 'remote' to launch remote daemons using a remote configuration generated by `remote_config()` or `ssh_config()`. +Created clusters may be used for any function in the `parallel` base package such as `parallel::clusterApply()` or `parallel::parLapply()`, or the load-balanced versions such as `parallel::parLapplyLB()`. ```{r cluster} library(mirai) @@ -36,20 +36,14 @@ parallel::parLapply(cl, iris, mean) Sys.sleep(1L) ``` `status()` may be called on a 'miraiCluster` to query the number of connected nodes at any time. - ```{r cluster2} status(cl) stop_cluster(cl) ``` -Specifying 'url' without 'remote' causes the shell commands for manual deployment of nodes to be printed to the console. - +Making a cluster specifying 'url' without 'remote' causes the shell commands for manual deployment of nodes to be printed to the console. ```{r cluster3} cl <- make_cluster(n = 2, url = host_url()) stop_cluster(cl) ``` - -Created clusters are fully compatible with `parallel` cluster types. - -They may be registered by package [`doParallel`](https://cran.r-project.org/package=doParallel) for use with the [`foreach`](https://cran.r-project.org/package=foreach) package. diff --git a/vignettes/plumber.Rmd b/vignettes/plumber.Rmd index 68ca2a6ae..e07f33adb 100644 --- a/vignettes/plumber.Rmd +++ b/vignettes/plumber.Rmd @@ -7,25 +7,22 @@ vignette: > --- - ### Plumber Integration -'mirai' supplies its own `as.promise()` method, allowing it to be used as a promise. - -A 'mirai' may be piped directly using the promise pipe `&...>%`, which implicitly calls `as.promise()` on the 'mirai', or converted into a promise by `as.promise()`, which then allows using the methods `$then()`, `$finally()` etc. +{mirai} may be used as an asynchronous / distributed backend for [{plumber}](https://cran.r-project.org/package=plumber) pipelines. -Below we provide example usage of how to use mirai as an async distributed backend for {plumber} pipelines. - -The plumber router code is run in a daemon process itself so that it does not block the interactive process. It is important to set up this daemon with the argument `autoexit = tools::SIGINT`, so that the plumber server is interrupted and exits cleanly when we tear it down. +Example usage is provided below for different types of endpoint. #### Example GET Endpoint -The /echo endpoint takes a GET request, sleeps for 1 second (simulating an expensive computation) and just returns the 'msg' request header together with a timestamp and the process ID of the process it is run on. +The plumber router code is run in a daemon process itself so that it does not block the interactive process. +The /echo endpoint takes a GET request, sleeps for 1 second (simulating an expensive computation) and simply returns the 'msg' request header together with a timestamp and the process ID of the process it is run on. ```r library(mirai) +# important to supply SIGINT so the plumber server is interrupted and exits cleanly when torn down. daemons(1L, dispatcher = FALSE, autoexit = tools::SIGINT) #> [1] 1 @@ -34,8 +31,8 @@ m <- mirai({ library(promises) # to provide the promise pipe library(mirai) - daemons(4L, dispatcher = FALSE) # handles 4 requests simultaneously # does not use dispatcher (suitable when all requests require similar compute) + daemons(4L, dispatcher = FALSE) # handles 4 requests simultaneously pr() |> pr_get( @@ -61,8 +58,7 @@ m <- mirai({ The API can be queried using an async HTTP client such as `nanonext::ncurl_aio()`. -Here, all 8 requests are submitted at once, but we note that that responses have differing timestamps as only 4 can be processed at any given time. - +Here, all 8 requests are submitted at once, but we note that that responses have differing timestamps as only 4 can be processed at any one time (limited by the number of daemons set). ```r library(nanonext) @@ -71,14 +67,14 @@ res <- lapply(1:8, headers = c(msg = as.character(i)))) res <- lapply(res, call_aio) for (r in res) print(r$data) -#> [1] "{\"time\":[\"2024-01-10 22:18:07\"],\"msg\":[\"1\"],\"pid\":[20080]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:07\"],\"msg\":[\"2\"],\"pid\":[20087]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:07\"],\"msg\":[\"3\"],\"pid\":[20082]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:08\"],\"msg\":[\"4\"],\"pid\":[20080]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:07\"],\"msg\":[\"5\"],\"pid\":[20084]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:08\"],\"msg\":[\"6\"],\"pid\":[20084]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:08\"],\"msg\":[\"7\"],\"pid\":[20082]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:08\"],\"msg\":[\"8\"],\"pid\":[20087]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:11\"],\"msg\":[\"1\"],\"pid\":[34995]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:11\"],\"msg\":[\"2\"],\"pid\":[34987]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:11\"],\"msg\":[\"3\"],\"pid\":[34992]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:11\"],\"msg\":[\"4\"],\"pid\":[34989]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:12\"],\"msg\":[\"5\"],\"pid\":[34989]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:12\"],\"msg\":[\"6\"],\"pid\":[34987]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:12\"],\"msg\":[\"7\"],\"pid\":[34992]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:12\"],\"msg\":[\"8\"],\"pid\":[34995]}" daemons(0) #> [1] 0 @@ -90,10 +86,10 @@ Below is a demonstration of the equivalent using a POST endpoint, accepting a JS It is important to note in this case that `req$postBody` should be accessed in the router process and passed in as an argument to the 'mirai' as this is retrieved using a connection that is not serializable. - ```r library(mirai) +# important to supply SIGINT so the plumber server is interrupted and exits cleanly when torn down. daemons(1L, dispatcher = FALSE, autoexit = tools::SIGINT) #> [1] 1 @@ -102,8 +98,8 @@ m <- mirai({ library(promises) # to provide the promise pipe library(mirai) - daemons(4L) # handles 4 requests simultaneously # uses dispatcher (suitable for requests with differing compute lengths) + daemons(4L, dispatcher = TRUE) # handles 4 requests simultaneously pr() |> pr_post( @@ -130,7 +126,6 @@ m <- mirai({ Querying the endpoint produces the same set of outputs as the previous example. - ```r library(nanonext) res <- lapply(1:8, @@ -139,14 +134,14 @@ res <- lapply(1:8, data = sprintf('{"msg":"%d"}', i))) res <- lapply(res, call_aio) for (r in res) print(r$data) -#> [1] "{\"time\":[\"2024-01-10 22:18:11\"],\"msg\":[\"1\"],\"pid\":[20357]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:11\"],\"msg\":[\"2\"],\"pid\":[20359]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:11\"],\"msg\":[\"3\"],\"pid\":[20362]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:11\"],\"msg\":[\"4\"],\"pid\":[20366]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:12\"],\"msg\":[\"5\"],\"pid\":[20357]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:12\"],\"msg\":[\"6\"],\"pid\":[20359]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:12\"],\"msg\":[\"7\"],\"pid\":[20366]}" -#> [1] "{\"time\":[\"2024-01-10 22:18:12\"],\"msg\":[\"8\"],\"pid\":[20362]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:15\"],\"msg\":[\"1\"],\"pid\":[35264]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:16\"],\"msg\":[\"2\"],\"pid\":[35269]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:15\"],\"msg\":[\"3\"],\"pid\":[35266]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:16\"],\"msg\":[\"4\"],\"pid\":[35264]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:15\"],\"msg\":[\"5\"],\"pid\":[35269]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:15\"],\"msg\":[\"6\"],\"pid\":[35272]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:16\"],\"msg\":[\"7\"],\"pid\":[35272]}" +#> [1] "{\"time\":[\"2024-01-11 12:16:16\"],\"msg\":[\"8\"],\"pid\":[35266]}" daemons(0) #> [1] 0 diff --git a/vignettes/plumber.Rmd.orig b/vignettes/plumber.Rmd.orig index 6464081ca..974698b5b 100644 --- a/vignettes/plumber.Rmd.orig +++ b/vignettes/plumber.Rmd.orig @@ -13,24 +13,21 @@ knitr::opts_chunk$set( out.width = "100%" ) ``` - ### Plumber Integration -'mirai' supplies its own `as.promise()` method, allowing it to be used as a promise. - -A 'mirai' may be piped directly using the promise pipe `&...>%`, which implicitly calls `as.promise()` on the 'mirai', or converted into a promise by `as.promise()`, which then allows using the methods `$then()`, `$finally()` etc. +{mirai} may be used as an asynchronous / distributed backend for [{plumber}](https://cran.r-project.org/package=plumber) pipelines. -Below we provide example usage of how to use mirai as an async distributed backend for {plumber} pipelines. - -The plumber router code is run in a daemon process itself so that it does not block the interactive process. It is important to set up this daemon with the argument `autoexit = tools::SIGINT`, so that the plumber server is interrupted and exits cleanly when we tear it down. +Example usage is provided below for different types of endpoint. #### Example GET Endpoint -The /echo endpoint takes a GET request, sleeps for 1 second (simulating an expensive computation) and just returns the 'msg' request header together with a timestamp and the process ID of the process it is run on. +The plumber router code is run in a daemon process itself so that it does not block the interactive process. +The /echo endpoint takes a GET request, sleeps for 1 second (simulating an expensive computation) and simply returns the 'msg' request header together with a timestamp and the process ID of the process it is run on. ```{r get} library(mirai) +# important to supply SIGINT so the plumber server is interrupted and exits cleanly when torn down. daemons(1L, dispatcher = FALSE, autoexit = tools::SIGINT) m <- mirai({ @@ -38,8 +35,8 @@ m <- mirai({ library(promises) # to provide the promise pipe library(mirai) - daemons(4L, dispatcher = FALSE) # handles 4 requests simultaneously # does not use dispatcher (suitable when all requests require similar compute) + daemons(4L, dispatcher = FALSE) # handles 4 requests simultaneously pr() |> pr_get( @@ -67,8 +64,7 @@ Sys.sleep(2) ``` The API can be queried using an async HTTP client such as `nanonext::ncurl_aio()`. -Here, all 8 requests are submitted at once, but we note that that responses have differing timestamps as only 4 can be processed at any given time. - +Here, all 8 requests are submitted at once, but we note that that responses have differing timestamps as only 4 can be processed at any one time (limited by the number of daemons set). ```{r queryapi} library(nanonext) res <- lapply(1:8, @@ -85,10 +81,10 @@ daemons(0) Below is a demonstration of the equivalent using a POST endpoint, accepting a JSON instruction sent as request data. It is important to note in this case that `req$postBody` should be accessed in the router process and passed in as an argument to the 'mirai' as this is retrieved using a connection that is not serializable. - ```{r post} library(mirai) +# important to supply SIGINT so the plumber server is interrupted and exits cleanly when torn down. daemons(1L, dispatcher = FALSE, autoexit = tools::SIGINT) m <- mirai({ @@ -96,8 +92,8 @@ m <- mirai({ library(promises) # to provide the promise pipe library(mirai) - daemons(4L) # handles 4 requests simultaneously # uses dispatcher (suitable for requests with differing compute lengths) + daemons(4L, dispatcher = TRUE) # handles 4 requests simultaneously pr() |> pr_post( @@ -125,7 +121,6 @@ m <- mirai({ Sys.sleep(2) ``` Querying the endpoint produces the same set of outputs as the previous example. - ```{r queryapipost} library(nanonext) res <- lapply(1:8, diff --git a/vignettes/precompile.R b/vignettes/precompile.R index 6bd3dcaaf..661a9321e 100644 --- a/vignettes/precompile.R +++ b/vignettes/precompile.R @@ -2,4 +2,6 @@ knitr::knit("vignettes/mirai.Rmd.orig", "vignettes/mirai.Rmd") knitr::knit("vignettes/parallel.Rmd.orig", "vignettes/parallel.Rmd") knitr::knit("vignettes/plumber.Rmd.orig", "vignettes/plumber.Rmd") +knitr::knit("vignettes/promises.Rmd.orig", "vignettes/promises.Rmd") +# Not pre-compiled: vignettes/shiny.Rmd knitr::knit("vignettes/torch.Rmd.orig", "vignettes/torch.Rmd") diff --git a/vignettes/promises.Rmd b/vignettes/promises.Rmd new file mode 100644 index 000000000..a3a80d2f2 --- /dev/null +++ b/vignettes/promises.Rmd @@ -0,0 +1,39 @@ +--- +title: "mirai - Promises Integration" +vignette: > + %\VignetteIndexEntry{mirai - Promises Integration} + %\VignetteEngine{knitr::knitr} + %\VignetteEncoding{UTF-8} +--- + + +### Promises Integration + +{mirai} supplies its own `as.promise()` method, allowing it to be used as a promise. + +A 'mirai' may be piped directly using the promise pipe `&...>%`, which implicitly calls `as.promise()` on the 'mirai'. + +Alternatively, it may be converted into a promise by `as.promise()`, which then allows using the methods `$then()`, `$finally()` etc. + +The following example outputs "hello" to the console after one second when the 'mirai' resolves. + +```r +library(promises) +p <- mirai({Sys.sleep(1); "hello"}) %...>% cat() +p +#> +``` +It is possible to both access a 'mirai' value at `$data` and to use a promise for enacting a side effect (assigning the value to an environment in the example below). + +```r +library(promises) +env <- new.env() + +m <- mirai({Sys.sleep(1); "hello"}) +p <- as.promise(m)$then(function(x) env[["response"]] <- x) + +call_mirai(m)$data +#> [1] "hello" +env[["response"]] +#> [1] "hello" +``` diff --git a/vignettes/promises.Rmd.orig b/vignettes/promises.Rmd.orig new file mode 100644 index 000000000..4120e0714 --- /dev/null +++ b/vignettes/promises.Rmd.orig @@ -0,0 +1,40 @@ +--- +title: "mirai - Promises Integration" +vignette: > + %\VignetteIndexEntry{mirai - Promises Integration} + %\VignetteEngine{knitr::knitr} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + out.width = "100%" +) +``` +### Promises Integration + +{mirai} supplies its own `as.promise()` method, allowing it to be used as a promise. + +A 'mirai' may be piped directly using the promise pipe `&...>%`, which implicitly calls `as.promise()` on the 'mirai'. + +Alternatively, it may be converted into a promise by `as.promise()`, which then allows using the methods `$then()`, `$finally()` etc. + +The following example outputs "hello" to the console after one second when the 'mirai' resolves. +```{r promises} +library(promises) +p <- mirai({Sys.sleep(1); "hello"}) %...>% cat() +p +``` +It is possible to both access a 'mirai' value at `$data` and to use a promise for enacting a side effect (assigning the value to an environment in the example below). +```{r promisesalt} +library(promises) +env <- new.env() + +m <- mirai({Sys.sleep(1); "hello"}) +p <- as.promise(m)$then(function(x) env[["response"]] <- x) + +call_mirai(m)$data +env[["response"]] +``` diff --git a/vignettes/shiny.Rmd b/vignettes/shiny.Rmd index 297cc6554..37ea96b07 100644 --- a/vignettes/shiny.Rmd +++ b/vignettes/shiny.Rmd @@ -13,16 +13,15 @@ knitr::opts_chunk$set( out.width = "100%" ) ``` - ### Shiny Integration #### Shiny Example Usage -{mirai} may be used to scale enterprise Shiny applications and plugs straight into the reactive framework, without the need to use promises. +{mirai} may be used as an asynchronous / distributed backend to scale [{shiny}](https://cran.r-project.org/package=shiny) applications and plugs directly into the reactive framework, without the need to use promises. Because of this, it is especially suited to large-scale enterprise applications. The following example has a button to submit tasks, which will be processed by one of 5 daemons, outputting a pretty spiral pattern upon completion. -You will find that if you submit more than 5 tasks that the chart updates 5 at a time, limited by the parallel capacity of the application. +You will find that if you submit more than 5 tasks, the chart updates 5 at a time, limited by the number of daemons set (the parallel-processing capacity of the application). ```{r shiny, eval=FALSE} library(mirai) @@ -95,7 +94,7 @@ server <- function(input, output, session) { shinyApp(ui = ui, server = server) ``` -*Thanks to Daniel Woodie and William Landau for providing the original example on which this is based.* +*Thanks to Daniel Woodie and William Landau for providing the original example on which this is based. Please see which also has examples of the fantastic artwork produced.* #### Example Using Promises @@ -103,9 +102,9 @@ Alternatively, a 'mirai' may be used as a promise as it supplies its own `as.pro A 'mirai' may be piped directly using the promise pipe `&...>%`, which implicitly calls `as.promise()` on the 'mirai', or converted into a promise by `as.promise()`, which then allows using the methods `$then()`, `$finally()` etc. -The below example simulates a plot function requiring a long compute in a ‘shiny’ app. +The below example simulates a plot function requiring a long compute in a ‘shiny’ application. -This app takes c. 2s to start compared to the 8s it would otherwise take if not running in parallel. +The application starts in around 2s rather than the 8s it would take if not running in parallel. ```{r shinypromises, eval=FALSE} library(shiny) diff --git a/vignettes/torch.Rmd b/vignettes/torch.Rmd index 39afd94d8..f574960ea 100644 --- a/vignettes/torch.Rmd +++ b/vignettes/torch.Rmd @@ -7,18 +7,19 @@ vignette: > --- - ### Torch Integration Custom serialization functions may be registered to handle external pointer type reference objects. -This allows tensors from the [`torch`](https://cran.r-project.org/package=torch) package to be used seamlessly in 'mirai' computations. +This allows tensors from the [{torch}](https://cran.r-project.org/package=torch) package to be used seamlessly in 'mirai' computations. + +#### Setup Steps -1. Register the serialization and unserialization functions as a list supplied to the 'refhook' argument of `serialization()`. +1. Register the serialization and unserialization functions as a list supplied to `serialization()`. 2. Set up dameons - this may be done before or after setting `serialization()`. -3. Use `everywhere()` to make the 'torch' package available on all dameons (for convenience, optional). +3. Use `everywhere()` to make the {torch} package available on all dameons (for convenience, optional). ```r @@ -28,9 +29,13 @@ daemons(1) #> [1] 1 everywhere(library(torch)) ``` +#### Example Usage -The below example creates a convolutional neural network using `torch::nn_module()`, which is then passed a set of parameters and initialized within a 'mirai'. +The below example creates a convolutional neural network using `torch::nn_module()`. +A set of model parameters is also specified. + +The model specification and parameters are then passed to and initialized within a 'mirai'. ```r model <- nn_module( @@ -50,6 +55,7 @@ model <- nn_module( params <- list(in_size = 1, out_size = 20) m <- mirai(do.call(model, params), .args = list(model, params)) + call_mirai(m)$data #> An `nn_module` containing 1,040 parameters. #> @@ -59,49 +65,49 @@ call_mirai(m)$data ``` The returned model is an object containing many tensor elements. - ```r m$data$parameters$conv1.weight #> torch_tensor #> (1,1,.,.) = -#> -0.1205 -0.1757 -0.0501 0.1389 0.0922 -#> -0.1649 -0.1273 0.0191 0.1186 -0.0943 -#> 0.1665 -0.0876 -0.1023 0.0301 0.1141 -#> 0.0322 0.0580 0.0123 -0.0415 -0.1614 -#> 0.1639 0.1523 -0.1057 -0.0188 -0.0248 +#> -0.0679 0.1029 0.1385 0.1610 -0.0206 +#> -0.0851 -0.0514 -0.1901 0.0832 0.1121 +#> 0.1122 0.0309 -0.1937 -0.1249 0.0343 +#> 0.1398 -0.0798 0.0933 0.0672 -0.0778 +#> -0.0879 -0.0727 -0.1004 0.1328 0.0231 #> #> (2,1,.,.) = -#> -0.1008 0.1984 -0.0736 -0.1917 0.1427 -#> 0.0703 0.0128 0.0620 -0.0524 -0.0565 -#> 0.0750 -0.0657 0.1786 -0.1733 -0.0639 -#> -0.1578 0.1989 0.1473 -0.1071 0.0607 -#> -0.1334 -0.1061 -0.1487 -0.1097 -0.0244 +#> 0.1520 -0.0140 0.0621 0.1891 -0.0192 +#> 0.1381 0.1579 0.0415 0.1586 -0.1211 +#> -0.0809 -0.0243 -0.1879 -0.1244 -0.1937 +#> 0.0408 0.1539 -0.0597 -0.1490 0.1888 +#> 0.1035 0.0616 0.0775 0.0979 -0.1342 #> #> (3,1,.,.) = -#> 0.0814 0.1406 0.0929 0.0101 0.0254 -#> 0.0645 0.0082 -0.1432 -0.0091 -0.1674 -#> 0.0242 -0.1321 -0.1690 -0.0254 -0.1345 -#> -0.1011 0.0083 -0.1139 -0.0948 -0.0941 -#> -0.0352 -0.1540 0.0097 -0.1977 0.0735 +#> 0.0944 -0.1438 -0.0957 -0.1855 -0.1500 +#> -0.1392 -0.1959 -0.0065 0.1261 -0.1844 +#> 0.1375 0.0872 -0.0247 -0.1190 0.1837 +#> 0.0579 -0.0641 0.1278 -0.1847 -0.1106 +#> -0.0202 -0.1681 0.0986 -0.1300 0.1095 #> #> (4,1,.,.) = -#> 0.1920 0.1211 0.1399 0.1913 -0.0115 -#> -0.1858 0.0491 -0.0151 -0.0219 -0.1164 -#> -0.1854 0.0309 -0.0076 0.0381 -0.0900 -#> 0.0488 0.1269 0.0352 0.0338 -0.0510 -#> -0.0565 -0.0516 -0.0590 0.1656 -0.1994 +#> 0.0105 0.0562 0.1678 0.1800 0.1642 +#> 0.1107 -0.0196 -0.0244 -0.1933 -0.1752 +#> -0.1280 0.0362 -0.1671 -0.0661 0.1871 +#> -0.0716 0.1232 0.0725 -0.0820 0.0994 +#> -0.1820 -0.1687 0.1173 0.1916 -0.1074 #> #> (5,1,.,.) = -#> 0.1760 -0.0951 -0.0583 0.1938 0.0130 +#> 0.1298 0.1826 0.1107 -0.0559 0.1817 #> ... [the output was truncated (use n=-1 to disable)] #> [ CPUFloatType{20,1,5,5} ][ requires_grad = TRUE ] ``` +It is usual for model parameters to then be passed to an optimiser. -The model parameters can then be further passed to an optimiser, with that also initialized within a 'mirai' process. - +This can also be initialized within a 'mirai' process. ```r optim <- mirai(optim_rmsprop(params = params), params = m$data$parameters) + call_mirai(optim)$data #> #> Inherits from: @@ -124,4 +130,6 @@ daemons(0) ``` Above, tensors and complex objects containing tensors were passed seamlessly between host and daemon processes, in the same way as any other R object. -The implementation leverages R's own native 'refhook' mechanism to allow such completely transparent usage, and is designed to be fast and efficient using the serialization methods from the 'torch' package directly, minimising data copies where possible. +The custom serialization in {mirai} leverages R's own native 'refhook' mechanism to allow such completely transparent usage. + +It is designed to be fast and efficient, minimising data copies and using the serialization methods from the 'torch' package directly. diff --git a/vignettes/torch.Rmd.orig b/vignettes/torch.Rmd.orig index 23655474e..141150fc8 100644 --- a/vignettes/torch.Rmd.orig +++ b/vignettes/torch.Rmd.orig @@ -13,18 +13,19 @@ knitr::opts_chunk$set( out.width = "100%" ) ``` - ### Torch Integration Custom serialization functions may be registered to handle external pointer type reference objects. -This allows tensors from the [`torch`](https://cran.r-project.org/package=torch) package to be used seamlessly in 'mirai' computations. +This allows tensors from the [{torch}](https://cran.r-project.org/package=torch) package to be used seamlessly in 'mirai' computations. + +#### Setup Steps -1. Register the serialization and unserialization functions as a list supplied to the 'refhook' argument of `serialization()`. +1. Register the serialization and unserialization functions as a list supplied to `serialization()`. 2. Set up dameons - this may be done before or after setting `serialization()`. -3. Use `everywhere()` to make the 'torch' package available on all dameons (for convenience, optional). +3. Use `everywhere()` to make the {torch} package available on all dameons (for convenience, optional). ```{r tensors} library(torch) @@ -32,9 +33,13 @@ serialization(refhook = list(torch:::torch_serialize, torch::torch_load)) daemons(1) everywhere(library(torch)) ``` +#### Example Usage -The below example creates a convolutional neural network using `torch::nn_module()`, which is then passed a set of parameters and initialized within a 'mirai'. +The below example creates a convolutional neural network using `torch::nn_module()`. +A set of model parameters is also specified. + +The model specification and parameters are then passed to and initialized within a 'mirai'. ```{r tensors2} model <- nn_module( initialize = function(in_size, out_size) { @@ -53,22 +58,25 @@ model <- nn_module( params <- list(in_size = 1, out_size = 20) m <- mirai(do.call(model, params), .args = list(model, params)) + call_mirai(m)$data ``` The returned model is an object containing many tensor elements. - ```{r tensors3} m$data$parameters$conv1.weight ``` +It is usual for model parameters to then be passed to an optimiser. -The model parameters can then be further passed to an optimiser, with that also initialized within a 'mirai' process. - +This can also be initialized within a 'mirai' process. ```{r tensors4} optim <- mirai(optim_rmsprop(params = params), params = m$data$parameters) + call_mirai(optim)$data daemons(0) ``` Above, tensors and complex objects containing tensors were passed seamlessly between host and daemon processes, in the same way as any other R object. -The implementation leverages R's own native 'refhook' mechanism to allow such completely transparent usage, and is designed to be fast and efficient using the serialization methods from the 'torch' package directly, minimising data copies where possible. +The custom serialization in {mirai} leverages R's own native 'refhook' mechanism to allow such completely transparent usage. + +It is designed to be fast and efficient, minimising data copies and using the serialization methods from the 'torch' package directly.