Nothing
# 050_ExecutionConfig.R
# ::rtemis::
# 2026- EDG rtemis.org
# %% ExecutionConfig ----
#' ExecutionConfig Class
#'
#' @description
#' Abstract base for the execution family: how a run dispatches work. `backend`
#' is the discriminator and the variant holds what that backend actually needs.
#' Concrete variants are `SerialExecutionConfig`, `FutureExecutionConfig` and
#' `MiraiExecutionConfig`; build one with [setup_SerialExecution],
#' [setup_FutureExecution] or [setup_MiraiExecution].
#'
#' @author EDG
#' @noRd
ExecutionConfig <- schema_class(
name = "ExecutionConfig",
package = "rtemis",
abstract = TRUE,
# Only what every variant shares *unchanged*. A property a variant needs to
# constrain is declared on the variants instead, never here and overridden:
# `own_prop_names()` subtracts by name, so an override is not a leaf's own
# property and would vanish from the published leaf schema, taking its
# constraint with it.
defaults = list(
seed = DefaultPolicy(
kind = "runtime",
on_null = TRUE,
requires = "seed",
reason = "Drawn from the runtime random stream and recorded."
)
),
properties = list(
backend = class_character,
# Threads inside a worker rather than worker processes, which is why this composes
# with either dispatch level and is declared on the base: it is meaningful under
# serial execution too.
n_workers_algorithm = prop_integer(
NULL,
min = 1L,
nullable = TRUE,
description = "Threads for a self-parallelizing algorithm. Unset lets the worker ladder assign it."
),
# Where an algorithm computes. On the base because it composes with every
# backend: a serial run on a GPU is as ordinary as a parallel one on CPUs.
device = prop_object(
DeviceConfig,
nullable = TRUE,
description = "Compute device. Unset selects one per algorithm: cuda where the machine has it and the algorithm can use it, else the CPU. An algorithm that cannot use the requested device runs on the CPU."
),
on_error = prop_string(
"continue",
enum = c("continue", "stop", "stop_outer"),
description = "Failure policy."
),
# Master seed for the run's *computation* RNG, distinct from a ResamplerConfig's
# seed, which governs how the data is split. Independent substreams are derived
# from it and assigned by task index, so a run's results do not depend on backend,
# worker count, or the order tasks happened to finish in.
seed = prop_integer(
NULL,
min = 0L,
nullable = TRUE,
description = "Master seed for the run's computation RNG."
),
# Every parallel task receives the same training data and slices its own rows from
# it, so the data is serialized once per task. Shared memory replaces those copies
# with one region every worker maps. Default "auto" because the transport is cheaper
# at every measured size and identical in result; it is skipped, silently, wherever
# it cannot apply.
shared_memory = prop_string(
"auto",
enum = c("none", "auto", "always"),
description = "Share worker data through OS shared memory."
)
),
publication = SchemaPublication(
role = "family",
slug = "execution",
title = "rtemis ExecutionConfig",
description = "Language-independent config for rtemis execution: sequential, parallel, or distributed.",
discriminator = "backend",
discriminator_description = "Execution backend.",
order = 9L
)
) # /rtemis::ExecutionConfig
# %% SerialExecutionConfig ----
#' SerialExecutionConfig Class
#'
#' @description
#' Execution in the calling process: nothing is dispatched to a worker. The
#' backend value is `"none"` because `backend` names the backend and there is
#' no third backend, only the absence of one; the class is named for what it
#' is.
#'
#' Descends from `ExecutionConfig` rather than `ParallelExecutionConfig`, so it
#' carries no worker pool and no dispatch levels: `n_workers`,
#' `n_workers_outer` and `n_workers_tuning` are properties of the parallel
#' backends only. `n_workers_algorithm` sits on the base and applies here,
#' being threads inside the calling process rather than worker processes.
#'
#' @author EDG
#' @noRd
SerialExecutionConfig <- schema_class(
name = "SerialExecutionConfig",
parent = ExecutionConfig,
package = "rtemis",
properties = list(
backend = prop_algorithm("none")
),
publication = SchemaPublication(
role = "leaf",
description = "Execution in the calling process, dispatching nothing.",
order = 1L
)
) # /rtemis::SerialExecutionConfig
# %% ParallelExecutionConfig ----
#' ParallelExecutionConfig Class
#'
#' @description
#' Abstract base for the backends that dispatch to worker processes. Named for
#' parallelism rather than asynchrony: what varies across these variants is how
#' many workers run at once, not whether the caller blocks -- `train()` blocks
#' under every backend.
#'
#' Declares the cross-field rule shared by every concrete backend.
#'
#' @author EDG
#' @noRd
ParallelExecutionConfig <- schema_class(
name = "ParallelExecutionConfig",
parent = ExecutionConfig,
package = "rtemis",
abstract = TRUE,
defaults = list(
n_workers = DefaultPolicy(
kind = "runtime",
on_null = TRUE,
requires = "n_workers",
reason = "Resolved from available cores or explicit dispatch levels."
)
),
properties = list(
n_workers = prop_integer(
1L,
min = 1L,
description = "Number of parallel workers."
),
# Per-level overrides. NULL means "let the ladder decide"; setting any one of them
# turns the ladder off entirely and the unset levels take 1, so a config never mixes
# a hand-picked level with an inferred one.
n_workers_outer = prop_integer(
NULL,
min = 1L,
nullable = TRUE,
description = "Workers for outer resampling. Unset lets the worker ladder assign it."
),
n_workers_tuning = prop_integer(
NULL,
min = 1L,
nullable = TRUE,
description = "Workers for tuning. Unset lets the worker ladder assign it."
)
),
rules = list(ForbidTogether(
id = "execution.parallel-dispatch",
conditions = list(
SchemaPredicate(property = "n_workers_outer", minimum = 2),
SchemaPredicate(property = "n_workers_tuning", minimum = 2)
),
message = paste0(
"Only one dispatch level can run in parallel. Set n_workers_outer or ",
"n_workers_tuning to 1. An outer fold cannot dispatch again from inside ",
"a worker process; algorithm threads may combine with either level."
)
))
) # /rtemis::ParallelExecutionConfig
# %% FutureExecutionConfig ----
#' FutureExecutionConfig Class
#'
#' @description
#' Dispatch through \pkg{future}. `future_plan` is non-nullable here, which is
#' what the old "must be set when backend is 'future'" rule stated at runtime.
#'
#' @author EDG
#' @noRd
FutureExecutionConfig <- schema_class(
name = "FutureExecutionConfig",
parent = ParallelExecutionConfig,
package = "rtemis",
defaults = list(
future_plan = DefaultPolicy(
kind = "runtime",
requires = "future_plan",
reason = "Read from the runtime future plan option."
)
),
properties = list(
backend = prop_algorithm("future"),
future_plan = prop_string(
"mirai_multisession",
description = "Future plan to use."
)
),
publication = SchemaPublication(
role = "leaf",
description = "Parallel execution through the future package.",
order = 2L
)
) # /rtemis::FutureExecutionConfig
# %% MiraiExecutionConfig ----
#' MiraiExecutionConfig Class
#'
#' @description
#' Dispatch through \pkg{mirai}. rtemis starts the daemons and they persist for
#' the run, so `warm_workers`, which loads rtemis in each daemon as the pool
#' starts, is declared on this variant. A \pkg{future} plan guarantees neither
#' which worker evaluates a task nor that a worker outlives it, so the future
#' variant has no such property.
#'
#' @author EDG
#' @noRd
MiraiExecutionConfig <- schema_class(
name = "MiraiExecutionConfig",
parent = ParallelExecutionConfig,
package = "rtemis",
properties = list(
backend = prop_algorithm("mirai"),
warm_workers = prop_boolean(
TRUE,
description = "Load rtemis in every worker when the pool is built, rather than on each worker's first task."
)
),
publication = SchemaPublication(
role = "leaf",
description = "Parallel execution through the mirai package.",
order = 3L
)
) # /rtemis::MiraiExecutionConfig
# %% execution_n_workers ----
#' The worker pool an execution config builds
#'
#' Declared on `ParallelExecutionConfig` because only a parallel backend has a
#' pool. Serial execution runs in the calling process, which is one worker, and
#' says so here.
#'
#' @param x `ExecutionConfig` object.
#'
#' @return Integer.
#'
#' @author EDG
#' @keywords internal
#' @noRd
execution_n_workers <- function(x) {
if (S7_inherits(x, ParallelExecutionConfig)) x@n_workers else 1L
} # /rtemis::execution_n_workers
# %% execution_dispatch_level ----
#' A named dispatch level, or NULL where the backend has none
#'
#' `n_workers_outer` and `n_workers_tuning` are declared on
#' `ParallelExecutionConfig`: they name how many worker *processes* a level
#' claims, and serial execution dispatches to none. Reading them through here
#' lets the worker ladder ask every config the same question.
#'
#' @param x `ExecutionConfig` object.
#' @param level Character \{"n_workers_outer", "n_workers_tuning"\}.
#'
#' @return Integer, or NULL.
#'
#' @author EDG
#' @keywords internal
#' @noRd
execution_dispatch_level <- function(x, level) {
if (S7_inherits(x, ParallelExecutionConfig)) prop(x, level) else NULL
} # /rtemis::execution_dispatch_level
# %% execution_future_plan ----
#' The future plan an execution config runs under, or NULL
#'
#' Only `FutureExecutionConfig` has one; the other backends do not consult a
#' plan, so they do not declare the property.
#'
#' @param x `ExecutionConfig` object.
#'
#' @return Character, or NULL.
#'
#' @author EDG
#' @keywords internal
#' @noRd
execution_future_plan <- function(x) {
if (S7_inherits(x, FutureExecutionConfig)) x@future_plan else NULL
} # /rtemis::execution_future_plan
# %% execution_warm_workers ----
#' Whether an execution config loads rtemis in its workers as the pool starts
#'
#' Only `MiraiExecutionConfig` declares `warm_workers`; every other backend
#' loads rtemis on each worker's first task.
#'
#' @param x `ExecutionConfig` object.
#'
#' @return Logical.
#'
#' @author EDG
#' @keywords internal
#' @noRd
execution_warm_workers <- function(x) {
S7_inherits(x, MiraiExecutionConfig) && x@warm_workers
} # /rtemis::execution_warm_workers
# %% algorithm_threads ----
#' Threads an algorithm may use
#'
#' The execution config's named algorithm share, or its worker count. With no
#' config -- prediction, applying a fitted decomposition -- the host's default
#' worker count, so the machine that runs the work decides, never the one that
#' trained the model. `default_n_workers()` resolves to 1 under a CRAN check.
#'
#' @param execution_config Optional `ExecutionConfig` object.
#'
#' @return Integer.
#'
#' @author EDG
#' @keywords internal
#' @noRd
algorithm_threads <- function(execution_config = NULL) {
if (is.null(execution_config)) {
return(default_n_workers())
}
check_is_S7(execution_config, ExecutionConfig)
execution_config@n_workers_algorithm %||%
execution_n_workers(execution_config)
} # /rtemis::algorithm_threads
# %% algorithm_execution_config ----
#' The execution config for work that runs inside one fit
#'
#' A serial config carrying the run's device, seed and failure policy, with the
#' thread count the worker ladder gave this work. `train()` hands it to the
#' fit's own predictions and to its decomposition step: they run in the fit's
#' process, so they may use its threads and must not dispatch. Built with the
#' class constructor, so no seed is drawn.
#'
#' @param execution_config `ExecutionConfig` object: The run's config.
#' @param n_threads Integer: Threads for the work.
#'
#' @return `SerialExecutionConfig` object.
#'
#' @author EDG
#' @keywords internal
#' @noRd
algorithm_execution_config <- function(execution_config, n_threads) {
SerialExecutionConfig(
n_workers_algorithm = as.integer(n_threads),
device = execution_config@device,
on_error = execution_config@on_error,
seed = execution_config@seed,
shared_memory = execution_config@shared_memory
)
} # /rtemis::algorithm_execution_config
# %% execution_device ----
#' The device an execution config requests, or NULL
#'
#' @param execution_config Optional `ExecutionConfig` object.
#'
#' @return `DeviceConfig` object, or NULL for automatic selection.
#'
#' @author EDG
#' @keywords internal
#' @noRd
execution_device <- function(execution_config = NULL) {
if (is.null(execution_config)) NULL else execution_config@device
} # /rtemis::execution_device
# %% execution_backend_label ----
#' The execution backend as the resources line names it
#'
#' `serial` for the `none` backend, which dispatches nothing, and the future
#' plan beside `future`, since the plan decides where the workers run.
#'
#' @param x `ExecutionConfig` object.
#'
#' @return Character.
#'
#' @author EDG
#' @keywords internal
#' @noRd
execution_backend_label <- function(x) {
switch(
x@backend,
none = "serial",
future = paste0("future (", execution_future_plan(x), ")"),
x@backend
)
} # /rtemis::execution_backend_label
# %% repr.ExecutionConfig ----
method(repr, ExecutionConfig) <- function(x, pad = 0L, output_type = NULL) {
out <- repr_S7name(S7_class(x)@name, pad = pad, output_type = output_type)
.props <- props(x)
# An unset level follows `n_workers` and is omitted; a set one is shown.
for (level in c(
"n_workers_outer",
"n_workers_tuning",
"n_workers_algorithm"
)) {
if (is.null(.props[[level]])) {
.props[[level]] <- NULL
}
}
out <- paste0(
out,
repr_ls(.props, pad = pad, output_type = output_type)
)
} # /rtemis::repr.ExecutionConfig
# %% print.ExecutionConfig ----
method(print, ExecutionConfig) <- function(x, output_type = NULL, ...) {
cat(repr(x, output_type = output_type), "\n")
invisible(x)
} # /rtemis::print.ExecutionConfig
# %% default_n_workers ----
#' Default number of workers
#'
#' Determine the default number of parallel workers, guarding against errors in
#' environments where `parallelly::availableCores()` is unavailable (e.g. wasm/webR).
#'
#' @param omit Integer: Number of cores to omit from the count.
#'
#' @return Integer: Number of workers.
#'
#' @author EDG
#' @keywords internal
#' @noRd
default_n_workers <- function(omit = 3L) {
# `availableCores()` names its result after the mechanism it consulted ("system",
# "cgroups", ...), and that name rides along into every message that prints the worker
# count -- "Max workers: c(system = 7)".
# The fallback covers that probe failing in an environment it cannot read, not the
# package being absent: `future` is an Import and hard-imports `parallelly`, so it is
# always installed. `availableCores()` floors at 1 and does not error on `omit`
# exceeding the core count, so this is reached only by an unreadable environment.
unname(tryCatch(
parallelly::availableCores(omit = omit),
error = function(e) 1L
))
} # /rtemis::default_n_workers
# %% --- User API ----
# %% EXECUTION_CLASSES ----
# The execution family, keyed by the `backend` value each variant declares.
EXECUTION_CLASSES <- list(
none = SerialExecutionConfig,
future = FutureExecutionConfig,
mirai = MiraiExecutionConfig
)
EXECUTION_SETUP <- c(
none = "setup_SerialExecution",
future = "setup_FutureExecution",
mirai = "setup_MiraiExecution"
)
# %% .execution_common ----
#' Validate and resolve the settings every execution variant shares
#'
#' @param n_workers_outer,n_workers_tuning,n_workers_algorithm Optional Integer.
#' @param device Optional `DeviceConfig`, Character, or list.
#' @param on_error,shared_memory Character: Already matched by the caller.
#' @param seed Optional Integer.
#'
#' @return Named list of resolved shared settings.
#'
#' @author EDG
#' @keywords internal
#' @noRd
.execution_common <- function(
n_workers_algorithm,
device,
on_error,
seed,
shared_memory
) {
if (!is.null(n_workers_algorithm)) {
n_workers_algorithm <- clean_int(n_workers_algorithm)
check_pos_integer_scalar(n_workers_algorithm)
}
# "always" is a demand, so an unusable request is an error here, before dispatch. "auto" is best-effort: a missing mori is one more reason it cannot
# share, not a mistake to correct.
if (shared_memory == "always") {
check_dependencies("mori")
}
# Resolved here so it is recorded on the config, and therefore in the run record, and
# every run, including one with defaults, is reproducible.
# Drawing from the current stream keeps `set.seed(1); train(...)` deterministic.
seed <- if (is.null(seed)) {
sample.int(.Machine[["integer.max"]], 1L)
} else {
clean_int(seed)
}
list(
n_workers_algorithm = n_workers_algorithm,
device = as_device_config(device),
on_error = on_error,
seed = seed,
shared_memory = shared_memory
)
} # /rtemis::.execution_common
# %% .execution_dispatch ----
#' Validate and resolve the settings only a parallel backend has
#'
#' @param n_workers,n_workers_outer,n_workers_tuning Optional Integer.
#'
#' @return Named list of resolved dispatch settings.
#'
#' @author EDG
#' @keywords internal
#' @noRd
.execution_dispatch <- function(n_workers, n_workers_outer, n_workers_tuning) {
for (nm in c("n_workers_outer", "n_workers_tuning")) {
v <- get(nm)
if (!is.null(v)) {
v <- clean_int(v)
check_pos_integer_scalar(v)
assign(nm, v)
}
}
list(
n_workers = .resolve_n_workers(
n_workers,
n_workers_outer,
n_workers_tuning
),
n_workers_outer = n_workers_outer,
n_workers_tuning = n_workers_tuning
)
} # /rtemis::.execution_dispatch
# %% .resolve_n_workers ----
#' Size the worker pool for a parallel backend
#'
#' With the dispatch levels named explicitly the pool follows from them rather
#' than from `default_n_workers()`. Only the dispatch levels count: algorithm
#' workers are threads inside a worker, not workers of their own.
#'
#' @param n_workers Optional Integer: What the caller supplied.
#' @param n_workers_outer,n_workers_tuning Optional Integer.
#'
#' @return Integer.
#'
#' @author EDG
#' @keywords internal
#' @noRd
.resolve_n_workers <- function(n_workers, n_workers_outer, n_workers_tuning) {
if (is.null(n_workers)) {
n_workers <- if (is.null(n_workers_outer) && is.null(n_workers_tuning)) {
default_n_workers()
} else {
max(1L, n_workers_outer %||% 1L, n_workers_tuning %||% 1L)
}
}
n_workers <- clean_int(n_workers)
if (n_workers < 1L) {
rtemis.core::abort(
"n_workers must be at least 1.",
class = c("rtemis_range_error", "rtemis_input_error")
)
}
n_workers
} # /rtemis::.resolve_n_workers
# %% --- User API ----
# %% setup_FutureExecution ----
#' Set up a `FutureExecutionConfig`
#'
#' @description
#' Parallel execution through \pkg{future}. A plan is required, and is filled in
#' from `getOption("future.plan", "mirai_multisession")` when not given.
#'
#' @param n_workers Integer [1, Inf): Number of parallel workers. Left `NULL`,
#' it follows the named dispatch levels, or `default_n_workers()` if none is named.
#' @param n_workers_outer Optional Integer [1, Inf): Workers for outer resampling,
#' overriding the automatic assignment.
#' @param n_workers_tuning Optional Integer [1, Inf): Workers for tuning, overriding the
#' automatic assignment.
#' @param n_workers_algorithm Optional Integer [1, Inf): Threads for a self-parallelizing
#' algorithm, overriding the automatic assignment.
#' @param device Optional `DeviceConfig`: Compute device, as a type name --
#' `"cpu"`, `"cuda"`, `"mps"` (the Apple silicon GPU) or `"opencl"` (a LightGBM
#' GPU build) -- or [setup_CUDA] to name which GPUs. `NULL` selects one per
#' algorithm: `"cuda"` where the machine has it and the algorithm can use it,
#' else the CPU. An algorithm that cannot use the requested device runs on the
#' CPU, and the run's resources line says so; a device the machine lacks is an
#' error when an algorithm tries to use it. `"mps"` is never selected
#' automatically: in rtemis benchmarks on an Apple M5 the CPU was faster for
#' every network narrower than about 1024 units or trained in batches smaller
#' than about 2048 cases, often by 2 to 5 times, and `"mps"` was faster by up to
#' about 25% only with both and thousands of features. It also slows down across
#' many fits in one R session, and a seed does not reach dropout on it.
#' @param future_plan Character: Future plan to use. Defaults to
#' `getOption("future.plan", "mirai_multisession")`.
#' @param on_error Character \{"continue", "stop", "stop_outer"\}: Failure policy.
#' `"continue"` makes grid cells and unscorable hyperparameter combinations
#' non-fatal (recorded, warned, and excluded), failing only when nothing is scorable or
#' the final model fails; `"stop"` aborts on any error; `"stop_outer"` tolerates grid-cell
#' failures but aborts on an outer-fold failure.
#' @param seed Optional Integer [0, Inf): Master seed for the run's computation RNG,
#' from which one independent substream per parallel task is derived. Left `NULL`, a
#' seed is drawn from the current RNG stream and recorded on the returned object, so
#' every run is reproducible and the seed it used is auditable. Distinct from a
#' `ResamplerConfig` seed, which governs how the data is split.
#' @param shared_memory Character \{"none", "auto", "always"\}: Whether to hand workers
#' the training data through OS shared memory instead of serializing a copy to each.
#' `"auto"` is the default: it shares whenever it can -- workers parallel and on this
#' machine, \pkg{mori} installed -- and quietly does not when it cannot, falling back to
#' the ordinary transport. `"none"` disables it. `"always"` is a demand: it shares even
#' when the run is sequential, so a run can be compared against its own shared
#' counterpart, and raises an error when the request cannot be met, for a caller relying
#' on sharing to stay inside a memory budget.
#'
#' @details
#' **Worker levels**
#'
#' There are three levels that can absorb workers: a self-parallelizing algorithm,
#' tuning, and outer resampling. Left alone, `n_workers` is assigned to exactly one of
#' them, in that order of priority, so no two levels compete for the same cores.
#'
#' Naming any of `n_workers_outer`, `n_workers_tuning` or `n_workers_algorithm` takes
#' over that assignment: the automatic one is switched off entirely and any level not
#' named gets 1, so a run never mixes a hand-picked level with an inferred one.
#'
#' Outer resampling and tuning dispatch to worker *processes*, and an outer fold runs
#' inside one of those processes, so only one of the two can be parallel -- setting both
#' above 1 is an error. `n_workers_algorithm` is threads within a worker, so it combines
#' with either.
#'
#' **Reproducibility**
#'
#' Substreams are assigned by task index, so a run gives the same answer under every
#' backend and worker count, and the parallel result matches the sequential one exactly.
#'
#' Shared memory needs \pkg{mori}, whose minimum R version (4.3) is above
#' \pkg{rtemis}'s own (4.1), so `"auto"` is the portable setting for a script that may
#' run anywhere. It is local RAM: workers on another machine cannot map it.
#'
#' @return `FutureExecutionConfig` object.
#'
#' @author EDG
#' @export
#' @examples
#' setup_FutureExecution(n_workers = 4L, future_plan = "multisession")
setup_FutureExecution <- function(
n_workers = NULL,
n_workers_outer = NULL,
n_workers_tuning = NULL,
n_workers_algorithm = NULL,
device = NULL,
future_plan = getOption("future.plan", "mirai_multisession"),
on_error = c("continue", "stop", "stop_outer"),
seed = NULL,
shared_memory = c("auto", "none", "always")
) {
# Captured before anything is filled in: this function's defaults are not the
# class's, so a record comparing the two would report the pool it sized and
# the seed it drew as the caller's choices.
origins <- supplied_origins()
apply_setup_defaults(FutureExecutionConfig)
on_error <- match.arg(on_error)
shared_memory <- match.arg(shared_memory)
check_dependencies("future")
check_character(future_plan)
if (!future_plan %in% ALLOWED_PLANS) {
rtemis.core::abort(
"'",
future_plan,
"' is not an allowed future plan. Allowed plans: ",
paste(ALLOWED_PLANS, collapse = ", "),
".",
class = c("rtemis_value_error", "rtemis_input_error")
)
}
out <- do.call(
FutureExecutionConfig,
c(
.execution_common(
n_workers_algorithm,
device,
on_error,
seed,
shared_memory
),
.execution_dispatch(n_workers, n_workers_outer, n_workers_tuning),
list(future_plan = future_plan)
)
)
config_origins(out) <- origins
out
} # /rtemis::setup_FutureExecution
# %% setup_MiraiExecution ----
#' Set up a `MiraiExecutionConfig`
#'
#' @description
#' Parallel execution through \pkg{mirai}. Takes no plan: \pkg{mirai} has no
#' equivalent of a `future_plan`, which is why it is not an argument here.
#'
#' @inheritParams setup_FutureExecution
#' @param warm_workers Logical: Load \pkg{rtemis} in every worker as the pool is built,
#' rather than leaving each worker to load it on its first task. Either way the result is
#' the same; TRUE records the loading time in the run's worker-pool setup step.
#'
#' @details
#' See [setup_FutureExecution] for how the three worker levels are assigned and
#' how the run's seed is resolved; both are identical under either backend.
#'
#' @return `MiraiExecutionConfig` object.
#'
#' @author EDG
#' @export
#' @examples
#' setup_MiraiExecution(n_workers = 2L)
setup_MiraiExecution <- function(
n_workers = NULL,
n_workers_outer = NULL,
n_workers_tuning = NULL,
n_workers_algorithm = NULL,
device = NULL,
on_error = c("continue", "stop", "stop_outer"),
seed = NULL,
shared_memory = c("auto", "none", "always"),
warm_workers = TRUE
) {
origins <- supplied_origins()
apply_setup_defaults(MiraiExecutionConfig)
on_error <- match.arg(on_error)
shared_memory <- match.arg(shared_memory)
check_logical_scalar(warm_workers)
check_dependencies("mirai")
out <- do.call(
MiraiExecutionConfig,
c(
.execution_common(
n_workers_algorithm,
device,
on_error,
seed,
shared_memory
),
.execution_dispatch(n_workers, n_workers_outer, n_workers_tuning),
list(warm_workers = warm_workers)
)
)
config_origins(out) <- origins
out
} # /rtemis::setup_MiraiExecution
# %% setup_SerialExecution ----
#' Set up a `SerialExecutionConfig`
#'
#' @description
#' Execution in the calling process: nothing is dispatched to a worker. Takes
#' no `n_workers` and no `future_plan`, neither of which a serial run can act
#' on.
#'
#' @inheritParams setup_FutureExecution
#'
#' @details
#' `n_workers_algorithm` still applies: it is threads *within the calling
#' process*, not worker processes, so a self-parallelizing algorithm runs
#' multi-threaded under a serial config.
#'
#' See [setup_FutureExecution] for how the run's seed is resolved.
#'
#' @return `SerialExecutionConfig` object.
#'
#' @author EDG
#' @export
#' @examples
#' setup_SerialExecution()
setup_SerialExecution <- function(
n_workers_algorithm = NULL,
device = NULL,
on_error = c("continue", "stop", "stop_outer"),
seed = NULL,
shared_memory = c("auto", "none", "always")
) {
origins <- supplied_origins()
apply_setup_defaults(SerialExecutionConfig)
on_error <- match.arg(on_error)
shared_memory <- match.arg(shared_memory)
out <- do.call(
SerialExecutionConfig,
.execution_common(
n_workers_algorithm,
device,
on_error,
seed,
shared_memory
)
)
config_origins(out) <- origins
out
} # /rtemis::setup_SerialExecution
# %% .list_to_ExecutionConfig ----
#' Rebuild an execution config from a wire list
#'
#' @param x Named list: The `execution_config` block of a config document.
#'
#' @return `ExecutionConfig` subclass object.
#'
#' @author EDG
#' @keywords internal
#' @noRd
.list_to_ExecutionConfig <- function(x) {
keys <- unique(unlist(c(
list(names(ExecutionConfig@properties)),
lapply(EXECUTION_CLASSES, function(cls) names(cls@properties))
)))
check_wire_keys(x, keys, "execution config")
args <- .drop_meta_keys(x)
backend <- args[["backend"]]
if (is.null(backend) || !backend %in% names(EXECUTION_SETUP)) {
rtemis.core::abort(
"An execution config needs a `backend`, one of: ",
paste0("'", names(EXECUTION_SETUP), "'", collapse = ", "),
".",
class = c("rtemis_value_error", "rtemis_input_error")
)
}
args[["backend"]] <- NULL
# A variant declares only what its backend acts on, so a key it has no
# formal for is a document claiming something that backend cannot do -- a
# `n_workers` on a serial config, a `future_plan` on a mirai one. The
# published schema rejects it too: the dispatcher closes the composed
# document with `unevaluatedProperties`, and the leaf declares no such
# property to evaluate it.
fn <- get(EXECUTION_SETUP[[backend]], envir = asNamespace("rtemis"))
args <- read_wire_objects(args, EXECUTION_CLASSES[[backend]])
carried <- setdiff(names(args), names(formals(fn)))
if (length(carried) > 0L) {
rtemis.core::abort(
"An execution config with backend '",
backend,
"' cannot carry: ",
paste(carried, collapse = ", "),
".",
class = c("rtemis_value_error", "rtemis_input_error")
)
}
do.call(fn, args)
} # /rtemis::.list_to_ExecutionConfig
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.