R Academy · leksjon

Feilsøking og lastbalansering av parallell kode

Håndter feil i parallelle arbeidere og balanser ujevne arbeidsmengder effektivt.

Leksjon 4 av 413 trinn

Feilsøking og lastbalansering av parallell kode er en gratis leksjon i R Academy på CoddyKit. Dette er leksjon 4 av 4. Du kan lese valgfritt 3 leksjoner fra denne læringsstien gratis i sin helhet – deretter låser CoddyKit PRO opp alle leksjoner, samt praktisk øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i R Academy, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i R Academy inneholder totalt 4 leksjoner.

Hvorfor er parallell feilsøking vanskelig?

Feilsøking av parallell kode er krevende fordi arbeidere kjører i separate prosesser: print()-utsagn er usynlige for hovedprosessen, interaktive feilsøkere som browser() fungerer ikke inne i arbeidere, og feil serialiseres og utløses på nytt i hovedprosessen, slik at den opprinnelige stakksporingen går tapt.

library(parallel)

# Problem: cat() inside worker is invisible in the master
cl <- makeCluster(2)
clusterExport(cl, c())

result <- parLapply(cl, 1:4, function(i) {
  # This cat() goes to the worker's stdout, NOT the master
  cat('Worker processing', i, '\n')  # you will NOT see this
  i^2
})

cat('Master received:', unlist(result), '\n')
stopCluster(cl)

Feilformidling i parLapply

Når en arbeider utløser en feil, pakker parLapply() den inn og utløser den på nytt i hovedprosessen. Feilmeldingen bevares, men stakksporingen fra den eksterne prosessen gjør det ikke. Test alltid funksjonen sekvensielt først, før De parallelliserer den.

library(parallel)
cl <- makeCluster(2)

# Step 1: test sequentially first!
my_fn <- function(x) {
  if (x == 3) stop('bad value: 3')
  sqrt(x)
}

# Sequential test reveals the bug before parallelising
tryCatch(
  lapply(1:5, my_fn),
  error = function(e) cat('Sequential error caught:', e$message, '\n')
)

# Fix: guard the bad case
my_fn_safe <- function(x) {
  if (x <= 0) return(NA_real_)
  sqrt(x)
}
clusterExport(cl, 'my_fn_safe')
cat(unlist(parLapply(cl, 1:5, my_fn_safe)), '\n')

stopCluster(cl)

tryCatch i arbeiderfunksjoner

Hvis De pakker kroppen til arbeiderfunksjonen inn i tryCatch(), kan De fange opp og logge feil per element uten å avslutte hele den parallelle jobben. Returner en markørverdi (for eksempel NA) ved feil, slik at etterbehandlingen kan identifisere problematiske inndata.

library(parallel)
cl <- makeCluster(2)

safe_compute <- function(x) {
  tryCatch({
    if (x == 3) stop('simulated error on element 3')
    list(value = x^2, error = NULL)
  }, error = function(e) {
    list(value = NA_real_, error = conditionMessage(e))
  })
}
clusterExport(cl, 'safe_compute')

results <- parLapply(cl, 1:5, safe_compute)

for (i in seq_along(results)) {
  r <- results[[i]]
  if (is.null(r$error)) {
    cat('Element', i, ': value =', r$value, '\n')
  } else {
    cat('Element', i, ': ERROR -', r$error, '\n')
  }
}

stopCluster(cl)

future::value() med tryCatch

I pakken future utløser value(f) den eksterne feilen på nytt. Hvis De pakker den inn i tryCatch(), kan De håndtere feil i enkeltstående futures og samtidig fortsette å samle inn resultater fra andre futures.

library(future)
plan(multisession, workers = 2)

# Create futures — some will fail
inputs <- list(4, -1, 9, 'x', 16)

futures <- lapply(inputs, function(val) {
  future({
    if (!is.numeric(val)) stop('non-numeric input')
    if (val < 0) stop('negative input')
    sqrt(val)
  })
})

# Collect with individual error handling
results <- lapply(futures, function(f) {
  tryCatch(
    value(f),
    error = function(e) paste('ERROR:', e$message)
  )
})

for (i in seq_along(results)) {
  cat('Input:', inputs[[i]], '-> Result:', as.character(results[[i]]), '\n')
}

plan(sequential)

foreach med %dopar% og .errorhandling

Operatoren %dopar% i pakken foreach fordeler iterasjoner på en registrert backend. Argumentet .errorhandling styrer hva som skjer ved feil: 'stop' (standard), 'remove' (hopp over) eller 'pass' (inkluder betingelsesobjektet).

library(foreach)
library(doParallel)

cl <- makeCluster(2)
registerDoParallel(cl)

