--- title: "mirai - Serialization (Arrow, ADBC, polars, torch)" vignette: > %\VignetteIndexEntry{mirai - Serialization (Arrow, ADBC, polars, torch)} %\VignetteEngine{litedown::vignette} %\VignetteEncoding{UTF-8} --- ### 1. Serialization: Arrow, polars and beyond Native R serialization transfers data between host and daemons. Objects accessed via external pointers cannot be serialized and normally error in mirai operations. Using [`arrow`](https://arrow.apache.org/docs/r/) as an example: ``` r library(mirai) library(arrow, warn.conflicts = FALSE) daemons(1) everywhere(library(arrow)) x <- as_arrow_table(iris) m <- mirai(list(a = head(x), b = "some text"), x = x) m[] #> 'miraiError' chr Error: Invalid , external pointer to null daemons(0) ``` `serial_config()` creates custom serialization configurations with functions that hook into R's native serialization mechanism for reference objects ('refhooks'). Pass this configuration to the 'serial' argument of `daemons()`: ``` r cfg <- serial_config( "ArrowTabular", arrow::write_to_raw, \(x) arrow::read_ipc_stream(x, as_data_frame = FALSE) ) daemons(1, serial = cfg) everywhere(library(arrow)) m <- mirai(list(a = head(x), b = "some text"), x = x) m[] #> $a #> Table #> 6 rows x 5 columns #> $Sepal.Length #> $Sepal.Width #> $Petal.Length #> $Petal.Width #> $Species > #> #> See $metadata for additional Schema metadata #> #> $b #> [1] "some text" daemons(0) ``` The arrow table now handles seamlessly, even when deeply nested in lists or other structures. Register multiple serialization functions for different object classes. This example combines Arrow with [`polars`](https://pola-rs.github.io/r-polars/), a Rust dataframe library (requires polars >= 1.0.0): ``` r daemons( n = 1, serial = serial_config( c("ArrowTabular", "polars_data_frame"), list(arrow::write_to_raw, \(x) x$serialize()), list(\(x) arrow::read_ipc_stream(x, as_data_frame = FALSE), polars::pl$deserialize_df) ) ) x <- polars::as_polars_df(iris) m <- mirai(list(a = head(x), b = "some text"), x = x) m[] #> $a #> shape: (6, 5) #> ┌──────────────┬─────────────┬──────────────┬─────────────┬─────────┐ #> │ Sepal.Length ┆ Sepal.Width ┆ Petal.Length ┆ Petal.Width ┆ Species │ #> │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ #> │ f64 ┆ f64 ┆ f64 ┆ f64 ┆ cat │ #> ╞══════════════╪═════════════╪══════════════╪═════════════╪═════════╡ #> │ 5.1 ┆ 3.5 ┆ 1.4 ┆ 0.2 ┆ setosa │ #> │ 4.9 ┆ 3.0 ┆ 1.4 ┆ 0.2 ┆ setosa │ #> │ 4.7 ┆ 3.2 ┆ 1.3 ┆ 0.2 ┆ setosa │ #> │ 4.6 ┆ 3.1 ┆ 1.5 ┆ 0.2 ┆ setosa │ #> │ 5.0 ┆ 3.6 ┆ 1.4 ┆ 0.2 ┆ setosa │ #> │ 5.4 ┆ 3.9 ┆ 1.7 ┆ 0.4 ┆ setosa │ #> └──────────────┴─────────────┴──────────────┴─────────────┴─────────┘ #> #> $b #> [1] "some text" daemons(0) ``` ### 2. Serialization: Torch [`torch`](https://torch.mlverse.org/) tensors work seamlessly in mirai computations. **Setup:** 1. Create serialization configuration with 'class' as 'torch_tensor' 2. Set up daemons, supplying configuration to 'serial' 3. (Optional) Use `everywhere()` to load `torch` on all daemons ``` r library(mirai) library(torch) cfg <- serial_config( class = "torch_tensor", sfunc = torch::torch_serialize, ufunc = torch::torch_load ) daemons(1, serial = cfg) everywhere(library(torch)) ``` **Example Usage:** This creates a convolutional neural network with `torch::nn_module()`, specifies parameters, then initializes them in a parallel process: ``` r model <- nn_module( initialize = function(in_size, out_size) { self$conv1 <- nn_conv2d(in_size, out_size, 5) self$conv2 <- nn_conv2d(in_size, out_size, 5) }, forward = \(x) { x <- self$conv1(x) x <- nnf_relu(x) x <- self$conv2(x) x <- nnf_relu(x) x } ) params <- list(in_size = 1, out_size = 20) m <- mirai(do.call(model, params), model = model, params = params) m[] #> An `nn_module` containing 1,040 parameters. #> #> ── Modules ───────────────────────────────────────────────────────────────────────────────── #> • conv1: #520 parameters #> • conv2: #520 parameters ``` The returned model contains many tensor elements: ``` r m$data$parameters$conv1.weight #> torch_tensor #> (1,1,.,.) = #> -0.0857 -0.1251 0.1377 0.1328 -0.1928 #> 0.0165 -0.1619 0.0201 0.0201 -0.1663 #> -0.1365 -0.0559 0.0189 -0.1072 -0.0852 #> 0.1580 -0.0606 0.1258 -0.1551 -0.0132 #> -0.0780 0.0685 -0.1514 0.1451 0.0735 #> #> (2,1,.,.) = #> -0.0758 -0.1536 0.1792 0.1410 -0.1115 #> 0.1274 -0.0175 -0.1426 0.0470 -0.0959 #> -0.0682 0.1954 0.0961 -0.0061 0.1571 #> 0.1654 0.0397 -0.1638 -0.1155 0.1208 #> -0.1486 -0.1936 0.1217 -0.1016 0.0139 #> #> (3,1,.,.) = #> 0.1694 0.0830 0.1361 -0.0924 -0.0131 #> 0.1325 0.0720 0.1806 -0.1409 -0.0302 #> -0.1241 0.1135 -0.1845 -0.0455 0.1466 #> -0.0849 -0.1224 0.1740 -0.1332 0.0679 #> 0.0436 -0.0271 0.0862 -0.1048 -0.1420 #> #> (4,1,.,.) = #> 0.1299 0.0352 0.1679 -0.0350 0.1596 #> 0.0006 -0.1701 0.0643 -0.1827 -0.1157 #> 0.1647 0.0789 0.0277 0.0672 0.0694 #> 0.1079 -0.0205 -0.0281 -0.0550 0.1013 #> 0.1596 0.0933 0.1892 -0.1925 -0.1579 #> #> (5,1,.,.) = #> -0.0214 0.1610 0.0383 0.0132 0.1843 #> ... [the output was truncated (use n=-1 to disable)] #> [ CPUFloatType{20,1,5,5} ][ requires_grad = TRUE ] ``` Pass model parameters to an optimizer, also initialized in a parallel process: ``` r optim <- mirai(optim_rmsprop(params = params), params = m$data$parameters) optim[] #> #> Inherits from: #> Public: #> add_param_group: function (param_group) #> clone: function (deep = FALSE) #> defaults: list #> initialize: function (params, lr = 0.01, alpha = 0.99, eps = 1e-08, weight_decay = 0, #> load_state_dict: function (state_dict, ..., .refer_to_state_dict = FALSE) #> param_groups: list #> state: State, R6 #> state_dict: function () #> step: function (closure = NULL) #> zero_grad: function (set_to_none = FALSE) #> Private: #> deep_clone: function (name, value) #> step_helper: function (closure, loop_fun) daemons(0) ``` Tensors and complex objects containing tensors pass seamlessly between host and daemons like any R object. Custom serialization leverages R's native 'refhook' mechanism for transparent usage. Fast and efficient, it minimizes data copies and uses official `torch` serialization methods directly. ### 3. Database Hosting using Arrow Database Connectivity Use `DBI` to access and manipulate Apache Arrow data efficiently through ADBC (Arrow Database Connectivity). This creates an in-memory SQLite connection using the `adbcsqlite` backend. Serialization uses `arrow` functions in the `daemons()` call. The class is 'nanoarrow_array_stream' since `nanoarrow` backs all DBI `db*Arrow()` queries: ``` r library(mirai) cfg <- serial_config( class = "nanoarrow_array_stream", sfunc = arrow::write_to_raw, ufunc = \(x) arrow::read_ipc_stream(x, as_data_frame = FALSE) ) daemons(1, serial = cfg) everywhere( { library(DBI) # `adbi` and `adbcsqlite` packages must also be installed con <<- dbConnect(adbi::adbi("adbcsqlite"), uri = ":memory:") } ) ``` Use `mirai()` to write or query the database in Arrow format: ``` r m <- mirai(dbWriteTableArrow(con, "iris", iris)) m[] #> [1] TRUE m <- mirai(dbReadTableArrow(con, "iris")) m[] #> Table #> 150 rows x 5 columns #> $Sepal.Length #> $Sepal.Width #> $Petal.Length #> $Petal.Width #> $Species m <- mirai(dbGetQueryArrow(con, 'SELECT * FROM iris WHERE "Sepal.Length" < 4.6')) m[] #> Table #> 5 rows x 5 columns #> $Sepal.Length #> $Sepal.Width #> $Petal.Length #> $Petal.Width #> $Species ``` Tight integration with R's 'refhook' system allows returning complex nested objects with multiple Arrow queries: ``` r m <- mirai({ a <- dbGetQueryArrow(con, 'SELECT * FROM iris WHERE "Sepal.Length" < 4.6') b <- dbGetQueryArrow(con, 'SELECT * FROM iris WHERE "Sepal.Width" < 2.6') x <- dbGetQueryArrow(con, 'SELECT * FROM iris WHERE "Petal.Length" < 1.5') y <- dbGetQueryArrow(con, 'SELECT * FROM iris WHERE "Petal.Width" < 0.2') list(sepal = list(length = a, width = b), petal = list(length = x, width = y)) }) m[] #> $sepal #> $sepal$length #> Table #> 5 rows x 5 columns #> $Sepal.Length #> $Sepal.Width #> $Petal.Length #> $Petal.Width #> $Species #> #> $sepal$width #> Table #> 19 rows x 5 columns #> $Sepal.Length #> $Sepal.Width #> $Petal.Length #> $Petal.Width #> $Species #> #> #> $petal #> $petal$length #> Table #> 24 rows x 5 columns #> $Sepal.Length #> $Sepal.Width #> $Petal.Length #> $Petal.Width #> $Species #> #> $petal$width #> Table #> 5 rows x 5 columns #> $Sepal.Length #> $Sepal.Width #> $Petal.Length #> $Petal.Width #> $Species ``` Use `everywhere()` to cleanly tear down databases before resetting daemons: ``` r everywhere(dbDisconnect(con)) daemons(0) ``` ### 4. Shiny / mirai / DBI / ADBC Integrated Example This demonstrates database connections hosted in mirai daemons powering a Shiny app. One-time `serialization()` setup ensures seamless Arrow data transport in the global environment outside `server()`. Each Shiny session creates a new database connection in a new daemon process, freeing resources when the session ends. This logic lives in `server()`. A unique ID identifies each session and specifies the daemons 'compute profile'. Non-dispatcher daemons work since scheduling isn't needed (all queries take a similar time, each session uses one daemon). Shiny ExtendedTask performs queries via `mirai()` using the session-specific compute profile: ``` r library(mirai) library(secretbase) library(shiny) library(bslib) # create an Arrow serialization configuration cfg <- serial_config( class = "nanoarrow_array_stream", sfunc = arrow::write_to_raw, ufunc = nanoarrow::read_nanoarrow ) # write 'iris' dataset to temp database file (for this demonstration) file <- tempfile() con <- DBI::dbConnect(adbi::adbi("adbcsqlite"), uri = file) DBI::dbWriteTableArrow(con, "iris", iris) DBI::dbDisconnect(con) # common input parameters slmin <- min(iris$Sepal.Length) slmax <- max(iris$Sepal.Length) ui <- page_fluid( p("The time is ", textOutput("current_time", inline = TRUE)), hr(), h3("Shiny / mirai / DBI / ADBC demonstration"), p("New daemon-hosted database connection is created for every Shiny session"), sliderInput( "sl", "Query iris dataset based on Sepal Length", min = slmin, max = slmax, value = c(slmin, slmax), width = "75%" ), input_task_button("btn", "Return query"), tableOutput("table") ) # uses Shiny ExtendedTask with mirai server <- function(input, output, session) { # create unique session id by hashing current time with a random key id <- secretbase::siphash13(Sys.time(), key = nanonext::random(4L)) # create new daemon for each session daemons(1L, serial = cfg, .compute = id) # tear down daemon when session ends session$onEnded(function() daemons(0L, .compute = id)) # everywhere() loads DBI and creates ADBC connection in each daemon # and sets up serialization everywhere( { library(DBI) # `adbi` and `adbcsqlite` packages must also be installed con <<- dbConnect(adbi::adbi("adbcsqlite"), uri = file) }, file = file, .compute = id ) output$current_time <- renderText({ invalidateLater(1000) format(Sys.time(), "%H:%M:%S %p") }) task <- ExtendedTask$new( function(...) mirai( dbGetQueryArrow( con, sprintf( "SELECT * FROM iris WHERE \"Sepal.Length\" BETWEEN %.2f AND %.2f", sl[1L], sl[2L] ) ), ..., .compute = id ) ) |> bind_task_button("btn") observeEvent(input$btn, task$invoke(sl = input$sl)) output$table <- renderTable(task$result()) } # run Shiny app shinyApp(ui = ui, server = server) # deletes temp database file (for this demonstration) unlink(file) ```