mirai: Asynchronous Evaluation Framework for R

repository·main·Indexed 43 days ago

https://github.com/r-lib/mirai

A minimalist, event-driven asynchronous evaluation framework for R providing high-performance, microsecond-latency messaging. It enables async, parallel, and distributed computing across local laptops, HPC, and cloud environments using a host/client model with persistent worker processes called daemons.

Tokens
4.3K
Snippets
16
Records
17
Agent score
56%

What's inside mirai

  1. Use compute profiles to manage multiple daemon pools

    main

    You can create independent pools of workers using the .compute argument. This allows you to separate workloads (e.g., CPU-bound vs GPU-bound tasks).

    • daemons(n, .compute = "name"): Creates a new compute profile.
    • local_daemons("name"): Switches the active compute profile for the duration of the current function.
    • with_daemons("name", { ... }): Switches the active compute profile for a specific block of code.
    # Create independent pools
    daemons(4, .compute = "cpu")
    daemons(2, .compute = "gpu")
    
    # Run tasks on specific profiles
    m1 <- mirai(cpu_work(), .compute = "cpu")
    m2 <- mirai(gpu_work(), .compute = "gpu")
    
    # Switch active profile for a block
    with_daemons("workers", {
      m <- mirai(task())
      m[]
    })
  2. How mirai architecture works

    main

    The mirai() function sends tasks to daemons, which are persistent R worker processes.

    • Host/Client Model: The host listens at a URL, and daemons dial in to pull work.
    • Dispatcher Thread: An in-process thread handles scheduling, cancellation, and bounded queues.
    • Scalability: You can add or remove daemons at any time and direct tasks to different compute profiles (e.g., CPU pool, GPU pool, or remote clusters) from the same session.
    • Performance: Round-trip latency is in the microsecond range.
  3. Pass dependencies to mirai() using .args or ...

    main

    Because mirai() evaluates expressions in a clean environment on a daemon process, you must explicitly pass all required objects from your calling environment. Failure to do so is the most common cause of errors.

    There are two primary methods for passing dependencies:

    1. .args (Recommended): Objects in .args populate the expression's local evaluation environment. Use this for data and simple functions that you want to reference directly by name inside the expression.
    2. ... (dot-dot-dot): Objects passed via ... are assigned to the daemon's global environment. Use this for helper functions that need to be found via standard R scoping rules (e.g., functions called by other functions).

    Summary Table:

    ScenarioUse
    Data and simple functions.args
    Helper functions requiring lexical scoping...
    Pass entire local scope to local eval env.args = environment()
    Pass entire local scope to daemon global envmirai(expr, environment())
    Large objects shared across many taskseverywhere() first, then reference by name
    # Using .args (Recommended for data/functions)
    m <- mirai(my_func(my_data), .args = list(my_func = my_func, my_data = my_data))
    
    # Using ... (For daemon global environment)
    m <- mirai(my_func(my_data), my_func = my_func, my_data = my_data)
    
    # Shortcut: pass the whole calling environment
    process <- function(x, y) mirai(x + y, .args = environment())
  4. Handle package dependencies on daemons

    main

    Daemons start with no user packages loaded. To use package functions inside a mirai() call or a mirai_map() callback, you must use one of the following patterns:

    1. Namespace qualification (Best Practice): Use package::function() directly in the expression.
    2. Load inside the expression: Call library(package) within the curly braces of the expression.
    3. Pre-load on all daemons: Use everywhere(library(package)) to load the package on all existing daemons before running tasks.
    # CORRECT: namespace-qualify
    m <- mirai(dplyr::filter(df, x > 5), .args = list(df = my_df))
    
    # CORRECT: load inside the expression
    m <- mirai({
      library(dplyr)
      filter(df, x > 5)
    }, .args = list(df = my_df))
    
    # CORRECT: pre-load on all daemons
    everywhere(library(dplyr))
    m <- mirai(filter(df, x > 5), .args = list(df = my_df))
  5. Handle errors and timeouts in mirai tasks

    main

    When a mirai task fails, you can inspect the error using the following methods:

    • m[]: Blocks until the task is resolved (success or error).
    • is_mirai_error(m$data): Returns TRUE if the execution resulted in an error.
    • is_mirai_interrupt(m$data): Returns TRUE if the task was cancelled.
    • is_error_value(m$data): Returns TRUE for any error, interrupt, or timeout.
    • m$data$message: Access the error message.
    • m$data$stack.trace: Access the full stack trace.

    Timeouts and Cancellation:

    • Use .timeout (in milliseconds) within mirai() to set a limit (requires dispatcher).
    • Use stop_mirai(m) to cancel a running task (requires dispatcher).
    # Error handling
    m <- mirai(stop("something went wrong"))
    m[]
    
    if (is_mirai_error(m$data)) {
      print(m$data$message)
    }
    
    # Timeouts (milliseconds)
    m <- mirai(Sys.sleep(60), .timeout = 5000)
    
    # Cancellation
    m <- mirai(long_running_task())
    stop_mirai(m)
  6. Deploy mirai to different environments

    main

    You can configure daemons() to run on various infrastructures using specific configuration functions:

    EnvironmentSetup Function
    Local machinedaemons(n)
    SSH (direct or tunnelled)ssh_config()
    HPC scheduler (Slurm, SGE, Torque/PBS, LSF)cluster_config()
    HTTP API (Posit Workbench, custom)http_config()
    Anywhere elseremote_config()

    Example for an HPC cluster using Slurm:

    daemons(
      n = 6,
      url = host_url(tls = TRUE),
      remote = cluster_config(options = "#SBATCH --mem=10G")
    )
  7. Install the mirai skill using Claude Code Plugins

    main

    If you are using Claude Code, it is recommended to add the Posit marketplace first. This allows you to browse, install, and update skills easily.

    1. Add the Posit marketplace:
    claude plugin marketplace add posit-dev/skills
    1. Install the r-lib category (which includes mirai and all R package development skills):
    claude plugin install r-lib@posit-dev-skills

    Alternatively, you can perform a direct install without adding the marketplace:

    claude plugin install r-lib@posit-dev-skills
    claude plugin marketplace add posit-dev/skills
    claude plugin install r-lib@posit-dev-skills
  8. Update the mirai skill

    main

    Updating the skill depends on how it was originally installed.

    If installed via Claude Code Plugins

    You must update the marketplace index before updating the specific plugin:

    claude plugin marketplace update
    claude plugin update r-lib@posit-dev-skills

    If installed via npx skills add

    Skills installed this way are copied into your agent's configuration at install time. To update, re-run the original installation command to fetch the latest version:

    npx skills add posit-dev/skills --skill mirai
    # For Claude Code plugins
    claude plugin marketplace update
    claude plugin update r-lib@posit-dev-skills
    
    # For npx installations
    npx skills add posit-dev/skills --skill mirai
  9. Implement memory backpressure with try_mirai()

    main

    To prevent a high-throughput producer from overwhelming the system, you can cap the queued task payload using the memory argument in daemons().

    When the queue reaches the specified limit (in MB), mirai() will block the host thread. To avoid blocking the host thread, use try_mirai(), which returns NULL invisibly if the memory cap is hit.

    Note: memory requires the dispatcher (which is the default).

    # 100 MB queue cap
    daemons(4, memory = 100)
    
    # try_mirai() returns NULL instead of blocking when the cap is hit
    m <- try_mirai(work(x), .args = list(x = x))
    if (is.null(m)) {
      # backpressure: drop, retry later, or signal upstream
    } else {
      # m is a regular mirai
    }
    
    # Inspect current and peak queue usage
    status()$memory
  10. Avoid conflicts in nested parallelism

    main

    When performing nested parallel tasks (e.g., calling mirai_map inside a daemon callback), do not use daemons(n) to spawn new workers. This can conflict with the existing outer daemon pool. Instead, use local_url() combined with launch_local() to create a localized, isolated pool for the inner tasks. Always ensure you shut down the local daemons using daemons(0) before the inner task completes to prevent resource leaks.

    mirai_map(1:10, \(x) {
      daemons(url = local_url())
      launch_local(2)
      result <- mirai_map(1:5, function(y, x) x * y, .args = list(x = x))[]
      daemons(0)
      result
    })[]
  11. Install mirai

    main

    Install the mirai package from CRAN to use the minimalist async evaluation framework for R.

    install.packages("mirai")
  12. Integrate mirai with Shiny and promises

    main

    Shiny: ExtendedTask

    For Shiny applications, use the ExtendedTask pattern from the bslib package. This allows you to trigger async tasks from UI elements (like buttons) and render results reactively without blocking the Shiny event loop.

    Promises

    mirai objects can be converted to promises for use with the promises package using the %...>% operator.

    # Shiny ExtendedTask pattern
    library(shiny)
    library(bslib)
    library(mirai)
    
    daemons(4)
    
    server <- function(input, output, session) {
      task <- ExtendedTask$new(
        function(n) mirai(rnorm(n), .args = list(n = n))
      ) |> bind_task_button("run")
    
      observeEvent(input$run, task$invoke(input$n))
      output$result <- renderPlot(hist(task$result()))
    }
    
    # Promise piping
    library(promises)
    mirai({Sys.sleep(1); "done"}) %...>% cat()