---
title: "Use parallel execution responsibly"
name: speed-parallel
description: Select per-call parallel controls and verify what the method actually used.
output:
rmarkdown::html_vignette:
toc: yes
toc_depth: 2
css: albers.css
includes:
in_header: albers-header.html
params:
family: red
base_size: 13
content_width: 72
preset: homage
resource_files:
- albers.css
- albers.js
- albers-header.html
- fonts
vignette: |
%\VignetteIndexEntry{Use parallel execution responsibly}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(
collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE,
fig.align = "center", fig.retina = 2, out.width = "100%"
)
if (requireNamespace("albersdown", quietly = TRUE) &&
requireNamespace("ggplot2", quietly = TRUE)) {
ggplot2::theme_set(albersdown::theme_albers(family = params$family, preset = params$preset))
}
library(neurocluster)
```
```{r albers-classes, echo=FALSE, results='asis'}
cat(sprintf(
paste0(''),
params$family, params$preset, params$content_width
))
```
Parallel execution is a property of a particular method path, input size, and
backend. More threads can lose to scheduling overhead on small masks, and a
method that accepts `parallel = TRUE` may still choose a serial path for a tiny
job. Treat thread settings as part of the benchmark receipt, not as a universal
speed switch.
## Which unified methods accept `parallel`?
In the current `cluster4d()` contract, `parallel` is supported for
`method = "supervoxels"` and `method = "slic"`. An explicit `parallel = TRUE`
is rejected for methods without a parallel unified path. This fail-closed
behaviour prevents a script from appearing parallel while silently ignoring
the request.
```{r parallel-data, include=FALSE}
small_syn <- generate_synthetic_volume(
scenario = "gaussian_blobs", dims = c(14, 14, 3),
n_clusters = 4, n_time = 12, seed = 650
)
large_syn <- generate_synthetic_volume(
scenario = "gaussian_blobs", dims = c(18, 18, 8),
n_clusters = 4, n_time = 12, seed = 650
)
```
```{r execution-mode-fits}
small_requested <- cluster4d(
small_syn$vec, small_syn$mask, n_clusters = 4,
method = "slic", parallel = TRUE,
preserve_k = TRUE, max_iterations = 3
)
large_serial <- cluster4d(
large_syn$vec, large_syn$mask, n_clusters = 4,
method = "slic", parallel = FALSE,
preserve_k = TRUE, max_iterations = 3
)
large_parallel <- cluster4d(
large_syn$vec, large_syn$mask, n_clusters = 4,
method = "slic", parallel = TRUE,
preserve_k = TRUE, max_iterations = 3
)
```
```{r serial-check, include=FALSE}
small_check <- validate_cluster4d(
small_requested, small_syn$vec, small_syn$mask
)
serial_check <- validate_cluster4d(
large_serial, large_syn$vec, large_syn$mask
)
parallel_check <- validate_cluster4d(
large_parallel, large_syn$vec, large_syn$mask
)
stopifnot(small_check$valid, serial_check$valid, parallel_check$valid)
stopifnot(
small_requested$actual_k == 4L,
large_serial$actual_k == 4L,
large_parallel$actual_k == 4L
)
stopifnot(
isTRUE(small_requested$metadata$assignment_parallel_requested),
!isTRUE(small_requested$metadata$assignment_parallel_used),
identical(small_requested$metadata$native_n_threads, 1L),
!isTRUE(large_serial$metadata$assignment_parallel_requested),
!isTRUE(large_serial$metadata$assignment_parallel_used),
identical(large_serial$metadata$native_n_threads, 1L),
isTRUE(large_parallel$metadata$assignment_parallel_requested),
isTRUE(large_parallel$metadata$assignment_parallel_used),
identical(large_parallel$metadata$native_n_threads, 0L),
identical(large_serial$labels, large_parallel$labels)
)
execution_receipt <- data.frame(
workload = c("small", "large", "large"),
masked_voxels = c(length(small_requested$labels),
length(large_serial$labels),
length(large_parallel$labels)),
requested = c(
small_requested$metadata$assignment_parallel_requested,
large_serial$metadata$assignment_parallel_requested,
large_parallel$metadata$assignment_parallel_requested
),
used = c(
small_requested$metadata$assignment_parallel_used,
large_serial$metadata$assignment_parallel_used,
large_parallel$metadata$assignment_parallel_used
),
native_n_threads = c(
small_requested$metadata$native_n_threads,
large_serial$metadata$native_n_threads,
large_parallel$metadata$native_n_threads
)
)
```
```{r execution-receipt, echo=FALSE}
knitr::kable(
execution_receipt,
caption = "Requested and effective SLIC assignment modes in this render."
)
```
The small 588-voxel call requests parallel assignment but deliberately uses the
serial kernel; the 2,592-voxel call exercises the parallel path. The serial and
parallel large-workload calls return identical labels in this render. These are
execution-contract checks, not a speedup claim. Inspect the recorded metadata
rather than inferring thread use from elapsed time alone.
## How do direct APIs control threads?
- `supervoxels(..., parallel = TRUE, num_threads = n)` scopes an optional
RcppParallel thread override to that call and restores the previous setting.
- `slic4d_supervoxels(..., n_threads = n)` passes the requested thread count to
its native assignment operations. `0` means backend default and `1` requests
serial assignment.
- `cluster4d(..., method = "slic", parallel = FALSE)` maps to one native thread.
`parallel = TRUE` requests the backend default, but inputs below 2,000 masked
voxels use the serial assignment kernel and report that fallback in metadata.
Prefer these per-call controls. Changing `RcppParallel` global options in an
analysis or vignette can affect unrelated work in the same R session and makes
the execution receipt harder to interpret.
## How should a speedup claim be tested?
Run serial and parallel calls on the same installed package, input, K, method
arguments, and machine. Warm both paths, repeat them in interleaved order, and
validate every returned fit. Report elapsed-time distributions and whether
labels or quality estimands changed. Record CPU, backend, requested threads,
and method metadata.
Do not tune `grain_size` from a formula presented as universally optimal. It is
a scheduling control for `supervoxels()` and should be benchmarked over a small
prespecified grid on the actual workload. The executable receipt pattern is in
[Benchmark with a reproducible receipt](benchmarks.html); memory boundaries are
in [Performance and memory](performance-memory.html).