Repository: dmlc/xgboost
Pull request #11357: [R] Implement Booster reset.
Author: login: trivialfis
Created: 2025-03-22T16:16:41Z | Merged: 2025-03-24T05:52:30Z

Close https://github.com/dmlc/xgboost/issues/11355


Diff:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ee18a2afdf96..4a7542d61bb9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -145,6 +145,9 @@ if(R_LIB AND GOOGLE_TEST)
     "Some C++ tests will fail with `R_LIB` enabled, as R package redirects some functions to R runtime implementation."
   )
 endif()
+if(R_LIB AND USE_NCCL)
+  message(SEND_ERROR "`R_LIB` doesn't support distributed computing with NCCL yet.")
+endif()
 if(PLUGIN_RMM AND NOT (USE_CUDA))
   message(SEND_ERROR "`PLUGIN_RMM` must be enabled with `USE_CUDA` flag.")
 endif()
diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R
index 5c30ffccaa16..485a1bc23830 100644
--- a/R-package/R/xgb.Booster.R
+++ b/R-package/R/xgb.Booster.R
@@ -1206,6 +1206,14 @@ xgb.copy.Booster <- function(model) {
   return(.Call(XGDuplicate_R, model))
 }
 
+xgb.reset.Booster <- function(model) {
+  if (!inherits(model, "xgb.Booster")) {
+    stop("'model' must be an 'xgb.Booster' object.")
+  }
+  .Call(XGBoosterReset_R, xgb.get.handle(model))
+  return(model)
+}
+
 #' Check if two boosters share the same C object
 #'
 #' Checks whether two booster objects refer to the same underlying C object.
diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R
index f81fd51dfec8..88217a04d35d 100644
--- a/R-package/R/xgb.cv.R
+++ b/R-package/R/xgb.cv.R
@@ -273,6 +273,7 @@ xgb.cv <- function(params = xgb.params(), data, nrounds, nfold,
 
     if (should_stop) break
   }
