---
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)
```