如何防止嵌套的 foreach 循环使用 R 中所有内核的 100% CPU?

How to prevent nested foreach loop using 100% CPU of all cores in R?

我是运行一个嵌套的3层foreach循环,但无法阻止代码100%占用远程服务器(Linux,centOS,物理核心= 14,逻辑核心= 56 ).我使用的框架是:

Library(doParallel)
doParallel::registerDoParallel(20)
outRes <- foreach1(I = seq1, …) %:% 
              foreach2(j = seq2, …) %dopar% {
                  innerRes <- foreach3(k = seq3, …)
              }

我遇到了三个问题。

  1. 对于嵌套的foreach 循环,是否将已注册的后端传递给每个foreach 循环并实际产生20*3 = 60 个工人?
  2. 工人数量与CPU效用百分比之间的数学关系是什么?
  3. 在我的真实案例中,foreach1 和foreach2 是小进程,而foreach3 是大进程。这就造成了一个问题,就是worker大部分时间都在闲着等待,导致worker的浪费。有解决办法吗?

PS: 附上可重现的代码示例。

library(mlbench)
data("Sonar")
str(Sonar)
table(Sonar$Class)

seed <- 1234
# for cross validation
number_outCV <- 10
repeats_outCV <- 10
number_innerCV <- 10
repeats_innerCV <- 10

# list of numbers of features to model
featureSeq <- c(10, 30, 50)
# for LASSO training
lambda <- exp(seq(-7, 0, 1))
alpha <- 1

dataList <- list(data1 = Sonar, data2 = Sonar, data3 = Sonar, data4 = Sonar, data5 = Sonar, data6 = Sonar)

# library(doMC)
# doMC::registerDoMC(cores = 20)
library(doParallel)
doParallel::registerDoParallel(20)

nestedCV <- foreach::foreach(clust = 1:length(dataList), .combine = "c", .verbose = TRUE) %:%
  foreach::foreach(outCV = 1:(number_outCV*repeats_outCV), .combine = "c", .verbose = TRUE) %dopar% {
    # prepare data
    dataset <- dataList[[clust]]
    table(dataset$Class)

    # split data into model developing and testing data in the outCV: repeated 10-fold CV
    set.seed(seed)
    ResampIndex <- caret::createMultiFolds(y = dataset$Class, k = number_outCV, times = repeats_outCV)
    developIndex <- ResampIndex[[outCV]]
    developX <- dataset[developIndex, !colnames(dataset) %in% c("Class")]
    developY <- dataset$Class[developIndex]

    testX <- dataset[-developIndex, !colnames(dataset) %in% c("Class")]
    testY <- dataset$Class[-developIndex]

    # get a pool of all the features
    features_all <- colnames(developX)

    # training model with inner repeated 10-fold CV
    # foreach for nfeature search
    nfeatureRes <- foreach::foreach(featNumIndex = seq(along = featureSeq), .combine = "c", .verbose = TRUE) %dopar% {
      nfeature <- featureSeq[featNumIndex]
      selectedFeatures <- features_all[1:nfeature]

      # train LASSO
      lassoCtrl <- trainControl(method = "repeatedCV", 
                                number = number_innerCV, 
                                repeats = repeats_innerCV, 
                                verboseIter = TRUE, returnResamp = "all", savePredictions = "all", 
                                classProbs = TRUE, summaryFunction = twoClassSummary)
      lassofit.cv <- train(x = developX[, selectedFeatures], 
                           y = developY, 
                           method = "glmnet",
                           metric = "ROC",
                           trControl = lassoCtrl, 
                           tuneGrid = expand.grid(lambda = lambda, alpha = alpha),
                           preProcess = c("center", "scale"))

      AUC.test <- pROC::auc(response = testY, predictor = predict(lassofit.cv, newdata = testX[, selectedFeatures], type = "prob")[[2]])
      performance <- data.frame(Class = clust, outCV = outCV, nfeature = nfeature, AUC.cv = max(lassofit.cv$results$ROC), AUC.test = as.numeric(AUC.test))
    }
    # end of nfeature search foreach loop
    nfeatureRes
  }
# end of outCV foreach loop as well as the dataList foreach loop
foreach::registerDoSEQ()

我不知道这是否可行,但也许您可以尝试使用 "nice" 命令通过 运行 降低服务器优先级(这样,即使它使用 100% CPU, 只会在闲暇时取)?

如果你想确保你的代码只使用一定数量的核心,你可以将你的进程固定到特定的核心。这个叫"CPU affinity" and in R you can use parallel::mcaffinity来设置,例如:

parallel::mcaffinity(1:20)

允许您的 R 进程仅使用前 20 个内核。无论此进程中使用的其他库如何,这都有效,因为它调用 OS 级资源控制(一些罕见的库产生或与其他进程通信,但您的代码似乎没有使用类似的东西)。

%:% 是嵌套 foreach 循环的正确方法 — foreach 包在其调度中将同时考虑内循环和外循环,并且只执行 registerDoParallel 内循环一次处理多个主体——无论它们是否来自同一个外循环迭代。错误的方法是例如foreach(…) %dopar% { foreach(…) %dopar% { … } } — 这将一次产生 registerDoParallel 平方的计算(因此,在您的情况下为 400)。 foreach(…) %do% { foreach(…) %dopar% { … } }(或相反)会更好,但不是最理想的。有关详细信息,请参阅 foreachnesting vignette

在你的情况下,最好保持两个外循环(%:%%doPar%),并将内循环更改为 %do% .在两个外部循环中,您仍然有相当多的迭代总数来填充 20 个核心,并且常见的规则是,如果可能的话,并行化外部循环比内部循环更好。

通过许多实验,我猜这就是 foreach() 可能分叉工人的方式:

  1. 如果使用嵌套的 foreach(例如 foreach() %:% foreach() %dopar% {}):分叉的工作人员(共享存储的逻辑 CPU 内核)将是 foreach() 乘法之前注册的内核foreach() 次。例如:

    registerDoMc(cores = 10)
    foreach() %:% foreach() %:% foreach() %dopar% {} # 10x3 = 30 workers will be finally forked in the following example.
    
  2. 如果一个 foreach() 嵌套在另一个 foreach() 中而不使用 %:%,则派生的工人(逻辑 CUP 核心)将是从 %:% 部分乘以独立的嵌套部分。例如:

    registerDoMc(cores = 10)
    foreach() %:% foreach() %dopar% { foreach()} # (10+10)x10 = 200 workers will the finally forked.
    

如有不妥欢迎指正