+
   cb_outputs <- .execute.cb.after.training(
     callbacks,
     bst_folds,
@@ -282,6 +283,11 @@ xgb.cv <- function(params = xgb.params(), data, nrounds, nfold,
     msg
   )
 
+  # Just in case if the model is referenced in callbacks.
+  lapply(bst_folds, function(fd) {
+    xgb.reset.Booster(fd$bst)
+  })
+
   # the CV result
   ret <- list(
     call = match.call(),
diff --git a/R-package/R/xgb.train.R b/R-package/R/xgb.train.R
index a94f42593121..ad4e9298abe3 100644
--- a/R-package/R/xgb.train.R
+++ b/R-package/R/xgb.train.R
@@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
   curr_attrs <- attributes(bst)
   if (NROW(curr_attrs)) {
     curr_attrs <- curr_attrs[
diff --git a/R-package/src/init.c b/R-package/src/init.c
index 523e5118a6f5..fb40933ccb5b 100644
--- a/R-package/src/init.c
+++ b/R-package/src/init.c
@@ -20,6 +20,7 @@ extern SEXP XGDuplicate_R(SEXP);
 extern SEXP XGPointerEqComparison_R(SEXP, SEXP);
 extern SEXP XGBoosterTrainOneIter_R(SEXP, SEXP, SEXP, SEXP, SEXP);
 extern SEXP XGBoosterCreate_R(SEXP);
+extern SEXP XGBoosterReset_R(SEXP);
 extern SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP, SEXP);
 extern SEXP XGBoosterSetStrFeatureInfo_R(SEXP, SEXP, SEXP);
 extern SEXP XGBoosterGetStrFeatureInfo_R(SEXP, SEXP);
@@ -83,6 +84,7 @@ static const R_CallMethodDef CallEntries[] = {
   {"XGPointerEqComparison_R",     (DL_FUNC) &XGPointerEqComparison_R,     2},
   {"XGBoosterTrainOneIter_R",     (DL_FUNC) &XGBoosterTrainOneIter_R,     5},
   {"XGBoosterCreate_R",           (DL_FUNC) &XGBoosterCreate_R,           1},
+  {"XGBoosterReset_R",            (DL_FUNC) &XGBoosterReset_R,            1},
   {"XGBoosterCopyInfoFromDMatrix_R", (DL_FUNC) &XGBoosterCopyInfoFromDMatrix_R, 2},
   {"XGBoosterSetStrFeatureInfo_R",(DL_FUNC) &XGBoosterSetStrFeatureInfo_R,3},  // NOLINT
   {"XGBoosterGetStrFeatureInfo_R",(DL_FUNC) &XGBoosterGetStrFeatureInfo_R,2},  // NOLINT
diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc
index 68a45ba96780..90f73343cf21 100644
--- a/R-package/src/xgboost_R.cc
+++ b/R-package/src/xgboost_R.cc
@@ -306,18 +306,24 @@ object, need to be destructed before triggering the R error.
 In order to preserve the error message, it gets copied to a temporary
 buffer, and the R error section is reached through a 'goto' statement
 that bypasses usual function control flow. */
-char cpp_ex_msg[512];
+namespace {
+constexpr std::size_t MsgSize = 512;
+char cpp_ex_msg[MsgSize];
+}  // anonymous namespace
+
 /*!
  * \brief macro to annotate end of api
  */
-#define R_API_END()                             \
-  } catch(std::exception &e) {                  \
-    std::strncpy(cpp_ex_msg, e.what(), 512);    \
-    goto throw_cpp_ex_as_R_err;                 \
-  }                                             \
-  if (false) {                                  \
-    throw_cpp_ex_as_R_err:                      \
-    Rf_error("%s", cpp_ex_msg);                 \
+#define R_API_END()                                  \
+  }                                                  \
+  catch (std::exception & e) {                       \
+    cpp_ex_msg[MsgSize - 1] = 0;                     \
+    std::strncpy(cpp_ex_msg, e.what(), MsgSize - 1); \
+    goto throw_cpp_ex_as_R_err;                      \
+  }                                                  \
+  if (false) {                                       \
+  throw_cpp_ex_as_R_err:                             \
+    Rf_error("%s", cpp_ex_msg);                      \
   }
 
 /**
@@ -990,7 +996,7 @@ SEXP XGBAltrepSerializer_R(SEXP R_altrepped_obj) {
   return R_NilValue; /* <- should not be reached */
 }
 
-SEXP XGBAltrepDeserializer_R(SEXP unused, SEXP R_state) {
+SEXP XGBAltrepDeserializer_R(SEXP /*unused*/, SEXP R_state) {
   SEXP R_altrepped_obj = Rf_protect(XGBMakeEmptyAltrep());
   R_API_BEGIN();
   BoosterHandle handle = nullptr;
@@ -1081,6 +1087,13 @@ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) {
   return out;
 }
 
+XGB_DLL SEXP XGBoosterReset_R(SEXP handle) {
+  R_API_BEGIN();
+  CHECK_CALL(XGBoosterReset(R_ExternalPtrAddr(handle)));
+  R_API_END();
+  return R_NilValue;
+}
+
 XGB_DLL SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP booster, SEXP dmat) {
   R_API_BEGIN();
   char const **feature_names;
diff --git a/include/xgboost/logging.h b/include/xgboost/logging.h
index 86550cc13f70..eaed72f18bad 100644
--- a/include/xgboost/logging.h
+++ b/include/xgboost/logging.h
@@ -89,10 +89,8 @@ class LogCallbackRegistry {
  public:
   using Callback = void (*)(const char*);
   LogCallbackRegistry() {}
-  inline void Register(Callback log_callback) {}
-  inline Callback Get() const {
-    return nullptr;
-  }
+  inline void Register(Callback) {}
+  inline Callback Get() const { return nullptr; }
 };
 #endif  // !defined(XGBOOST_STRICT_R_MODE) || XGBOOST_STRICT_R_MODE == 0
 


Reviews:
user: login: david-cortes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T11:52:58Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:40:09Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:40:46Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:42:03Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:42:36Z
user: login: david-cortes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T14:44:10Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T16:01:20Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T16:03:20Z
user: login: david-cortes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T16:06:48Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T18:22:46Z
user: login: david-cortes
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T18:25:41Z
user: login: trivialfis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T18:49:55Z
user: login: david-cortes
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T18:51:46Z

Review comments:
diff_hunk: @@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
path: R-package/R/xgb.train.R
user: login: david-cortes
body: Should also be called at the end of `xgb.cv` when retaining the boosters.
created_at: 2025-03-23T11:39:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1206,6 +1206,14 @@ xgb.copy.Booster <- function(model) {
   return(.Call(XGDuplicate_R, model))
 }
 
+xgb.reset.Booster <- function(model) {
path: R-package/R/xgb.Booster.R
user: login: david-cortes
body: Does this function also clean up memory produced during predictions? If so, perhaps it could also get exported.
created_at: 2025-03-23T11:40:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -306,18 +306,24 @@ object, need to be destructed before triggering the R error.
 In order to preserve the error message, it gets copied to a temporary
 buffer, and the R error section is reached through a 'goto' statement
 that bypasses usual function control flow. */
-char cpp_ex_msg[512];
+namespace {
+constexpr std::size_t MsgSize = 512;
+char cpp_ex_msg[MsgSize];
+}  // anonymous namespace
+
 /*!
  * \brief macro to annotate end of api
  */
-#define R_API_END()                             \
-  } catch(std::exception &e) {                  \
-    std::strncpy(cpp_ex_msg, e.what(), 512);    \
-    goto throw_cpp_ex_as_R_err;                 \
-  }                                             \
-  if (false) {                                  \
-    throw_cpp_ex_as_R_err:                      \
-    Rf_error("%s", cpp_ex_msg);                 \
+#define R_API_END()                                  \
+  }                                                  \
+  catch (std::exception & e) {                       \
+    cpp_ex_msg[MsgSize - 1] = 0;                     \
path: R-package/src/xgboost_R.cc
user: login: david-cortes
body: The issue of messages potentially being longer could also be solved by using `%.s` instead of `%s`.
created_at: 2025-03-23T11:42:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -306,18 +306,24 @@ object, need to be destructed before triggering the R error.
 In order to preserve the error message, it gets copied to a temporary
 buffer, and the R error section is reached through a 'goto' statement
 that bypasses usual function control flow. */
-char cpp_ex_msg[512];
+namespace {
+constexpr std::size_t MsgSize = 512;
+char cpp_ex_msg[MsgSize];
path: R-package/src/xgboost_R.cc
user: login: david-cortes
body: Would this be compliant with standard C++17?
created_at: 2025-03-23T11:52:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1206,6 +1206,14 @@ xgb.copy.Booster <- function(model) {
   return(.Call(XGDuplicate_R, model))
 }
 
+xgb.reset.Booster <- function(model) {
path: R-package/R/xgb.Booster.R
user: login: trivialfis
body: Let's keep it internal for now. I don't want others to abuse it as it has a huge overhead.
created_at: 2025-03-23T14:40:09Z
author_association: MEMBER
diff_hunk: @@ -306,18 +306,24 @@ object, need to be destructed before triggering the R error.
 In order to preserve the error message, it gets copied to a temporary
 buffer, and the R error section is reached through a 'goto' statement
 that bypasses usual function control flow. */
-char cpp_ex_msg[512];
+namespace {
+constexpr std::size_t MsgSize = 512;
+char cpp_ex_msg[MsgSize];
path: R-package/src/xgboost_R.cc
user: login: trivialfis
body: Any red flag?
created_at: 2025-03-23T14:40:46Z
author_association: MEMBER
diff_hunk: @@ -306,18 +306,24 @@ object, need to be destructed before triggering the R error.
 In order to preserve the error message, it gets copied to a temporary
 buffer, and the R error section is reached through a 'goto' statement
 that bypasses usual function control flow. */
-char cpp_ex_msg[512];
+namespace {
+constexpr std::size_t MsgSize = 512;
+char cpp_ex_msg[MsgSize];
+}  // anonymous namespace
+
 /*!
  * \brief macro to annotate end of api
  */
-#define R_API_END()                             \
-  } catch(std::exception &e) {                  \
-    std::strncpy(cpp_ex_msg, e.what(), 512);    \
-    goto throw_cpp_ex_as_R_err;                 \
-  }                                             \
-  if (false) {                                  \
-    throw_cpp_ex_as_R_err:                      \
-    Rf_error("%s", cpp_ex_msg);                 \
+#define R_API_END()                                  \
+  }                                                  \
+  catch (std::exception & e) {                       \
+    cpp_ex_msg[MsgSize - 1] = 0;                     \
path: R-package/src/xgboost_R.cc
user: login: trivialfis
body: Just need to make sure it's null terminated. This is looks straight forward.
created_at: 2025-03-23T14:42:03Z
author_association: MEMBER
diff_hunk: @@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
path: R-package/R/xgb.train.R
user: login: trivialfis
body: Thank you for the catch, will fix it for CV.
created_at: 2025-03-23T14:42:36Z
author_association: MEMBER
diff_hunk: @@ -306,18 +306,24 @@ object, need to be destructed before triggering the R error.
 In order to preserve the error message, it gets copied to a temporary
 buffer, and the R error section is reached through a 'goto' statement
 that bypasses usual function control flow. */
-char cpp_ex_msg[512];
+namespace {
+constexpr std::size_t MsgSize = 512;
+char cpp_ex_msg[MsgSize];
path: R-package/src/xgboost_R.cc
user: login: david-cortes
body: No, but I don't know if every standards-compliant compiler out there would accept this pattern (array size determined by an `std::size_t` constexpr variable).
created_at: 2025-03-23T14:44:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
path: R-package/R/xgb.train.R
user: login: trivialfis
body: I can't find where the `bst_folds` is returned in the CV function, could you please help give a pointer?
created_at: 2025-03-23T16:01:20Z
author_association: MEMBER
diff_hunk: @@ -306,18 +306,24 @@ object, need to be destructed before triggering the R error.
 In order to preserve the error message, it gets copied to a temporary
 buffer, and the R error section is reached through a 'goto' statement
 that bypasses usual function control flow. */
-char cpp_ex_msg[512];
+namespace {
+constexpr std::size_t MsgSize = 512;
+char cpp_ex_msg[MsgSize];
path: R-package/src/xgboost_R.cc
user: login: trivialfis
body: Yes, it's valid c++ code. It's used very often 
created_at: 2025-03-23T16:03:20Z
author_association: MEMBER
diff_hunk: @@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
path: R-package/R/xgb.train.R
user: login: david-cortes
body: They are returned through the callback `xgb.cb.cv.predict`:
https://github.com/dmlc/xgboost/blob/0500992cccd1a695fe0030184fb84d0a5f3d703b/R-package/R/callbacks.R#L882
created_at: 2025-03-23T16:06:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
path: R-package/R/xgb.train.R
user: login: trivialfis
body: But the `save_models` is false: https://github.com/dmlc/xgboost/blob/0500992cccd1a695fe0030184fb84d0a5f3d703b/R-package/R/xgb.cv.R#L196 .
created_at: 2025-03-23T18:22:46Z
author_association: MEMBER
diff_hunk: @@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
path: R-package/R/xgb.train.R
user: login: david-cortes
body: Yes, that's the default, but the callbacks are all exported so a user can pass the callback directly with non-default arguments.
created_at: 2025-03-23T18:25:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,7 +390,7 @@ xgb.train <- function(params = xgb.params(), data, nrounds, evals = list(),
     call = match.call(),
     params = params
   )
-
+  bst <- xgb.reset.Booster(bst)
path: R-package/R/xgb.train.R
user: login: trivialfis
body: Thank you for pointing that out. I added a reset to the CV function. It's a bit hacky though, since I'm relying on creating side effects using pointers
created_at: 2025-03-23T18:49:55Z
author_association: MEMBER

PR comments:
user: login: trivialfis
created_at: 2025-03-22T19:06:36Z
body: cc @david-cortes 
author_association: MEMBER
user: login: trivialfis
created_at: 2025-03-24T06:26:00Z
body: I have tested the memory usage. Please use nightly build for now.
author_association: MEMBER