R Academy · Lektion

Debugging og load balancing af parallel kode

Håndtér fejl i parallelle workers, og fordel skæve arbejdsbelastninger effektivt.

Lektion 4 af 413 trin

Debugging og load balancing af parallel kode er en gratis R Academy-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i R Academy, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. R Academy-kurset indeholder 4 lektioner i alt.

Hvorfor parallel fejlfinding er vanskelig

Fejlfinding af parallel kode er udfordrende, fordi arbejdsprocesser kører i separate processer: print()-udsagn er usynlige for hovedprocessen, interaktive fejlfindere som browser() fungerer ikke inde i arbejdsprocesser, og fejl serialiseres og udløses igen i hovedprocessen, så det oprindelige stakspor går tabt.

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)

Fejlvideregivelse i parLapply

Når en arbejdsproces udløser en fejl, pakker parLapply() den ind og udløser den igen i hovedprocessen. Fejlmeddelelsen bevares, men det eksterne stakspor gør ikke. Test altid din funktion sekventielt først, før du paralleliserer 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 arbejdsfunktioner

Hvis du pakker arbejdsfunktionens krop ind i tryCatch(), kan du registrere og logge fejl for hvert element uden at få hele det parallelle job til at gå ned. Returnér en markørværdi (f.eks. NA) ved fejl, så efterbehandlingen kan identificere problematiske input.

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 udløser value(f) den eksterne fejl igen. Hvis du pakker den ind i tryCatch(), kan du håndtere individuelle future-fejl og samtidig fortsætte med at indsamle 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

Pakken foreachs operator %dopar% fordeler iterationer på tværs af en registreret backend. Argumentet .errorhandling styrer, hvad der sker ved fejl: 'stop' (standard), 'remove' (spring over) eller 'pass' (inkludér 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)

Belastningsfordeling: Statisk vs. dynamisk

Statisk planlægning tildeler på forhånd klumper af samme størrelse til arbejdsprocesser. Dynamisk planlægning giver hver arbejdsproces én opgave ad gangen, så hurtige arbejdsprocesser tager flere. Dynamisk planlægning er bedre, når opgavernes varighed varierer meget.

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)

Klumper: Reducering af ekstra belastning

Kommunikation mellem processer har en fast ekstra belastning pr. opgave. Når du behandler mange små elementer, skal du samle dem i større klumper, så hvert kald til en arbejdsproces udfører mere arbejde pr. meddelelse, og forholdet mellem ekstra belastning og beregning dermed reduceres.

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)

Undgå at sende store objekter

Det er dyrt at serialisere store objekter (dataframes, matricer og modeller) til arbejdsprocesser. Send i stedet kun indeksene, og læs data fra en delt kilde (fil, database eller data, der er indlæst på forhånd i arbejdsprocessen). Hold nyttedataene til arbejdsprocesserne små.

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)

Logning fra arbejdsprocesser

Da arbejdsprocesser ikke kan skrive til hovedprocessens konsol, skal du omdirigere output fra arbejdsprocesserne til separate logfiler pr. arbejdsproces ved hjælp af makeCluster(outfile = '/path/to/log'). På Unix kan du omdirigere til /dev/null eller til en dedikeret logfil for hver arbejdsproces.

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

Profilering af parallel kode

Brug system.time() til at måle den samlede faktiske køretid, og opdel manuelt den ekstra belastning fra arbejdsprocesserne og beregningstiden. Hvis du vil have flere detaljer, skal du først køre mål-funktionen sekventielt med profvis::profvis() og derefter parallelisere 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')

Komplet robust mønster

Kombinér alle bedste fremgangsmåder: opdel arbejdet i klumper, brug belastningsbalanceret tildeling, håndtér fejl pr. element, log til en fil, og sørg for oprydning af 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')
}

Hurtig kontrol

Du har 100 parallelle opgaver med meget varierende køretider (nogle tager 10 ms, andre 500 ms). Hvilken planlægningsstrategi minimerer den samlede faktiske køretid?

Opsummering: Fejlfinding af parallel kode

Vigtigste pointer:

  • Test funktioner sekventielt, før du paralleliserer dem — brug først lapply()
  • Pak arbejdsfunktionernes kroppe ind i tryCatch() for at registrere fejl pr. element uden at få jobbet til at gå ned
  • future::value() med tryCatch() håndterer individuelle future-fejl på en robust måde
  • foreach %dopar% med .errorhandling = 'pass' returnerer fejlobjekter i resultatlisten
  • Brug clusterApplyLB() til dynamisk belastningsfordeling ved opgaver med forskellig varighed
  • Saml små opgaver i klumper for at reducere kommunikationens ekstra belastning
  • Hold nyttedataene til arbejdsprocesserne på et minimum — send indeks, ikke store dataobjekter
  • Log output fra arbejdsprocesser 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 at komme i gang

Lær R med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
43
Lektioner
159

Ofte stillede spørgsmål

Er lektionen “Debugging og load balancing af parallel kode” gratis?

Ja — alle 3 lektioner i læringssporet R Academy, inklusive “Debugging og load balancing af parallel kode”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. R Academy-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Debugging og load balancing af parallel kode”?

Håndtér fejl i parallelle workers, og fordel skæve arbejdsbelastninger effektivt. Du øver dig i R Academy med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på R Academy?

Der kræves ingen tidligere erfaring. R Academy på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.

Hvor lang tid tager lektionen “Debugging og load balancing af parallel kode”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne R Academy-lektion?

Ja. Alle R Academy-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Pakken parallel og detectCores()
  2. Fremtid-frameworket
  3. furrr: Parallelle purrr-operationer
  4. Debugging og load balancing af parallel kode
← Tilbage til R Academy