# .errorhandling = 'pass': failed elements return the condition
results <- foreach(
  x = 1:6,
  .errorhandling = 'pass'
) %dopar% {
  if (x == 4) stop('bad element')
  x^2
}

for (i in seq_along(results)) {
  if (inherits(results[[i]], 'error')) {
    cat('Element', i, ': ERROR -', results[[i]]$message, '\n')
  } else {
    cat('Element', i, ': value =', results[[i]], '\n')
  }
}

stopCluster(cl)

Lastbalansering: Statisk kontra dynamisk

Statisk planlegging forhåndstildeler biter av lik størrelse til arbeiderne. Dynamisk planlegging gir hver arbeider én oppgave om gangen, slik at raske arbeidere kan ta flere oppgaver. Dynamisk planlegging er bedre når oppgavenes varighet varierer mye.

library(parallel)
cl <- makeCluster(2)

# Simulate unequal task durations (element i takes i*0.05 seconds)
unequal_task <- function(i) {
  Sys.sleep(i * 0.05)
  i
}
clusterExport(cl, 'unequal_task')

# Static: parLapply distributes in fixed chunks
static_time <- system.time(
  parLapply(cl, 1:6, unequal_task)
)[['elapsed']]

# Dynamic: clusterApplyLB assigns one task per available worker
dynamic_time <- system.time(
  clusterApplyLB(cl, 1:6, unequal_task)
)[['elapsed']]

cat('Static LB: ', round(static_time, 2), 's\n')
cat('Dynamic LB:', round(dynamic_time, 2), 's\n')

stopCluster(cl)

Oppdeling i biter: Redusere belastning

Kommunikasjon mellom prosesser har en fast belastning per oppgave. Når De behandler mange små elementer, bør De gruppere dem i større biter, slik at hvert kall til en arbeider utfører mer arbeid per melding. Dermed reduseres forholdet mellom belastning og beregning.

library(parallel)
cl <- makeCluster(2)

# Naive: 1000 tiny tasks — high overhead
tiny_task <- function(x) x^2
clusterExport(cl, 'tiny_task')

t1 <- system.time(parLapply(cl, 1:1000, tiny_task))[['elapsed']]

# Chunked: 10 tasks of 100 items each — low overhead
chunk_task <- function(chunk) sapply(chunk, function(x) x^2)
chunks <- split(1:1000, cut(1:1000, 10, labels = FALSE))
clusterExport(cl, 'chunk_task')

t2 <- system.time(parLapply(cl, chunks, chunk_task))[['elapsed']]

cat('Unchunked:', round(t1, 4), 's\n')
cat('Chunked:  ', round(t2, 4), 's\n')

stopCluster(cl)

Unngå å sende store objekter

Det er kostbart å serialisere store objekter (data frames, matriser, modeller) til arbeidere. Send heller bare indeksene, og les data fra en delt kilde (fil, database eller data som er forhåndslastet i arbeideren). Hold datamengden som sendes til arbeiderne liten.

library(parallel)
cl <- makeCluster(2)

# BAD: sends the entire large data frame to every worker
big_df <- data.frame(x = rnorm(1e5), y = rnorm(1e5))
clusterExport(cl, 'big_df')  # 800 KB shipped to each worker

# BETTER: workers generate/read their own data slice
clusterEvalQ(cl, {
  set.seed(Sys.getpid())  # unique per worker
  local_data <- data.frame(x = rnorm(500), y = rnorm(500))
})

# Each worker uses its own local_data without receiving it from master
result <- parLapply(cl, 1:2, function(i) {
  coef(lm(y ~ x, data = local_data))
})
print(result)

stopCluster(cl)

Logging fra arbeidere

Siden arbeidere ikke kan skrive til konsollen i hovedprosessen, kan De omdirigere utdata fra arbeiderne til separate loggfiler per arbeider ved hjelp av makeCluster(outfile = '/path/to/log'). På Unix kan De omdirigere til /dev/null eller til en egen loggfil for hver arbeider.

library(parallel)

# Route all worker stdout/stderr to a log file
log_file <- tempfile(fileext = '.log')
cl <- makeCluster(2, outfile = log_file)

clusterEvalQ(cl, {
  cat('[Worker', Sys.getpid(), '] started\n')
})

parLapply(cl, 1:4, function(i) {
  cat('[Worker] processing item', i, '\n')  # goes to log file
  i * 10
})

stopCluster(cl)

# Read the log
log_content <- readLines(log_file)
cat(head(log_content, 8), sep = '\n')

Profilere parallell kode

Bruk system.time() til å måle samlet klokketid, og skill manuelt mellom belastningen fra arbeiderne og beregningstiden. For mer detaljert informasjon kan De først kjøre målfunksjonen sekvensielt med profvis::profvis() og deretter parallellisere flaskehalsen.

library(parallel)

# Profile the task sequentially first
target_fn <- function(n) {
  x <- rnorm(n)
  list(
    mean = mean(x),
    sd   = sd(x),
    q95  = quantile(x, 0.95)
  )
}

# Measure sequential baseline
t_seq <- system.time(lapply(rep(1000, 20), target_fn))[['elapsed']]

# Measure parallel speedup
cl <- makeCluster(2)
clusterExport(cl, 'target_fn')
t_par <- system.time(parLapply(cl, rep(1000, 20), target_fn))[['elapsed']]
stopCluster(cl)

cat('Sequential:', round(t_seq, 4), 's\n')
cat('Parallel:  ', round(t_par, 4), 's\n')
cat('Efficiency:', round(t_seq / (t_par * 2) * 100, 1), '%\n')

Komplett robust mønster

Kombiner alle anbefalte fremgangsmåter: del opp arbeidet i biter, bruk lastbalansert tildeling, håndter feil per element, logg til fil og garanter opprydding av klyngen med on.exit().

library(parallel)

robust_parallel <- function(items, fn, workers = 2) {
  cl <- makeCluster(workers, outfile = tempfile())
  on.exit(stopCluster(cl), add = TRUE)

  clusterExport(cl, 'fn', envir = environment())

  safe_fn <- function(x) {
    tryCatch(
      fn(x),
      error = function(e) list(error = e$message, value = NA)
    )
  }
  clusterExport(cl, 'safe_fn', envir = environment())

  # Load-balanced assignment for unequal task times
  clusterApplyLB(cl, items, safe_fn)
}

results <- robust_parallel(1:8, function(x) {
  if (x == 5) stop('deliberate error')
  x^3
})

cat('Results:\n')
for (i in seq_along(results)) {
  cat(' [', i, ']', ifelse(is.na(results[[i]]$value),
    paste('ERROR:', results[[i]]$error),
    results[[i]]
  ), '\n')
}

Hurtigsjekk

De har 100 parallelle oppgaver med svært varierende kjøretid (noen tar 10 ms, andre 500 ms). Hvilken planleggingsstrategi vil minimere den samlede klokketiden?

Oppsummering: Feilsøking av parallell kode

Viktigste punkter:

  • Test funksjoner sekvensielt før De parallelliserer dem – bruk lapply() først
  • Pakk arbeiderkroppene inn i tryCatch() for å fange opp feil per element uten å avslutte jobben
  • future::value() med tryCatch() håndterer feil i enkeltstående futures på en robust måte
  • foreach %dopar% med .errorhandling = 'pass' returnerer feilobjekter i resultatlisten
  • Bruk clusterApplyLB() for dynamisk lastbalansering når oppgavenes varighet varierer
  • Del opp små oppgaver i biter for å redusere kommunikasjonsbelastningen
  • Hold datamengden som sendes til arbeiderne minimal – send indekser, ikke store dataobjekter
  • Logg utdata fra arbeidere til filer via makeCluster(outfile=)
# Canonical debugging workflow
# 1. Test sequentially
# lapply(inputs, my_fn)

# 2. Wrap in tryCatch
# safe_fn <- function(x) tryCatch(my_fn(x), error = function(e) NA)

# 3. Parallelise the safe version
# cl <- makeCluster(2); on.exit(stopCluster(cl))
# parLapply(cl, inputs, safe_fn)

cat('Debugging workflow: sequential -> tryCatch -> parallel\n')
Gratis å komme i gang

Lær deg R med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
43
Leksjoner
159

Ofte stilte spørsmål

Er leksjonen «Feilsøking og lastbalansering av parallell kode» gratis?

Ja – du kan lese valgfritt 3 av leksjonene i læringsstien R Academy, inkludert «Feilsøking og lastbalansering av parallell kode», gratis i sin helhet her på nettet. Deretter låser CoddyKit PRO opp alle leksjoner, samt interaktiv øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Kurset i R Academy inneholder totalt 4 leksjoner.

Hva lærer jeg i «Feilsøking og lastbalansering av parallell kode»?

Håndter feil i parallelle arbeidere og balanser ujevne arbeidsmengder effektivt. Du øver på R Academy med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med R Academy?

Ingen tidligere erfaring er nødvendig. R Academy på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 4 av 4.

Hvor lang tid tar leksjonen «Feilsøking og lastbalansering av parallell kode»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne R Academy-leksjonen?

Ja. Alle R Academy-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. parallel-pakken og detectCores()
  2. future-rammeverket
  3. furrr: parallelle purrr-operasjoner
  4. Feilsøking og lastbalansering av parallell kode
← Tilbake til R Academy