From 093b6758380e4663d18729dd87d54a7845695a4b Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Fri, 3 Nov 2023 18:19:28 +0800 Subject: [PATCH 001/109] [Doc] update the tutorial of xgboost4j-spark-gpu (#9752) --------- Co-authored-by: Jiaming Yuan --- doc/jvm/xgboost4j_spark_gpu_tutorial.rst | 37 +++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/doc/jvm/xgboost4j_spark_gpu_tutorial.rst b/doc/jvm/xgboost4j_spark_gpu_tutorial.rst index 7b80286ef..3b2f92c6f 100644 --- a/doc/jvm/xgboost4j_spark_gpu_tutorial.rst +++ b/doc/jvm/xgboost4j_spark_gpu_tutorial.rst @@ -18,9 +18,9 @@ Build an ML Application with XGBoost4J-Spark-GPU Add XGBoost to Your Project =========================== -Before we go into the tour of how to use XGBoost4J-Spark-GPU, you should first consult -:ref:`Installation from Maven repository ` in order to add XGBoost4J-Spark-GPU as -a dependency for your project. We provide both stable releases and snapshots. +Prior to delving into the tutorial on utilizing XGBoost4J-Spark-GPU, it is advisable to refer to +:ref:`Installation from Maven repository ` for instructions on adding XGBoost4J-Spark-GPU +as a project dependency. We offer both stable releases and snapshots for your convenience. Data Preparation ================ @@ -54,7 +54,7 @@ Read Dataset with Spark's Built-In Reader .schema(schema) .csv(dataPath) -In the first line, we create an instance of a `SparkSession `_ +At first, we create an instance of a `SparkSession `_ which is the entry point of any Spark application working with DataFrames. The ``schema`` variable defines the schema of the DataFrame wrapping Iris data. With this explicitly set schema, we can define the column names as well as their types; otherwise the column names would be @@ -112,7 +112,7 @@ models. Although we use the Iris dataset in this tutorial to show how we use ``XGBoost/XGBoost4J-Spark-GPU`` to resolve a multi-classes classification problem, the usage in Regression is very similar to classification. -To train a XGBoost model for classification, we need to claim a XGBoostClassifier first: +To train a XGBoost model for classification, we need to define a XGBoostClassifier first: .. code-block:: scala @@ -130,9 +130,13 @@ To train a XGBoost model for classification, we need to claim a XGBoostClassifie .setFeaturesCol(featuresNames) .setLabelCol(labelName) -The ``device`` parameter is for informing XGBoost that CUDA devices should be used instead of CPU. Unlike the single-node mode, GPUs are managed by spark instead of by XGBoost. Therefore, explicitly specified device ordinal like ``cuda:1`` is not support. +The ``device`` parameter is for informing XGBoost that CUDA devices should be used instead of CPU. +Unlike the single-node mode, GPUs are managed by spark instead of by XGBoost. Therefore, +explicitly specified device ordinal like ``cuda:1`` is not support. -The available parameters for training a XGBoost model can be found in :doc:`here `. Similar to the XGBoost4J-Spark package, in addition to the default set of parameters, XGBoost4J-Spark-GPU also supports the camel-case variant of these parameters to be consistent with Spark's MLlib naming convention. +The available parameters for training a XGBoost model can be found in :doc:`here `. +Similar to the XGBoost4J-Spark package, in addition to the default set of parameters, +XGBoost4J-Spark-GPU also supports the camel-case variant of these parameters to be consistent with Spark's MLlib naming convention. Specifically, each parameter in :doc:`this page ` has its equivalent form in XGBoost4J-Spark-GPU with camel case. For example, to set ``max_depth`` for each tree, you @@ -211,32 +215,31 @@ and the prediction for each instance. Submit the application ********************** -Here’s an example to submit an end-to-end XGBoost-4j-Spark-GPU Spark application to an -Apache Spark Standalone cluster, assuming the application main class is Iris and the -application jar is iris-1.0.0.jar +Assuming that the application main class is "Iris" and the application jar is "iris-1.0.0.jar",` +provided below is an instance demonstrating how to submit the xgboost application to an Apache +Spark Standalone cluster. .. code-block:: bash - cudf_version=22.02.0 - rapids_version=22.02.0 - xgboost_version=1.6.1 + rapids_version=23.10.0 + xgboost_version=2.0.1 main_class=Iris app_jar=iris-1.0.0.jar spark-submit \ --master $master \ - --packages ai.rapids:cudf:${cudf_version},com.nvidia:rapids-4-spark_2.12:${rapids_version},ml.dmlc:xgboost4j-gpu_2.12:${xgboost_version},ml.dmlc:xgboost4j-spark-gpu_2.12:${xgboost_version} \ + --packages com.nvidia:rapids-4-spark_2.12:${rapids_version},ml.dmlc:xgboost4j-gpu_2.12:${xgboost_version},ml.dmlc:xgboost4j-spark-gpu_2.12:${xgboost_version} \ --conf spark.executor.cores=12 \ - --conf spark.task.cpus=1 \ + --conf spark.task.cpus=12 \ --conf spark.executor.resource.gpu.amount=1 \ - --conf spark.task.resource.gpu.amount=0.08 \ + --conf spark.task.resource.gpu.amount=1 \ --conf spark.rapids.sql.csv.read.double.enabled=true \ --conf spark.rapids.sql.hasNans=false \ --conf spark.plugins=com.nvidia.spark.SQLPlugin \ --class ${main_class} \ ${app_jar} -* First, we need to specify the ``RAPIDS Accelerator, cudf, xgboost4j-gpu, xgboost4j-spark-gpu`` packages by ``--packages`` +* First, we need to specify the ``RAPIDS Accelerator, xgboost4j-gpu, xgboost4j-spark-gpu`` packages by ``--packages`` * Second, ``RAPIDS Accelerator`` is a Spark plugin, so we need to configure it by specifying ``spark.plugins=com.nvidia.spark.SQLPlugin`` For details about other ``RAPIDS Accelerator`` other configurations, please refer to the `configuration `_. From 98238d63fabe3470f558e1705b47a53e3e27649c Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 7 Nov 2023 02:44:39 +0800 Subject: [PATCH 002/109] [dask] Change document to avoid using default import. (#9742) This aligns dask with pyspark, users need to explicitly call: ``` from xgboost.dask import DaskXGBClassifier from xgboost import dask as dxgb ``` In future releases, we might stop using the default import and remove the lazy loader. --- demo/dask/cpu_survival.py | 6 +-- demo/dask/cpu_training.py | 6 +-- demo/dask/dask_callbacks.py | 3 +- demo/dask/gpu_training.py | 9 ++--- demo/dask/sklearn_cpu_training.py | 4 +- demo/dask/sklearn_gpu_training.py | 4 +- doc/tutorials/dask.rst | 67 ++++++++++++++++--------------- 7 files changed, 51 insertions(+), 48 deletions(-) diff --git a/demo/dask/cpu_survival.py b/demo/dask/cpu_survival.py index 7fe0570de..8bf464ce2 100644 --- a/demo/dask/cpu_survival.py +++ b/demo/dask/cpu_survival.py @@ -9,7 +9,7 @@ import os import dask.dataframe as dd from dask.distributed import Client, LocalCluster -import xgboost as xgb +from xgboost import dask as dxgb from xgboost.dask import DaskDMatrix @@ -48,14 +48,14 @@ def main(client): "lambda": 0.01, "alpha": 0.02, } - output = xgb.dask.train( + output = dxgb.train( client, params, dtrain, num_boost_round=100, evals=[(dtrain, "train")] ) bst = output["booster"] history = output["history"] # you can pass output directly into `predict` too. - prediction = xgb.dask.predict(client, bst, dtrain) + prediction = dxgb.predict(client, bst, dtrain) print("Evaluation history: ", history) # Uncomment the following line to save the model to the disk diff --git a/demo/dask/cpu_training.py b/demo/dask/cpu_training.py index 811af5cd3..0f3316741 100644 --- a/demo/dask/cpu_training.py +++ b/demo/dask/cpu_training.py @@ -6,7 +6,7 @@ Example of training with Dask on CPU from dask import array as da from dask.distributed import Client, LocalCluster -import xgboost as xgb +from xgboost import dask as dxgb from xgboost.dask import DaskDMatrix @@ -25,7 +25,7 @@ def main(client): # distributed version of train returns a dictionary containing the # resulting booster and evaluation history obtained from # evaluation metrics. - output = xgb.dask.train( + output = dxgb.train( client, {"verbosity": 1, "tree_method": "hist"}, dtrain, @@ -36,7 +36,7 @@ def main(client): history = output["history"] # you can pass output directly into `predict` too. - prediction = xgb.dask.predict(client, bst, dtrain) + prediction = dxgb.predict(client, bst, dtrain) print("Evaluation history:", history) return prediction diff --git a/demo/dask/dask_callbacks.py b/demo/dask/dask_callbacks.py index 408297d9e..a4b0f5648 100644 --- a/demo/dask/dask_callbacks.py +++ b/demo/dask/dask_callbacks.py @@ -8,6 +8,7 @@ from dask_ml.datasets import make_regression from dask_ml.model_selection import train_test_split import xgboost as xgb +import xgboost.dask as dxgb from xgboost.dask import DaskDMatrix @@ -61,7 +62,7 @@ def main(client): dtrain = DaskDMatrix(client, X_train, y_train) dtest = DaskDMatrix(client, X_test, y_test) - output = xgb.dask.train( + output = dxgb.train( client, { "verbosity": 1, diff --git a/demo/dask/gpu_training.py b/demo/dask/gpu_training.py index 6eea00692..fd5b35bf3 100644 --- a/demo/dask/gpu_training.py +++ b/demo/dask/gpu_training.py @@ -8,7 +8,6 @@ from dask import dataframe as dd from dask.distributed import Client from dask_cuda import LocalCUDACluster -import xgboost as xgb from xgboost import dask as dxgb from xgboost.dask import DaskDMatrix @@ -21,7 +20,7 @@ def using_dask_matrix(client: Client, X: da.Array, y: da.Array) -> da.Array: # Use train method from xgboost.dask instead of xgboost. This distributed version # of train returns a dictionary containing the resulting booster and evaluation # history obtained from evaluation metrics. - output = xgb.dask.train( + output = dxgb.train( client, { "verbosity": 2, @@ -37,7 +36,7 @@ def using_dask_matrix(client: Client, X: da.Array, y: da.Array) -> da.Array: history = output["history"] # you can pass output directly into `predict` too. - prediction = xgb.dask.predict(client, bst, dtrain) + prediction = dxgb.predict(client, bst, dtrain) print("Evaluation history:", history) return prediction @@ -56,14 +55,14 @@ def using_quantile_device_dmatrix(client: Client, X: da.Array, y: da.Array) -> d # be used for anything else other than training unless a reference is specified. See # the `ref` argument of `DaskQuantileDMatrix`. dtrain = dxgb.DaskQuantileDMatrix(client, X, y) - output = xgb.dask.train( + output = dxgb.train( client, {"verbosity": 2, "tree_method": "hist", "device": "cuda"}, dtrain, num_boost_round=4, ) - prediction = xgb.dask.predict(client, output, X) + prediction = dxgb.predict(client, output, X) return prediction diff --git a/demo/dask/sklearn_cpu_training.py b/demo/dask/sklearn_cpu_training.py index 12d55493c..38ea25e61 100644 --- a/demo/dask/sklearn_cpu_training.py +++ b/demo/dask/sklearn_cpu_training.py @@ -5,7 +5,7 @@ Use scikit-learn regressor interface with CPU histogram tree method from dask import array as da from dask.distributed import Client, LocalCluster -import xgboost +from xgboost import dask as dxgb def main(client): @@ -16,7 +16,7 @@ def main(client): X = da.random.random((m, n), partition_size) y = da.random.random(m, partition_size) - regressor = xgboost.dask.DaskXGBRegressor(verbosity=1, n_estimators=2) + regressor = dxgb.DaskXGBRegressor(verbosity=1, n_estimators=2) regressor.set_params(tree_method="hist") # assigning client here is optional regressor.client = client diff --git a/demo/dask/sklearn_gpu_training.py b/demo/dask/sklearn_gpu_training.py index 32a994464..768690995 100644 --- a/demo/dask/sklearn_gpu_training.py +++ b/demo/dask/sklearn_gpu_training.py @@ -9,7 +9,7 @@ from dask.distributed import Client # It's recommended to use dask_cuda for GPU assignment from dask_cuda import LocalCUDACluster -import xgboost +from xgboost import dask as dxgb def main(client): @@ -20,7 +20,7 @@ def main(client): X = da.random.random((m, n), partition_size) y = da.random.random(m, partition_size) - regressor = xgboost.dask.DaskXGBRegressor(verbosity=1) + regressor = dxgb.DaskXGBRegressor(verbosity=1) # set the device to CUDA regressor.set_params(tree_method="hist", device="cuda") # assigning client here is optional diff --git a/doc/tutorials/dask.rst b/doc/tutorials/dask.rst index 7ab251bcf..148230fe6 100644 --- a/doc/tutorials/dask.rst +++ b/doc/tutorials/dask.rst @@ -39,7 +39,8 @@ on a dask cluster: .. code-block:: python - import xgboost as xgb + from xgboost import dask as dxgb + import dask.array as da import dask.distributed @@ -53,11 +54,11 @@ on a dask cluster: X = da.random.random(size=(num_obs, num_features), chunks=(1000, num_features)) y = da.random.random(size=(num_obs, 1), chunks=(1000, 1)) - dtrain = xgb.dask.DaskDMatrix(client, X, y) + dtrain = dxgb.DaskDMatrix(client, X, y) # or - # dtrain = xgb.dask.DaskQuantileDMatrix(client, X, y) + # dtrain = dxgb.DaskQuantileDMatrix(client, X, y) - output = xgb.dask.train( + output = dxgb.train( client, {"verbosity": 2, "tree_method": "hist", "objective": "reg:squarederror"}, dtrain, @@ -87,25 +88,27 @@ returns a model and the computation history as a Python dictionary: .. code-block:: python - {'booster': Booster, - 'history': dict} + { + "booster": Booster, + "history": dict, + } For prediction, pass the ``output`` returned by ``train`` into :py:func:`xgboost.dask.predict`: .. code-block:: python - prediction = xgb.dask.predict(client, output, dtrain) + prediction = dxgb.predict(client, output, dtrain) # Or equivalently, pass ``output['booster']``: - prediction = xgb.dask.predict(client, output['booster'], dtrain) + prediction = dxgb.predict(client, output['booster'], dtrain) Eliminating the construction of DaskDMatrix is also possible, this can make the computation a bit faster when meta information like ``base_margin`` is not needed: .. code-block:: python - prediction = xgb.dask.predict(client, output, X) + prediction = dxgb.predict(client, output, X) # Use inplace version. - prediction = xgb.dask.inplace_predict(client, output, X) + prediction = dxgb.inplace_predict(client, output, X) Here ``prediction`` is a dask ``Array`` object containing predictions from model if input is a ``DaskDMatrix`` or ``da.Array``. When putting dask collection directly into the @@ -134,14 +137,14 @@ both memory usage and prediction time. .. code-block:: python # dtrain is the DaskDMatrix defined above. - prediction = xgb.dask.predict(client, booster, dtrain) + prediction = dxgb.predict(client, booster, dtrain) or equivalently: .. code-block:: python # where X is a dask DataFrame or dask Array. - prediction = xgb.dask.predict(client, booster, X) + prediction = dxgb.predict(client, booster, X) Also for inplace prediction: @@ -149,7 +152,7 @@ Also for inplace prediction: # where X is a dask DataFrame or dask Array backed by cupy or cuDF. booster.set_param({"device": "cuda"}) - prediction = xgb.dask.inplace_predict(client, booster, X) + prediction = dxgb.inplace_predict(client, booster, X) When input is ``da.Array`` object, output is always ``da.Array``. However, if the input type is ``dd.DataFrame``, output can be ``dd.Series``, ``dd.DataFrame`` or ``da.Array``, @@ -174,7 +177,7 @@ One simple optimization for running consecutive predictions is using futures = [] for X in dataset: # Here we pass in a future instead of concrete booster - shap_f = xgb.dask.predict(client, booster_f, X, pred_contribs=True) + shap_f = dxgb.predict(client, booster_f, X, pred_contribs=True) futures.append(shap_f) results = client.gather(futures) @@ -186,7 +189,7 @@ Scikit-Learn wrapper object: .. code-block:: python - cls = xgb.dask.DaskXGBClassifier() + cls = dxgb.DaskXGBClassifier() cls.fit(X, y) booster = cls.get_booster() @@ -207,12 +210,12 @@ collection. .. code-block:: python from distributed import LocalCluster, Client - import xgboost as xgb + from xgboost import dask as dxgb def main(client: Client) -> None: X, y = load_data() - clf = xgb.dask.DaskXGBClassifier(n_estimators=100, tree_method="hist") + clf = dxgb.DaskXGBClassifier(n_estimators=100, tree_method="hist") clf.client = client # assign the client clf.fit(X, y, eval_set=[(X, y)]) proba = clf.predict_proba(X) @@ -242,7 +245,7 @@ In the example below, a ``KubeCluster`` is used for `deploying Dask on Kubernete from dask_kubernetes import KubeCluster # Need to install the ``dask-kubernetes`` package from dask.distributed import Client - import xgboost as xgb + from xgboost import dask as dxgb import dask import dask.array as da @@ -265,7 +268,7 @@ In the example below, a ``KubeCluster`` is used for `deploying Dask on Kubernete X = da.random.random(size=(m, n), chunks=100) y = da.random.random(size=(m, ), chunks=100) - regressor = xgb.dask.DaskXGBRegressor(n_estimators=10, missing=0.0) + regressor = dxgb.DaskXGBRegressor(n_estimators=10, missing=0.0) regressor.client = client regressor.set_params(tree_method='hist', device="cuda") regressor.fit(X, y, eval_set=[(X, y)]) @@ -298,7 +301,7 @@ threads in each process for training. But if ``nthread`` parameter is set: .. code-block:: python - output = xgb.dask.train( + output = dxgb.train( client, {"verbosity": 1, "nthread": 8, "tree_method": "hist"}, dtrain, @@ -330,12 +333,12 @@ Functional interface: async with dask.distributed.Client(scheduler_address, asynchronous=True) as client: X, y = generate_array() - m = await xgb.dask.DaskDMatrix(client, X, y) - output = await xgb.dask.train(client, {}, dtrain=m) + m = await dxgb.DaskDMatrix(client, X, y) + output = await dxgb.train(client, {}, dtrain=m) - with_m = await xgb.dask.predict(client, output, m) - with_X = await xgb.dask.predict(client, output, X) - inplace = await xgb.dask.inplace_predict(client, output, X) + with_m = await dxgb.predict(client, output, m) + with_X = await dxgb.predict(client, output, X) + inplace = await dxgb.inplace_predict(client, output, X) # Use ``client.compute`` instead of the ``compute`` method from dask collection print(await client.compute(with_m)) @@ -349,7 +352,7 @@ actual computation will return a coroutine and hence require awaiting: async with dask.distributed.Client(scheduler_address, asynchronous=True) as client: X, y = generate_array() - regressor = await xgb.dask.DaskXGBRegressor(verbosity=1, n_estimators=2) + regressor = await dxgb.DaskXGBRegressor(verbosity=1, n_estimators=2) regressor.set_params(tree_method='hist') # trivial method, synchronous operation regressor.client = client # accessing attribute, synchronous operation regressor = await regressor.fit(X, y, eval_set=[(X, y)]) @@ -371,7 +374,7 @@ To enable early stopping, pass one or more validation sets containing ``DaskDMat .. code-block:: python import dask.array as da - import xgboost as xgb + from xgboost import dask as dxgb num_rows = 1e6 num_features = 100 @@ -398,19 +401,19 @@ To enable early stopping, pass one or more validation sets containing ``DaskDMat chunks=(rows_per_chunk, 1) ) - dtrain = xgb.dask.DaskDMatrix( + dtrain = dxgb.DaskDMatrix( client=client, data=data, label=labels ) - dvalid = xgb.dask.DaskDMatrix( + dvalid = dxgb.DaskDMatrix( client=client, data=X_eval, label=y_eval ) - result = xgb.dask.train( + result = dxgb.train( client=client, params={ "objective": "reg:squarederror", @@ -421,7 +424,7 @@ To enable early stopping, pass one or more validation sets containing ``DaskDMat early_stopping_rounds=3 ) -When validation sets are provided to ``xgb.dask.train()`` in this way, the model object returned by ``xgb.dask.train()`` contains a history of evaluation metrics for each validation set, across all boosting rounds. +When validation sets are provided to :py:func:`xgboost.dask.train` in this way, the model object returned by :py:func:`xgboost.dask.train` contains a history of evaluation metrics for each validation set, across all boosting rounds. .. code-block:: python @@ -463,7 +466,7 @@ interface, including callback functions, custom evaluation metric and objective: save_best=True, ) - booster = xgb.dask.train( + booster = dxgb.train( client, params={ "objective": "binary:logistic", From 82828621d020299d1a1226b3131444298fe92278 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 7 Nov 2023 05:03:30 +0800 Subject: [PATCH 003/109] [doc] Add doc for linters and simplify c++ lint script. (#9750) --- .github/workflows/main.yml | 8 +------- doc/contrib/coding_guide.rst | 32 ++++++++++++++++++++++++++++---- tests/ci_build/lint_cpp.py | 9 ++++++++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3fd39bc36..67e77ad6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -144,11 +144,5 @@ jobs: python -m pip install wheel setuptools cmakelint cpplint pylint - name: Run lint run: | - python3 tests/ci_build/lint_cpp.py xgboost cpp R-package/src - - python3 tests/ci_build/lint_cpp.py xgboost cpp include src python-package \ - --exclude_path python-package/xgboost/dmlc-core python-package/xgboost/include \ - python-package/xgboost/lib python-package/xgboost/rabit \ - python-package/xgboost/src - + python3 tests/ci_build/lint_cpp.py sh ./tests/ci_build/lint_cmake.sh diff --git a/doc/contrib/coding_guide.rst b/doc/contrib/coding_guide.rst index 1169921bb..f7ed88b6c 100644 --- a/doc/contrib/coding_guide.rst +++ b/doc/contrib/coding_guide.rst @@ -118,16 +118,40 @@ two automatic checks to enforce coding style conventions. To expedite the code r Linter ====== -We use `pylint `_ and `cpplint `_ to enforce style convention and find potential errors. Linting is especially useful for Python, as we can catch many errors that would have otherwise occurred at run-time. +We use a combination of linters to enforce style convention and find potential errors. Linting is especially useful for scripting languages like Python, as we can catch many errors that would have otherwise occurred at run-time. -To run this check locally, run the following command from the top level source tree: +For Python scripts, `pylint `_, `black `__ and `isort `__ are used for providing guidance on coding style, and `mypy `__ is required for type checking. For C++, `cpplint `_ is used along with ``clang-tidy``. For R, ``lintr`` is used. + +To run checks for Python locally, install the checkers mentioned previously and run: .. code-block:: bash cd /path/to/xgboost/ - make lint + python ./tests/ci_build/lint_python.py --fix + +To run checks for R: + +.. code-block:: bash + + cd /path/to/xgboost/ + Rscript tests/ci_build/lint_r.R $(pwd) + +To run checks for cpplint locally: + +.. code-block:: bash + + cd /path/to/xgboost/ + python ./tests/ci_build/lint_cpp.py + + +See next section for clang-tidy. For CMake scripts: + +.. code-block:: bash + + bash ./tests/ci_build/lint_cmake.sh + +Lastly, the linter for jvm-packages is integrated into the maven build process. -This command requires the Python packages pylint and cpplint. Clang-tidy ========== diff --git a/tests/ci_build/lint_cpp.py b/tests/ci_build/lint_cpp.py index 593b8f870..6ec2b4e7f 100644 --- a/tests/ci_build/lint_cpp.py +++ b/tests/ci_build/lint_cpp.py @@ -134,7 +134,12 @@ def process(fname, allow_type): def main(): parser = argparse.ArgumentParser(description="run cpp lint") - parser.add_argument("path", nargs="+", help="path to traverse") + parser.add_argument( + "path", + nargs="*", + help="Path to traverse", + default=["src", "include", os.path.join("R-package", "src"), "python-package"], + ) parser.add_argument( "--exclude_path", nargs="+", @@ -148,6 +153,8 @@ def main(): allow_type += CXX_SUFFIX for path in args.path: + if not os.path.exists(path): + raise ValueError(f"Unknown path: {path}") if os.path.isfile(path): normpath = os.path.normpath(path) if normpath not in excluded_paths: From c3a0622b499526ef684c6dde855b925a34911742 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 7 Nov 2023 07:29:11 +0800 Subject: [PATCH 004/109] Fix using categorical data with the score function of ranker. (#9753) --- python-package/xgboost/sklearn.py | 12 ++++++++++- python-package/xgboost/testing/ranking.py | 25 +++++++++++++++++++++++ tests/python-gpu/test_gpu_with_sklearn.py | 7 ++++++- tests/python/test_with_sklearn.py | 7 ++++++- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index d5e20439a..3906973a8 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -2099,7 +2099,17 @@ class XGBRanker(XGBModel, XGBRankerMixIn): """ X, qid = _get_qid(X, None) - Xyq = DMatrix(X, y, qid=qid) + # fixme(jiamingy): base margin and group weight is not yet supported. We might + # need to make extra special fields in the dataframe. + Xyq = DMatrix( + X, + y, + qid=qid, + missing=self.missing, + enable_categorical=self.enable_categorical, + nthread=self.n_jobs, + feature_types=self.feature_types, + ) if callable(self.eval_metric): metric = ltr_metric_decorator(self.eval_metric, self.n_jobs) result_str = self.get_booster().eval_set([(Xyq, "eval")], feval=metric) diff --git a/python-package/xgboost/testing/ranking.py b/python-package/xgboost/testing/ranking.py index 7c75012c2..a11eb3e03 100644 --- a/python-package/xgboost/testing/ranking.py +++ b/python-package/xgboost/testing/ranking.py @@ -75,3 +75,28 @@ def run_ranking_qid_df(impl: ModuleType, tree_method: str) -> None: with pytest.raises(ValueError, match="Either `group` or `qid`."): ranker.fit(df, y, eval_set=[(X, y)]) + + +def run_ranking_categorical(device: str) -> None: + """Test LTR with categorical features.""" + from sklearn.model_selection import cross_val_score + + X, y = tm.make_categorical( + n_samples=512, n_features=10, n_categories=3, onehot=False + ) + rng = np.random.default_rng(1994) + qid = rng.choice(3, size=y.shape[0]) + qid = np.sort(qid) + X["qid"] = qid + + ltr = xgb.XGBRanker(enable_categorical=True, device=device) + ltr.fit(X, y) + score = ltr.score(X, y) + assert score > 0.9 + + ltr = xgb.XGBRanker(enable_categorical=True, device=device) + + # test using the score function inside sklearn. + scores = cross_val_score(ltr, X, y) + for s in scores: + assert s > 0.7 diff --git a/tests/python-gpu/test_gpu_with_sklearn.py b/tests/python-gpu/test_gpu_with_sklearn.py index 9f902ce32..650c0a047 100644 --- a/tests/python-gpu/test_gpu_with_sklearn.py +++ b/tests/python-gpu/test_gpu_with_sklearn.py @@ -10,7 +10,7 @@ import pytest import xgboost as xgb from xgboost import testing as tm -from xgboost.testing.ranking import run_ranking_qid_df +from xgboost.testing.ranking import run_ranking_categorical, run_ranking_qid_df sys.path.append("tests/python") import test_with_sklearn as twskl # noqa @@ -256,6 +256,11 @@ def test_ranking_qid_df(): run_ranking_qid_df(cudf, "gpu_hist") +@pytest.mark.skipif(**tm.no_pandas()) +def test_ranking_categorical() -> None: + run_ranking_categorical(device="cuda") + + @pytest.mark.skipif(**tm.no_cupy()) @pytest.mark.mgpu def test_device_ordinal() -> None: diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index c919a01ad..16f7ab9d1 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -12,7 +12,7 @@ from sklearn.utils.estimator_checks import parametrize_with_checks import xgboost as xgb from xgboost import testing as tm -from xgboost.testing.ranking import run_ranking_qid_df +from xgboost.testing.ranking import run_ranking_categorical, run_ranking_qid_df from xgboost.testing.shared import get_feature_weights, validate_data_initialization from xgboost.testing.updater import get_basescore @@ -173,6 +173,11 @@ def test_ranking(): np.testing.assert_almost_equal(pred, pred_orig) +@pytest.mark.skipif(**tm.no_pandas()) +def test_ranking_categorical() -> None: + run_ranking_categorical(device="cpu") + + def test_ranking_metric() -> None: from sklearn.metrics import roc_auc_score From 6c0a190f6d12d2ba6a1cabd7741881ea1913d433 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 7 Nov 2023 11:12:31 +0800 Subject: [PATCH 005/109] [coll] Add comm group. (#9759) - Implement `CommGroup` for double dispatching. - Small cleanup to tracker for handling abort. --- plugin/federated/federated_comm.cc | 10 +- plugin/federated/federated_comm.cu | 2 + plugin/federated/federated_comm.h | 7 +- src/collective/comm.cc | 20 ++- src/collective/comm_group.cc | 125 ++++++++++++++++ src/collective/comm_group.h | 53 +++++++ src/collective/tracker.cc | 134 +++++++++++------- tests/cpp/collective/test_comm_group.cc | 63 ++++++++ tests/cpp/collective/test_worker.h | 3 +- tests/cpp/common/test_linalg.cc | 14 ++ .../plugin/federated/test_federated_coll.cu | 3 + .../plugin/federated/test_federated_comm.cc | 13 +- .../federated/test_federated_comm_group.cc | 22 +++ .../federated/test_federated_comm_group.cu | 22 +++ tests/cpp/plugin/federated/test_worker.h | 50 ++++++- 15 files changed, 462 insertions(+), 79 deletions(-) create mode 100644 src/collective/comm_group.cc create mode 100644 src/collective/comm_group.h create mode 100644 tests/cpp/collective/test_comm_group.cc create mode 100644 tests/cpp/plugin/federated/test_federated_comm_group.cc create mode 100644 tests/cpp/plugin/federated/test_federated_comm_group.cu diff --git a/plugin/federated/federated_comm.cc b/plugin/federated/federated_comm.cc index 8a649340f..ec1287413 100644 --- a/plugin/federated/federated_comm.cc +++ b/plugin/federated/federated_comm.cc @@ -60,7 +60,8 @@ void FederatedComm::Init(std::string const& host, std::int32_t port, std::int32_ } } -FederatedComm::FederatedComm(Json const& config) { +FederatedComm::FederatedComm(std::int32_t retry, std::chrono::seconds timeout, std::string task_id, + Json const& config) { /** * Topology */ @@ -93,6 +94,13 @@ FederatedComm::FederatedComm(Json const& config) { CHECK_NE(world_size, 0) << "Parameter `federated_world_size` is required."; CHECK(!server_address.empty()) << "Parameter `federated_server_address` is required."; + /** + * Basic config + */ + this->retry_ = retry; + this->timeout_ = timeout; + this->task_id_ = task_id; + /** * Certificates */ diff --git a/plugin/federated/federated_comm.cu b/plugin/federated/federated_comm.cu index b05d38b1b..3eb8eb4f7 100644 --- a/plugin/federated/federated_comm.cu +++ b/plugin/federated/federated_comm.cu @@ -11,6 +11,8 @@ namespace xgboost::collective { CUDAFederatedComm::CUDAFederatedComm(Context const* ctx, std::shared_ptr impl) : FederatedComm{impl}, stream_{ctx->CUDACtx()->Stream()} { CHECK(impl); + CHECK(ctx->IsCUDA()); + dh::safe_cuda(cudaSetDevice(ctx->Ordinal())); } Comm* FederatedComm::MakeCUDAVar(Context const* ctx, std::shared_ptr) const { diff --git a/plugin/federated/federated_comm.h b/plugin/federated/federated_comm.h index fb97a78b0..a24798626 100644 --- a/plugin/federated/federated_comm.h +++ b/plugin/federated/federated_comm.h @@ -27,6 +27,10 @@ class FederatedComm : public Comm { this->rank_ = that->Rank(); this->world_ = that->World(); + this->retry_ = that->Retry(); + this->timeout_ = that->Timeout(); + this->task_id_ = that->TaskID(); + this->tracker_ = that->TrackerInfo(); } @@ -41,7 +45,8 @@ class FederatedComm : public Comm { * - federated_client_key_path * - federated_client_cert_path */ - explicit FederatedComm(Json const& config); + explicit FederatedComm(std::int32_t retry, std::chrono::seconds timeout, std::string task_id, + Json const& config); explicit FederatedComm(std::string const& host, std::int32_t port, std::int32_t world, std::int32_t rank) { this->Init(host, port, world, rank, {}, {}, {}); diff --git a/src/collective/comm.cc b/src/collective/comm.cc index 241dca2ce..964137ff1 100644 --- a/src/collective/comm.cc +++ b/src/collective/comm.cc @@ -5,13 +5,17 @@ #include // for copy #include // for seconds +#include // for exit #include // for shared_ptr +#include // for unique_lock #include // for string #include // for move, forward #include "../common/common.h" // for AssertGPUSupport +#include "../common/json_utils.h" // for OptionalArg #include "allgather.h" // for RingAllgather #include "protocol.h" // for kMagic +#include "tracker.h" // for GetHostAddress #include "xgboost/base.h" // for XGBOOST_STRICT_R_MODE #include "xgboost/collective/socket.h" // for TCPSocket #include "xgboost/json.h" // for Json, Object @@ -209,24 +213,18 @@ RabitComm::RabitComm(std::string const& host, std::int32_t port, std::chrono::se std::shared_ptr error_sock{TCPSocket::CreatePtr(domain)}; auto eport = error_sock->BindHost(); error_sock->Listen(); - error_worker_ = std::thread{[this, error_sock = std::move(error_sock)] { + error_worker_ = std::thread{[error_sock = std::move(error_sock)] { auto conn = error_sock->Accept(); - // On Windows accept returns an invalid socket after network is shutdown. + // On Windows, accept returns a closed socket after finalize. if (conn.IsClosed()) { return; } LOG(WARNING) << "Another worker is running into error."; - std::string scmd; - conn.Recv(&scmd); - auto jcmd = Json::Load(scmd); - auto rc = this->Shutdown(); - if (!rc.OK()) { - LOG(WARNING) << "Fail to shutdown worker:" << rc.Report(); - } #if !defined(XGBOOST_STRICT_R_MODE) || XGBOOST_STRICT_R_MODE == 0 - exit(-1); + // exit is nicer than abort as the former performs cleanups. + std::exit(-1); #else - LOG(FATAL) << rc.Report(); + LOG(FATAL) << "abort"; #endif }}; error_worker_.detach(); diff --git a/src/collective/comm_group.cc b/src/collective/comm_group.cc new file mode 100644 index 000000000..570500843 --- /dev/null +++ b/src/collective/comm_group.cc @@ -0,0 +1,125 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#include "comm_group.h" + +#include // for transform +#include // for seconds +#include // for int32_t +#include // for shared_ptr, unique_ptr +#include // for string +#include // for vector + +#include "../common/json_utils.h" // for OptionalArg +#include "coll.h" // for Coll +#include "comm.h" // for Comm +#include "tracker.h" // for GetHostAddress +#include "xgboost/collective/result.h" // for Result +#include "xgboost/context.h" // for DeviceOrd +#include "xgboost/json.h" // for Json + +#if defined(XGBOOST_USE_FEDERATED) +#include "../../plugin/federated/federated_coll.h" +#include "../../plugin/federated/federated_comm.h" +#endif + +namespace xgboost::collective { +[[nodiscard]] std::shared_ptr CommGroup::Backend(DeviceOrd device) const { + if (device.IsCUDA()) { + if (!gpu_coll_) { + gpu_coll_.reset(backend_->MakeCUDAVar()); + } + return gpu_coll_; + } + return backend_; +} + +[[nodiscard]] Comm const& CommGroup::Ctx(Context const* ctx, DeviceOrd device) const { + if (device.IsCUDA()) { + CHECK(ctx->IsCUDA()); + if (!gpu_comm_) { + gpu_comm_.reset(comm_->MakeCUDAVar(ctx, backend_)); + } + return *gpu_comm_; + } + return *comm_; +} + +CommGroup::CommGroup() + : comm_{std::shared_ptr(new RabitComm{})}, // NOLINT + backend_{std::shared_ptr(new Coll{})} {} // NOLINT + +[[nodiscard]] CommGroup* CommGroup::Create(Json config) { + if (IsA(config)) { + return new CommGroup; + } + + std::string type = OptionalArg(config, "dmlc_communicator", std::string{"rabit"}); + std::vector keys; + // Try both lower and upper case for compatibility + auto get_param = [&](std::string name, auto dft, auto t) { + std::string upper; + std::transform(name.cbegin(), name.cend(), std::back_inserter(upper), + [](char c) { return std::toupper(c); }); + std::transform(name.cbegin(), name.cend(), name.begin(), + [](char c) { return std::tolower(c); }); + keys.push_back(upper); + keys.push_back(name); + + auto const& obj = get(config); + auto it = obj.find(upper); + if (it != obj.cend()) { + return OptionalArg(config, upper, dft); + } else { + return OptionalArg(config, name, dft); + } + }; + // Common args + auto retry = + OptionalArg(config, "dmlc_retry", static_cast(DefaultRetry())); + auto timeout = OptionalArg(config, "dmlc_timeout_sec", + static_cast(DefaultTimeoutSec())); + auto task_id = get_param("dmlc_task_id", std::string{}, String{}); + + if (type == "rabit") { + auto host = get_param("dmlc_tracker_uri", std::string{}, String{}); + auto port = get_param("dmlc_tracker_port", static_cast(0), Integer{}); + auto ptr = + new CommGroup{std::shared_ptr{new RabitComm{ // NOLINT + host, static_cast(port), std::chrono::seconds{timeout}, + static_cast(retry), task_id}}, + std::shared_ptr(new Coll{})}; // NOLINT + return ptr; + } else if (type == "federated") { +#if defined(XGBOOST_USE_FEDERATED) + auto ptr = new CommGroup{ + std::make_shared(retry, std::chrono::seconds{timeout}, task_id, config), + std::make_shared()}; + return ptr; +#endif // defined(XGBOOST_USE_FEDERATED) + } else { + LOG(FATAL) << "Invalid communicator type"; + } + + return nullptr; +} + +std::unique_ptr& GlobalCommGroup() { + static std::unique_ptr sptr; + if (!sptr) { + Json config{Null{}}; + sptr.reset(CommGroup::Create(config)); + } + return sptr; +} + +void GlobalCommGroupInit(Json config) { + auto& sptr = GlobalCommGroup(); + sptr.reset(CommGroup::Create(std::move(config))); +} + +void GlobalCommGroupFinalize() { + auto& sptr = GlobalCommGroup(); + sptr.reset(); +} +} // namespace xgboost::collective diff --git a/src/collective/comm_group.h b/src/collective/comm_group.h new file mode 100644 index 000000000..62f3e565f --- /dev/null +++ b/src/collective/comm_group.h @@ -0,0 +1,53 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#pragma once +#include // for shared_ptr, unique_ptr +#include // for string +#include // for move + +#include "coll.h" // for Comm +#include "comm.h" // for Coll +#include "xgboost/collective/result.h" // for Result +#include "xgboost/collective/socket.h" // for GetHostName + +namespace xgboost::collective { +/** + * @brief Communicator group used for double dispatching between communicators and + * collective implementations. + */ +class CommGroup { + std::shared_ptr comm_; + mutable std::shared_ptr gpu_comm_; + + std::shared_ptr backend_; + mutable std::shared_ptr gpu_coll_; // lazy initialization + + CommGroup(std::shared_ptr comm, std::shared_ptr coll) + : comm_{std::move(comm)}, backend_{std::move(coll)} {} + + public: + CommGroup(); + + [[nodiscard]] auto World() const { return comm_->World(); } + [[nodiscard]] auto Rank() const { return comm_->Rank(); } + [[nodiscard]] bool IsDistributed() const { return comm_->IsDistributed(); } + + [[nodiscard]] static CommGroup* Create(Json config); + + [[nodiscard]] std::shared_ptr Backend(DeviceOrd device) const; + [[nodiscard]] Comm const& Ctx(Context const* ctx, DeviceOrd device) const; + [[nodiscard]] Result SignalError(Result const& res) { return comm_->SignalError(res); } + + [[nodiscard]] Result ProcessorName(std::string* out) const { + auto rc = GetHostName(out); + return rc; + } +}; + +std::unique_ptr& GlobalCommGroup(); + +void GlobalCommGroupInit(Json config); + +void GlobalCommGroupFinalize(); +} // namespace xgboost::collective diff --git a/src/collective/tracker.cc b/src/collective/tracker.cc index 4837e2ace..88c51d8a9 100644 --- a/src/collective/tracker.cc +++ b/src/collective/tracker.cc @@ -58,36 +58,35 @@ Result Tracker::WaitUntilReady() const { RabitTracker::WorkerProxy::WorkerProxy(std::int32_t world, TCPSocket sock, SockAddrV4 addr) : sock_{std::move(sock)} { - auto host = addr.Addr(); - std::int32_t rank{0}; - rc_ = Success() - << [&] { return proto::Magic{}.Verify(&sock_); } - << [&] { return proto::Connect{}.TrackerRecv(&sock_, &world_, &rank, &task_id_); }; - if (!rc_.OK()) { - return; - } - - std::string cmd; - sock_.Recv(&cmd); - auto jcmd = Json::Load(StringView{cmd}); - cmd_ = static_cast(get(jcmd["cmd"])); + Json jcmd; std::int32_t port{0}; - if (cmd_ == proto::CMD::kStart) { - proto::Start start; - rc_ = start.TrackerHandle(jcmd, &world_, world, &port, &sock_, &eport_); - } else if (cmd_ == proto::CMD::kPrint) { - proto::Print print; - rc_ = print.TrackerHandle(jcmd, &msg_); - } else if (cmd_ == proto::CMD::kError) { - proto::ErrorCMD error; - rc_ = error.TrackerHandle(jcmd, &msg_, &code_); - } - if (!rc_.OK()) { - return; - } - info_ = proto::PeerInfo{host, port, rank}; + rc_ = Success() << [&] { return proto::Magic{}.Verify(&sock_); } << [&] { + return proto::Connect{}.TrackerRecv(&sock_, &world_, &rank, &task_id_); + } << [&] { + std::string cmd; + sock_.Recv(&cmd); + jcmd = Json::Load(StringView{cmd}); + cmd_ = static_cast(get(jcmd["cmd"])); + return Success(); + } << [&] { + if (cmd_ == proto::CMD::kStart) { + proto::Start start; + return start.TrackerHandle(jcmd, &world_, world, &port, &sock_, &eport_); + } else if (cmd_ == proto::CMD::kPrint) { + proto::Print print; + return print.TrackerHandle(jcmd, &msg_); + } else if (cmd_ == proto::CMD::kError) { + proto::ErrorCMD error; + return error.TrackerHandle(jcmd, &msg_, &code_); + } + return Success(); + } << [&] { + auto host = addr.Addr(); + info_ = proto::PeerInfo{host, port, rank}; + return Success(); + }; } RabitTracker::RabitTracker(Json const& config) : Tracker{config} { @@ -137,15 +136,18 @@ Result RabitTracker::Bootstrap(std::vector* p_workers) { std::int32_t n_shutdown{0}; bool during_restart{false}; + bool running{false}; std::vector pending; explicit State(std::int32_t world) : n_workers{world} {} State(State const& that) = delete; State& operator=(State&& that) = delete; + // modifiers void Start(WorkerProxy&& worker) { CHECK_LT(pending.size(), n_workers); CHECK_LE(n_shutdown, n_workers); + CHECK(!running); pending.emplace_back(std::forward(worker)); @@ -155,6 +157,7 @@ Result RabitTracker::Bootstrap(std::vector* p_workers) { CHECK_GE(n_shutdown, 0); CHECK_LT(n_shutdown, n_workers); + running = false; ++n_shutdown; CHECK_LE(n_shutdown, n_workers); @@ -163,21 +166,26 @@ Result RabitTracker::Bootstrap(std::vector* p_workers) { CHECK_LE(pending.size(), n_workers); CHECK_LE(n_shutdown, n_workers); + running = false; during_restart = true; } - [[nodiscard]] bool Ready() const { - CHECK_LE(pending.size(), n_workers); - return static_cast(pending.size()) == n_workers; - } void Bootstrap() { CHECK_EQ(pending.size(), n_workers); CHECK_LE(n_shutdown, n_workers); + running = true; + // A reset. n_shutdown = 0; during_restart = false; pending.clear(); } + + // observers + [[nodiscard]] bool Ready() const { + CHECK_LE(pending.size(), n_workers); + return static_cast(pending.size()) == n_workers; + } [[nodiscard]] bool ShouldContinue() const { CHECK_LE(pending.size(), n_workers); CHECK_LE(n_shutdown, n_workers); @@ -187,7 +195,31 @@ Result RabitTracker::Bootstrap(std::vector* p_workers) { } }; - return std::async(std::launch::async, [this] { + auto handle_error = [&](WorkerProxy const& worker) { + auto msg = worker.Msg(); + auto code = worker.Code(); + LOG(WARNING) << "Recieved error from [" << worker.Host() << ":" << worker.Rank() << "]: " << msg + << " code:" << code; + auto host = worker.Host(); + // We signal all workers for the error, if they haven't aborted already. + for (auto& w : worker_error_handles_) { + if (w.first == host) { + continue; + } + TCPSocket out; + // Connecting to the error port as a signal for exit. + // + // retry is set to 1, just let the worker timeout or error. Otherwise the + // tracker and the worker might be waiting for each other. + auto rc = Connect(w.first, w.second, 1, timeout_, &out); + if (!rc.OK()) { + return Fail("Failed to inform workers to stop."); + } + } + return Success(); + }; + + return std::async(std::launch::async, [this, handle_error] { State state{this->n_workers_}; while (state.ShouldContinue()) { @@ -205,6 +237,16 @@ Result RabitTracker::Bootstrap(std::vector* p_workers) { } switch (worker.Command()) { case proto::CMD::kStart: { + if (state.running) { + // Something went wrong with one of the workers. It got disconnected without + // notice. + state.Error(); + rc = handle_error(worker); + if (!rc.OK()) { + return Fail("Failed to handle abort.", std::move(rc)); + } + } + state.Start(std::move(worker)); if (state.Ready()) { rc = this->Bootstrap(&state.pending); @@ -216,36 +258,20 @@ Result RabitTracker::Bootstrap(std::vector* p_workers) { continue; } case proto::CMD::kShutdown: { + if (state.during_restart) { + // The worker can still send shutdown after call to `std::exit`. + continue; + } state.Shutdown(); continue; } case proto::CMD::kError: { if (state.during_restart) { + // Ignore further errors. continue; } state.Error(); - auto msg = worker.Msg(); - auto code = worker.Code(); - LOG(WARNING) << "Recieved error from [" << worker.Host() << ":" << worker.Rank() - << "]: " << msg << " code:" << code; - auto host = worker.Host(); - // We signal all workers for the error, if they haven't aborted already. - for (auto& w : worker_error_handles_) { - if (w.first == host) { - continue; - } - TCPSocket out; - // retry is set to 1, just let the worker timeout or error. Otherwise the - // tracker and the worker might be waiting for each other. - auto rc = Connect(w.first, w.second, 1, timeout_, &out); - // send signal to stop the worker. - proto::ShutdownCMD shutdown; - rc = shutdown.Send(&out); - if (!rc.OK()) { - return Fail("Failed to inform workers to stop."); - } - } - + rc = handle_error(worker); continue; } case proto::CMD::kPrint: { diff --git a/tests/cpp/collective/test_comm_group.cc b/tests/cpp/collective/test_comm_group.cc new file mode 100644 index 000000000..0f6bc23a2 --- /dev/null +++ b/tests/cpp/collective/test_comm_group.cc @@ -0,0 +1,63 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#include +#include // for Json + +#include // for seconds +#include // for int32_t +#include // for string +#include // for thread + +#include "../../../src/collective/comm.h" +#include "../../../src/collective/comm_group.h" +#include "../../../src/common/common.h" // for AllVisibleGPUs +#include "../helpers.h" // for MakeCUDACtx +#include "test_worker.h" // for TestDistributed + +namespace xgboost::collective { +namespace { +auto MakeConfig(std::string host, std::int32_t port, std::chrono::seconds timeout, std::int32_t r) { + Json config{Object{}}; + config["dmlc_communicator"] = std::string{"rabit"}; + config["DMLC_TRACKER_URI"] = host; + config["DMLC_TRACKER_PORT"] = port; + config["dmlc_timeout_sec"] = static_cast(timeout.count()); + config["DMLC_TASK_ID"] = std::to_string(r); + config["dmlc_retry"] = 2; + return config; +} + +class CommGroupTest : public SocketTest {}; +} // namespace + +TEST_F(CommGroupTest, Basic) { + std::int32_t n_workers = std::min(std::thread::hardware_concurrency(), 5u); + TestDistributed(n_workers, [&](std::string host, std::int32_t port, std::chrono::seconds timeout, + std::int32_t r) { + Context ctx; + auto config = MakeConfig(host, port, timeout, r); + std::unique_ptr ptr{CommGroup::Create(config)}; + ASSERT_TRUE(ptr->IsDistributed()); + ASSERT_EQ(ptr->World(), n_workers); + auto const& comm = ptr->Ctx(&ctx, DeviceOrd::CPU()); + ASSERT_EQ(comm.TaskID(), std::to_string(r)); + ASSERT_EQ(comm.Retry(), 2); + }); +} + +#if defined(XGBOOST_USE_NCCL) +TEST_F(CommGroupTest, BasicGPU) { + std::int32_t n_workers = common::AllVisibleGPUs(); + TestDistributed(n_workers, [&](std::string host, std::int32_t port, std::chrono::seconds timeout, + std::int32_t r) { + auto ctx = MakeCUDACtx(r); + auto config = MakeConfig(host, port, timeout, r); + std::unique_ptr ptr{CommGroup::Create(config)}; + auto const& comm = ptr->Ctx(&ctx, DeviceOrd::CUDA(0)); + ASSERT_EQ(comm.TaskID(), std::to_string(r)); + ASSERT_EQ(comm.Retry(), 2); + }); +} +#endif // for defined(XGBOOST_USE_NCCL) +} // namespace xgboost::collective diff --git a/tests/cpp/collective/test_worker.h b/tests/cpp/collective/test_worker.h index 6578ff142..ad3213e81 100644 --- a/tests/cpp/collective/test_worker.h +++ b/tests/cpp/collective/test_worker.h @@ -95,7 +95,8 @@ void TestDistributed(std::int32_t n_workers, WorkerFn worker_fn) { std::chrono::seconds timeout{1}; std::string host; - ASSERT_TRUE(GetHostAddress(&host).OK()); + auto rc = GetHostAddress(&host); + ASSERT_TRUE(rc.OK()) << rc.Report(); RabitTracker tracker{StringView{host}, n_workers, 0, timeout}; auto fut = tracker.Run(); diff --git a/tests/cpp/common/test_linalg.cc b/tests/cpp/common/test_linalg.cc index f345b3a78..21c5ad30d 100644 --- a/tests/cpp/common/test_linalg.cc +++ b/tests/cpp/common/test_linalg.cc @@ -15,6 +15,15 @@ namespace xgboost::linalg { namespace { DeviceOrd CPU() { return DeviceOrd::CPU(); } + +template +void ConstView(linalg::VectorView v1, linalg::VectorView> v2) { + // compile test for being able to pass non-const view to const view. + auto s = v1.Slice(linalg::All()); + ASSERT_EQ(s.Size(), v1.Size()); + auto s2 = v2.Slice(linalg::All()); + ASSERT_EQ(s2.Size(), v2.Size()); +} } // namespace auto MakeMatrixFromTest(HostDeviceVector *storage, std::size_t n_rows, std::size_t n_cols) { @@ -206,6 +215,11 @@ TEST(Linalg, TensorView) { ASSERT_TRUE(t.FContiguous()); ASSERT_FALSE(t.CContiguous()); } + { + // const + TensorView t{data, {data.size()}, CPU()}; + ConstView(t, t); + } } TEST(Linalg, Tensor) { diff --git a/tests/cpp/plugin/federated/test_federated_coll.cu b/tests/cpp/plugin/federated/test_federated_coll.cu index 44211f8d7..a6ec7e352 100644 --- a/tests/cpp/plugin/federated/test_federated_coll.cu +++ b/tests/cpp/plugin/federated/test_federated_coll.cu @@ -124,6 +124,9 @@ TEST_F(FederatedCollTestGPU, Allgather) { TEST_F(FederatedCollTestGPU, AllgatherV) { std::int32_t n_workers = 2; + if (common::AllVisibleGPUs() < n_workers) { + GTEST_SKIP_("At least 2 GPUs are required for the test."); + } TestFederated(n_workers, [=](std::shared_ptr comm, std::int32_t rank) { TestAllgatherV(comm, rank); }); diff --git a/tests/cpp/plugin/federated/test_federated_comm.cc b/tests/cpp/plugin/federated/test_federated_comm.cc index b45b00910..0d0692b5f 100644 --- a/tests/cpp/plugin/federated/test_federated_comm.cc +++ b/tests/cpp/plugin/federated/test_federated_comm.cc @@ -1,6 +1,7 @@ /** * Copyright 2022-2023, XGBoost contributors */ +#include #include #include // for string @@ -19,12 +20,14 @@ class FederatedCommTest : public SocketTest {}; TEST_F(FederatedCommTest, ThrowOnWorldSizeTooSmall) { auto construct = [] { FederatedComm comm{"localhost", 0, 0, 0}; }; - ExpectThrow("Invalid world size.", construct); + ASSERT_THAT(construct, + ::testing::ThrowsMessage(::testing::HasSubstr("Invalid world size"))); } TEST_F(FederatedCommTest, ThrowOnRankTooSmall) { auto construct = [] { FederatedComm comm{"localhost", 0, 1, -1}; }; - ExpectThrow("Invalid worker rank.", construct); + ASSERT_THAT(construct, + ::testing::ThrowsMessage(::testing::HasSubstr("Invalid worker rank."))); } TEST_F(FederatedCommTest, ThrowOnRankTooBig) { @@ -38,7 +41,7 @@ TEST_F(FederatedCommTest, ThrowOnWorldSizeNotInteger) { config["federated_server_address"] = std::string("localhost:0"); config["federated_world_size"] = std::string("1"); config["federated_rank"] = Integer(0); - FederatedComm comm(config); + FederatedComm comm{DefaultRetry(), std::chrono::seconds{DefaultTimeoutSec()}, "", config}; }; ExpectThrow("got: `String`", construct); } @@ -49,7 +52,7 @@ TEST_F(FederatedCommTest, ThrowOnRankNotInteger) { config["federated_server_address"] = std::string("localhost:0"); config["federated_world_size"] = 1; config["federated_rank"] = std::string("0"); - FederatedComm comm(config); + FederatedComm comm(DefaultRetry(), std::chrono::seconds{DefaultTimeoutSec()}, "", config); }; ExpectThrow("got: `String`", construct); } @@ -59,7 +62,7 @@ TEST_F(FederatedCommTest, GetWorldSizeAndRank) { config["federated_world_size"] = 6; config["federated_rank"] = 3; config["federated_server_address"] = String{"localhost:0"}; - FederatedComm comm{config}; + FederatedComm comm{DefaultRetry(), std::chrono::seconds{DefaultTimeoutSec()}, "", config}; EXPECT_EQ(comm.World(), 6); EXPECT_EQ(comm.Rank(), 3); } diff --git a/tests/cpp/plugin/federated/test_federated_comm_group.cc b/tests/cpp/plugin/federated/test_federated_comm_group.cc new file mode 100644 index 000000000..9bfbdd3ae --- /dev/null +++ b/tests/cpp/plugin/federated/test_federated_comm_group.cc @@ -0,0 +1,22 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#include +#include // for Json + +#include "../../../../src/collective/comm_group.h" +#include "../../helpers.h" +#include "test_worker.h" + +namespace xgboost::collective { +TEST(CommGroup, Federated) { + std::int32_t n_workers = common::AllVisibleGPUs(); + TestFederatedGroup(n_workers, [&](std::shared_ptr comm_group, std::int32_t r) { + Context ctx; + ASSERT_EQ(comm_group->Rank(), r); + auto const& comm = comm_group->Ctx(&ctx, DeviceOrd::CPU()); + ASSERT_EQ(comm.TaskID(), std::to_string(r)); + ASSERT_EQ(comm.Retry(), 2); + }); +} +} // namespace xgboost::collective diff --git a/tests/cpp/plugin/federated/test_federated_comm_group.cu b/tests/cpp/plugin/federated/test_federated_comm_group.cu new file mode 100644 index 000000000..747adb6fd --- /dev/null +++ b/tests/cpp/plugin/federated/test_federated_comm_group.cu @@ -0,0 +1,22 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#include +#include // for Json + +#include "../../../../src/collective/comm_group.h" +#include "../../helpers.h" +#include "test_worker.h" + +namespace xgboost::collective { +TEST(CommGroup, FederatedGPU) { + std::int32_t n_workers = common::AllVisibleGPUs(); + TestFederatedGroup(n_workers, [&](std::shared_ptr comm_group, std::int32_t r) { + Context ctx = MakeCUDACtx(0); + auto const& comm = comm_group->Ctx(&ctx, DeviceOrd::CUDA(0)); + ASSERT_EQ(comm_group->Rank(), r); + ASSERT_EQ(comm.TaskID(), std::to_string(r)); + ASSERT_EQ(comm.Retry(), 2); + }); +} +} // namespace xgboost::collective diff --git a/tests/cpp/plugin/federated/test_worker.h b/tests/cpp/plugin/federated/test_worker.h index 38bc32c60..d0edecc15 100644 --- a/tests/cpp/plugin/federated/test_worker.h +++ b/tests/cpp/plugin/federated/test_worker.h @@ -5,10 +5,12 @@ #include -#include // for ms +#include // for ms, seconds +#include // for shared_ptr #include // for thread #include "../../../../plugin/federated/federated_tracker.h" +#include "../../../../src/collective/comm_group.h" #include "federated_comm.h" // for FederatedComm #include "xgboost/json.h" // for Json @@ -23,9 +25,8 @@ void TestFederated(std::int32_t n_workers, WorkerFn&& fn) { std::vector workers; using namespace std::chrono_literals; - while (tracker.Port() == 0) { - std::this_thread::sleep_for(100ms); - } + auto rc = tracker.WaitUntilReady(); + ASSERT_TRUE(rc.OK()) << rc.Report(); std::int32_t port = tracker.Port(); for (std::int32_t i = 0; i < n_workers; ++i) { @@ -34,7 +35,8 @@ void TestFederated(std::int32_t n_workers, WorkerFn&& fn) { config["federated_world_size"] = n_workers; config["federated_rank"] = i; config["federated_server_address"] = "0.0.0.0:" + std::to_string(port); - auto comm = std::make_shared(config); + auto comm = std::make_shared( + DefaultRetry(), std::chrono::seconds{DefaultTimeoutSec()}, std::to_string(i), config); fn(comm, i); }); @@ -44,7 +46,43 @@ void TestFederated(std::int32_t n_workers, WorkerFn&& fn) { t.join(); } - auto rc = tracker.Shutdown(); + rc = tracker.Shutdown(); + ASSERT_TRUE(rc.OK()) << rc.Report(); + ASSERT_TRUE(fut.get().OK()); +} + +template +void TestFederatedGroup(std::int32_t n_workers, WorkerFn&& fn) { + Json config{Object()}; + config["federated_secure"] = Boolean{false}; + config["n_workers"] = Integer{n_workers}; + FederatedTracker tracker{config}; + auto fut = tracker.Run(); + + std::vector workers; + auto rc = tracker.WaitUntilReady(); + ASSERT_TRUE(rc.OK()) << rc.Report(); + std::int32_t port = tracker.Port(); + + for (std::int32_t i = 0; i < n_workers; ++i) { + workers.emplace_back([=] { + Json config{Object{}}; + config["dmlc_communicator"] = std::string{"federated"}; + config["dmlc_task_id"] = std::to_string(i); + config["dmlc_retry"] = 2; + config["federated_world_size"] = n_workers; + config["federated_rank"] = i; + config["federated_server_address"] = "0.0.0.0:" + std::to_string(port); + std::shared_ptr comm_group{CommGroup::Create(config)}; + fn(comm_group, i); + }); + } + + for (auto& t : workers) { + t.join(); + } + + rc = tracker.Shutdown(); ASSERT_TRUE(rc.OK()) << rc.Report(); ASSERT_TRUE(fut.get().OK()); } From 06bdc15e9b72179c4bcdd01d43ca452ed72d5753 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 8 Nov 2023 09:54:05 +0800 Subject: [PATCH 006/109] [coll] Pass context to various functions. (#9772) * [coll] Pass context to various functions. In the future, the `Context` object would be required for collective operations, this PR passes the context object to some required functions to prepare for swapping out the implementation. --- include/xgboost/data.h | 2 +- include/xgboost/linalg.h | 12 +- plugin/federated/federated_coll.cc | 6 +- src/collective/allreduce.cc | 3 + src/collective/comm.cu | 3 +- src/collective/comm_group.cc | 2 +- src/common/device_helpers.cuh | 3 +- src/common/hist_util.cc | 4 +- src/common/hist_util.cu | 2 +- src/common/quantile.cc | 27 ++-- src/common/quantile.cu | 13 +- src/common/quantile.cuh | 4 +- src/common/quantile.h | 9 +- src/data/data.cc | 2 +- src/data/iterative_dmatrix.cc | 12 +- src/data/iterative_dmatrix.cu | 4 +- src/data/simple_dmatrix.cc | 2 +- src/data/simple_dmatrix.cu | 2 +- src/data/sparse_page_dmatrix.cc | 2 +- src/learner.cc | 4 +- src/metric/auc.cu | 28 ++--- src/metric/elementwise_metric.cu | 6 +- src/metric/rank_metric.cc | 16 +-- src/metric/survival_metric.cu | 2 +- src/predictor/cpu_predictor.cc | 35 +++--- src/predictor/gpu_predictor.cu | 14 +-- src/tree/gpu_hist/evaluate_splits.cu | 33 ++--- src/tree/gpu_hist/evaluate_splits.cuh | 4 +- src/tree/gpu_hist/histogram.cu | 11 +- src/tree/gpu_hist/histogram.cuh | 14 ++- src/tree/hist/histogram.h | 9 +- src/tree/hist/param.cc | 2 +- src/tree/hist/param.h | 2 +- src/tree/updater_approx.cc | 4 +- src/tree/updater_gpu_hist.cu | 10 +- src/tree/updater_quantile_hist.cc | 10 +- tests/cpp/common/test_hist_util.cu | 49 +++++--- tests/cpp/common/test_quantile.cc | 8 +- tests/cpp/common/test_quantile.cu | 6 +- tests/cpp/plugin/helpers.h | 1 + tests/cpp/test_learner.cc | 5 +- .../cpp/tree/gpu_hist/test_evaluate_splits.cu | 115 ++++++++++-------- tests/cpp/tree/gpu_hist/test_histogram.cu | 6 +- tests/cpp/tree/hist/test_histogram.cc | 14 +-- tests/cpp/tree/test_gpu_hist.cu | 8 +- 45 files changed, 275 insertions(+), 255 deletions(-) diff --git a/include/xgboost/data.h b/include/xgboost/data.h index 04b489d8b..69176994b 100644 --- a/include/xgboost/data.h +++ b/include/xgboost/data.h @@ -178,7 +178,7 @@ class MetaInfo { * in vertical federated learning, since each worker loads its own list of columns, * we need to sum them. */ - void SynchronizeNumberOfColumns(); + void SynchronizeNumberOfColumns(Context const* ctx); /*! \brief Whether the data is split row-wise. */ bool IsRowSplit() const { diff --git a/include/xgboost/linalg.h b/include/xgboost/linalg.h index 901c9ae91..8806818fb 100644 --- a/include/xgboost/linalg.h +++ b/include/xgboost/linalg.h @@ -582,20 +582,20 @@ auto MakeTensorView(Context const *ctx, Container &data, S &&...shape) { // NOL return TensorView{data, in_shape, ctx->Device()}; } -template -LINALG_HD auto MakeTensorView(DeviceOrd device, common::Span data, S &&...shape) { +template +LINALG_HD auto MakeTensorView(DeviceOrd device, common::Span data, S &&...shape) { std::size_t in_shape[sizeof...(S)]; detail::IndexToArr(in_shape, std::forward(shape)...); return TensorView{data, in_shape, device}; } -template -auto MakeTensorView(Context const *ctx, common::Span data, S &&...shape) { +template +auto MakeTensorView(Context const *ctx, common::Span data, S &&...shape) { return MakeTensorView(ctx->Device(), data, std::forward(shape)...); } -template -auto MakeTensorView(Context const *ctx, Order order, common::Span data, S &&...shape) { +template +auto MakeTensorView(Context const *ctx, Order order, common::Span data, S &&...shape) { std::size_t in_shape[sizeof...(S)]; detail::IndexToArr(in_shape, std::forward(shape)...); return TensorView{data, in_shape, ctx->Device(), order}; diff --git a/plugin/federated/federated_coll.cc b/plugin/federated/federated_coll.cc index 7c25eeba5..980992d61 100644 --- a/plugin/federated/federated_coll.cc +++ b/plugin/federated/federated_coll.cc @@ -29,7 +29,7 @@ namespace { auto stub = fed->Handle(); BroadcastRequest request; - request.set_sequence_number(*sequence_number++); + request.set_sequence_number((*sequence_number)++); request.set_rank(comm.Rank()); if (comm.Rank() != root) { request.set_send_buffer(nullptr, 0); @@ -90,9 +90,9 @@ Coll *FederatedColl::MakeCUDAVar() { [[nodiscard]] Result FederatedColl::Broadcast(Comm const &comm, common::Span data, std::int32_t root) { if (comm.Rank() == root) { - return BroadcastImpl(comm, &sequence_number_, data, root); + return BroadcastImpl(comm, &this->sequence_number_, data, root); } else { - return BroadcastImpl(comm, &sequence_number_, data, root); + return BroadcastImpl(comm, &this->sequence_number_, data, root); } } diff --git a/src/collective/allreduce.cc b/src/collective/allreduce.cc index 6948f6758..65c066868 100644 --- a/src/collective/allreduce.cc +++ b/src/collective/allreduce.cc @@ -62,6 +62,9 @@ Result RingScatterReduceTyped(Comm const& comm, common::Span data, Result RingAllreduce(Comm const& comm, common::Span data, Func const& op, ArrayInterfaceHandler::Type type) { + if (comm.World() == 1) { + return Success(); + } return DispatchDType(type, [&](auto t) { using T = decltype(t); // Divide the data into segments according to the number of workers. diff --git a/src/collective/comm.cu b/src/collective/comm.cu index 09faf31cd..09edc522d 100644 --- a/src/collective/comm.cu +++ b/src/collective/comm.cu @@ -10,6 +10,7 @@ #include // for stringstream #include // for vector +#include "../common/cuda_context.cuh" // for CUDAContext #include "../common/device_helpers.cuh" // for DefaultStream #include "../common/type.h" // for EraseType #include "broadcast.h" // for Broadcast @@ -60,7 +61,7 @@ Comm* Comm::MakeCUDAVar(Context const* ctx, std::shared_ptr pimpl) const { NCCLComm::NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr pimpl) : Comm{root.TrackerInfo().host, root.TrackerInfo().port, root.Timeout(), root.Retry(), root.TaskID()}, - stream_{dh::DefaultStream()} { + stream_{ctx->CUDACtx()->Stream()} { this->world_ = root.World(); this->rank_ = root.Rank(); this->domain_ = root.Domain(); diff --git a/src/collective/comm_group.cc b/src/collective/comm_group.cc index 570500843..3d2e24492 100644 --- a/src/collective/comm_group.cc +++ b/src/collective/comm_group.cc @@ -105,7 +105,7 @@ CommGroup::CommGroup() } std::unique_ptr& GlobalCommGroup() { - static std::unique_ptr sptr; + static thread_local std::unique_ptr sptr; if (!sptr) { Json config{Null{}}; sptr.reset(CommGroup::Create(config)); diff --git a/src/common/device_helpers.cuh b/src/common/device_helpers.cuh index 89b3ad2e6..74336ac61 100644 --- a/src/common/device_helpers.cuh +++ b/src/common/device_helpers.cuh @@ -480,7 +480,8 @@ struct XGBCachingDeviceAllocatorImpl : XGBBaseDeviceAllocator { cub::CachingDeviceAllocator& GetGlobalCachingAllocator() { // Configure allocator with maximum cached bin size of ~1GB and no limit on // maximum cached bytes - thread_local cub::CachingDeviceAllocator *allocator = new cub::CachingDeviceAllocator(2, 9, 29); + thread_local std::unique_ptr allocator{ + std::make_unique(2, 9, 29)}; return *allocator; } pointer allocate(size_t n) { // NOLINT diff --git a/src/common/hist_util.cc b/src/common/hist_util.cc index 65ab18630..f10124792 100644 --- a/src/common/hist_util.cc +++ b/src/common/hist_util.cc @@ -51,7 +51,7 @@ HistogramCuts SketchOnDMatrix(Context const *ctx, DMatrix *m, bst_bin_t max_bins for (auto const &page : m->GetBatches()) { container.PushRowPage(page, info, hessian); } - container.MakeCuts(m->Info(), &out); + container.MakeCuts(ctx, m->Info(), &out); } else { SortedSketchContainer container{ctx, max_bins, @@ -61,7 +61,7 @@ HistogramCuts SketchOnDMatrix(Context const *ctx, DMatrix *m, bst_bin_t max_bins for (auto const &page : m->GetBatches(ctx)) { container.PushColPage(page, info, hessian); } - container.MakeCuts(m->Info(), &out); + container.MakeCuts(ctx, m->Info(), &out); } return out; diff --git a/src/common/hist_util.cu b/src/common/hist_util.cu index 1f06c2a6f..fbe6356bf 100644 --- a/src/common/hist_util.cu +++ b/src/common/hist_util.cu @@ -359,7 +359,7 @@ HistogramCuts DeviceSketchWithHessian(Context const* ctx, DMatrix* p_fmat, bst_b } } - sketch_container.MakeCuts(&cuts, p_fmat->Info().IsColumnSplit()); + sketch_container.MakeCuts(ctx, &cuts, p_fmat->Info().IsColumnSplit()); return cuts; } } // namespace xgboost::common diff --git a/src/common/quantile.cc b/src/common/quantile.cc index 5250abd0f..0490add26 100644 --- a/src/common/quantile.cc +++ b/src/common/quantile.cc @@ -11,9 +11,7 @@ #include "categorical.h" #include "hist_util.h" -namespace xgboost { -namespace common { - +namespace xgboost::common { template SketchContainerImpl::SketchContainerImpl(Context const *ctx, std::vector columns_size, @@ -129,7 +127,7 @@ struct QuantileAllreduce { * \param rank rank of target worker * \param fidx feature idx */ - auto Values(int32_t rank, bst_feature_t fidx) const { + [[nodiscard]] auto Values(int32_t rank, bst_feature_t fidx) const { // get span for worker auto wsize = worker_indptr[rank + 1] - worker_indptr[rank]; auto worker_values = global_values.subspan(worker_indptr[rank], wsize); @@ -145,7 +143,7 @@ struct QuantileAllreduce { template void SketchContainerImpl::GatherSketchInfo( - MetaInfo const& info, + Context const *, MetaInfo const &info, std::vector const &reduced, std::vector *p_worker_segments, std::vector *p_sketches_scan, std::vector *p_global_sketches) { @@ -206,7 +204,7 @@ void SketchContainerImpl::GatherSketchInfo( } template -void SketchContainerImpl::AllreduceCategories(MetaInfo const& info) { +void SketchContainerImpl::AllreduceCategories(Context const*, MetaInfo const& info) { auto world_size = collective::GetWorldSize(); auto rank = collective::GetRank(); if (world_size == 1 || info.IsColumnSplit()) { @@ -274,16 +272,15 @@ void SketchContainerImpl::AllreduceCategories(MetaInfo const& info) { template void SketchContainerImpl::AllReduce( - MetaInfo const& info, - std::vector *p_reduced, - std::vector* p_num_cuts) { + Context const *ctx, MetaInfo const &info, + std::vector *p_reduced, std::vector *p_num_cuts) { monitor_.Start(__func__); size_t n_columns = sketches_.size(); collective::Allreduce(&n_columns, 1); CHECK_EQ(n_columns, sketches_.size()) << "Number of columns differs across workers"; - AllreduceCategories(info); + AllreduceCategories(ctx, info); auto& num_cuts = *p_num_cuts; CHECK_EQ(num_cuts.size(), 0); @@ -324,7 +321,7 @@ void SketchContainerImpl::AllReduce( std::vector sketches_scan((n_columns + 1) * world, 0); std::vector global_sketches; - this->GatherSketchInfo(info, reduced, &worker_segments, &sketches_scan, &global_sketches); + this->GatherSketchInfo(ctx, info, reduced, &worker_segments, &sketches_scan, &global_sketches); std::vector final_sketches(n_columns); @@ -383,11 +380,12 @@ auto AddCategories(std::set const &categories, HistogramCuts *cuts) { } template -void SketchContainerImpl::MakeCuts(MetaInfo const &info, HistogramCuts *p_cuts) { +void SketchContainerImpl::MakeCuts(Context const *ctx, MetaInfo const &info, + HistogramCuts *p_cuts) { monitor_.Start(__func__); std::vector reduced; std::vector num_cuts; - this->AllReduce(info, &reduced, &num_cuts); + this->AllReduce(ctx, info, &reduced, &num_cuts); p_cuts->min_vals_.HostVector().resize(sketches_.size(), 0.0f); std::vector final_summaries(reduced.size()); @@ -496,5 +494,4 @@ void SortedSketchContainer::PushColPage(SparsePage const &page, MetaInfo const & }); monitor_.Stop(__func__); } -} // namespace common -} // namespace xgboost +} // namespace xgboost::common diff --git a/src/common/quantile.cu b/src/common/quantile.cu index 2bf6070d5..4b110f5e0 100644 --- a/src/common/quantile.cu +++ b/src/common/quantile.cu @@ -22,9 +22,7 @@ #include "transform_iterator.h" // MakeIndexTransformIter #include "xgboost/span.h" -namespace xgboost { -namespace common { - +namespace xgboost::common { using WQSketch = HostSketchContainer::WQSketch; using SketchEntry = WQSketch::Entry; @@ -501,7 +499,7 @@ void SketchContainer::FixError() { }); } -void SketchContainer::AllReduce(bool is_column_split) { +void SketchContainer::AllReduce(Context const*, bool is_column_split) { dh::safe_cuda(cudaSetDevice(device_.ordinal)); auto world = collective::GetWorldSize(); if (world == 1 || is_column_split) { @@ -582,13 +580,13 @@ struct InvalidCatOp { }; } // anonymous namespace -void SketchContainer::MakeCuts(HistogramCuts* p_cuts, bool is_column_split) { +void SketchContainer::MakeCuts(Context const* ctx, HistogramCuts* p_cuts, bool is_column_split) { timer_.Start(__func__); dh::safe_cuda(cudaSetDevice(device_.ordinal)); p_cuts->min_vals_.Resize(num_columns_); // Sync between workers. - this->AllReduce(is_column_split); + this->AllReduce(ctx, is_column_split); // Prune to final number of bins. this->Prune(num_bins_ + 1); @@ -731,5 +729,4 @@ void SketchContainer::MakeCuts(HistogramCuts* p_cuts, bool is_column_split) { p_cuts->SetCategorical(this->has_categorical_, max_cat); timer_.Stop(__func__); } -} // namespace common -} // namespace xgboost +} // namespace xgboost::common diff --git a/src/common/quantile.cuh b/src/common/quantile.cuh index b47834782..f7124f079 100644 --- a/src/common/quantile.cuh +++ b/src/common/quantile.cuh @@ -151,9 +151,9 @@ class SketchContainer { Span that); /* \brief Merge quantiles from other GPU workers. */ - void AllReduce(bool is_column_split); + void AllReduce(Context const* ctx, bool is_column_split); /* \brief Create the final histogram cut values. */ - void MakeCuts(HistogramCuts* cuts, bool is_column_split); + void MakeCuts(Context const* ctx, HistogramCuts* cuts, bool is_column_split); Span Data() const { return {this->Current().data().get(), this->Current().size()}; diff --git a/src/common/quantile.h b/src/common/quantile.h index 47db5f875..0af93a03e 100644 --- a/src/common/quantile.h +++ b/src/common/quantile.h @@ -827,13 +827,14 @@ class SketchContainerImpl { return group_ind; } // Gather sketches from all workers. - void GatherSketchInfo(MetaInfo const& info, + void GatherSketchInfo(Context const *ctx, MetaInfo const &info, std::vector const &reduced, std::vector *p_worker_segments, std::vector *p_sketches_scan, std::vector *p_global_sketches); // Merge sketches from all workers. - void AllReduce(MetaInfo const& info, std::vector *p_reduced, + void AllReduce(Context const *ctx, MetaInfo const &info, + std::vector *p_reduced, std::vector *p_num_cuts); template @@ -887,11 +888,11 @@ class SketchContainerImpl { /* \brief Push a CSR matrix. */ void PushRowPage(SparsePage const &page, MetaInfo const &info, Span hessian = {}); - void MakeCuts(MetaInfo const& info, HistogramCuts* cuts); + void MakeCuts(Context const *ctx, MetaInfo const &info, HistogramCuts *cuts); private: // Merge all categories from other workers. - void AllreduceCategories(MetaInfo const& info); + void AllreduceCategories(Context const* ctx, MetaInfo const& info); }; class HostSketchContainer : public SketchContainerImpl> { diff --git a/src/data/data.cc b/src/data/data.cc index 7e70fff3f..50f64a406 100644 --- a/src/data/data.cc +++ b/src/data/data.cc @@ -745,7 +745,7 @@ void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_col } } -void MetaInfo::SynchronizeNumberOfColumns() { +void MetaInfo::SynchronizeNumberOfColumns(Context const*) { if (IsColumnSplit()) { collective::Allreduce(&num_col_, 1); } else { diff --git a/src/data/iterative_dmatrix.cc b/src/data/iterative_dmatrix.cc index 45f6286fb..e5aa98278 100644 --- a/src/data/iterative_dmatrix.cc +++ b/src/data/iterative_dmatrix.cc @@ -95,7 +95,7 @@ void GetCutsFromRef(Context const* ctx, std::shared_ptr ref, bst_featur namespace { // Synchronize feature type in case of empty DMatrix -void SyncFeatureType(std::vector* p_h_ft) { +void SyncFeatureType(Context const*, std::vector* p_h_ft) { if (!collective::IsDistributed()) { return; } @@ -193,7 +193,7 @@ void IterativeDMatrix::InitFromCPU(Context const* ctx, BatchParam const& p, // From here on Info() has the correct data shape Info().num_row_ = accumulated_rows; Info().num_nonzero_ = nnz; - Info().SynchronizeNumberOfColumns(); + Info().SynchronizeNumberOfColumns(ctx); CHECK(std::none_of(column_sizes.cbegin(), column_sizes.cend(), [&](auto f) { return f > accumulated_rows; })) << "Something went wrong during iteration."; @@ -213,9 +213,9 @@ void IterativeDMatrix::InitFromCPU(Context const* ctx, BatchParam const& p, while (iter.Next()) { if (!p_sketch) { h_ft = proxy->Info().feature_types.ConstHostVector(); - SyncFeatureType(&h_ft); - p_sketch.reset(new common::HostSketchContainer{ctx, p.max_bin, h_ft, column_sizes, - !proxy->Info().group_ptr_.empty()}); + SyncFeatureType(ctx, &h_ft); + p_sketch = std::make_unique(ctx, p.max_bin, h_ft, column_sizes, + !proxy->Info().group_ptr_.empty()); } HostAdapterDispatch(proxy, [&](auto const& batch) { proxy->Info().num_nonzero_ = batch_nnz[i]; @@ -230,7 +230,7 @@ void IterativeDMatrix::InitFromCPU(Context const* ctx, BatchParam const& p, CHECK_EQ(accumulated_rows, Info().num_row_); CHECK(p_sketch); - p_sketch->MakeCuts(Info(), &cuts); + p_sketch->MakeCuts(ctx, Info(), &cuts); } if (!h_ft.empty()) { CHECK_EQ(h_ft.size(), n_features); diff --git a/src/data/iterative_dmatrix.cu b/src/data/iterative_dmatrix.cu index 2fffd516b..09a3976d7 100644 --- a/src/data/iterative_dmatrix.cu +++ b/src/data/iterative_dmatrix.cu @@ -105,7 +105,7 @@ void IterativeDMatrix::InitFromCUDA(Context const* ctx, BatchParam const& p, sketch_containers.clear(); sketch_containers.shrink_to_fit(); - final_sketch.MakeCuts(&cuts, this->info_.IsColumnSplit()); + final_sketch.MakeCuts(ctx, &cuts, this->info_.IsColumnSplit()); } else { GetCutsFromRef(ctx, ref, Info().num_col_, p, &cuts); } @@ -167,7 +167,7 @@ void IterativeDMatrix::InitFromCUDA(Context const* ctx, BatchParam const& p, iter.Reset(); // Synchronise worker columns - info_.SynchronizeNumberOfColumns(); + info_.SynchronizeNumberOfColumns(ctx); } BatchSet IterativeDMatrix::GetEllpackBatches(Context const* ctx, diff --git a/src/data/simple_dmatrix.cc b/src/data/simple_dmatrix.cc index 3814d74d2..2bf81892f 100644 --- a/src/data/simple_dmatrix.cc +++ b/src/data/simple_dmatrix.cc @@ -283,7 +283,7 @@ SimpleDMatrix::SimpleDMatrix(AdapterT* adapter, float missing, int nthread, // Synchronise worker columns info_.data_split_mode = data_split_mode; ReindexFeatures(&ctx); - info_.SynchronizeNumberOfColumns(); + info_.SynchronizeNumberOfColumns(&ctx); if (adapter->NumRows() == kAdapterUnknownSize) { using IteratorAdapterT = diff --git a/src/data/simple_dmatrix.cu b/src/data/simple_dmatrix.cu index e41d59394..e5b4d18f7 100644 --- a/src/data/simple_dmatrix.cu +++ b/src/data/simple_dmatrix.cu @@ -42,7 +42,7 @@ SimpleDMatrix::SimpleDMatrix(AdapterT* adapter, float missing, std::int32_t nthr info_.num_row_ = adapter->NumRows(); // Synchronise worker columns info_.data_split_mode = data_split_mode; - info_.SynchronizeNumberOfColumns(); + info_.SynchronizeNumberOfColumns(&ctx); this->fmat_ctx_ = ctx; } diff --git a/src/data/sparse_page_dmatrix.cc b/src/data/sparse_page_dmatrix.cc index 042a75c56..f1754c1b5 100644 --- a/src/data/sparse_page_dmatrix.cc +++ b/src/data/sparse_page_dmatrix.cc @@ -97,7 +97,7 @@ SparsePageDMatrix::SparsePageDMatrix(DataIterHandle iter_handle, DMatrixHandle p this->info_.num_col_ = n_features; this->info_.num_nonzero_ = nnz; - info_.SynchronizeNumberOfColumns(); + info_.SynchronizeNumberOfColumns(&ctx); CHECK_NE(info_.num_col_, 0); fmat_ctx_ = ctx; diff --git a/src/learner.cc b/src/learner.cc index 08c59ba60..6b0fd7e4b 100644 --- a/src/learner.cc +++ b/src/learner.cc @@ -209,7 +209,7 @@ struct LearnerModelParamLegacy : public dmlc::Parameter return dmlc::Parameter::UpdateAllowUnknown(kwargs); } // sanity check - void Validate() { + void Validate(Context const*) { if (!collective::IsDistributed()) { return; } @@ -434,7 +434,7 @@ class LearnerConfiguration : public Learner { } // Update the shared model parameter this->ConfigureModelParamWithoutBaseScore(); - mparam_.Validate(); + mparam_.Validate(&ctx_); } CHECK(!std::isnan(mparam_.base_score)); CHECK(!std::isinf(mparam_.base_score)); diff --git a/src/metric/auc.cu b/src/metric/auc.cu index a4838d783..8b8349e1b 100644 --- a/src/metric/auc.cu +++ b/src/metric/auc.cu @@ -199,9 +199,9 @@ void Transpose(common::Span in, common::Span out, size_t m, }); } -double ScaleClasses(common::Span results, common::Span local_area, - common::Span tp, common::Span auc, size_t n_classes) { - dh::XGBDeviceAllocator alloc; +double ScaleClasses(Context const *ctx, common::Span results, + common::Span local_area, common::Span tp, + common::Span auc, size_t n_classes) { if (collective::IsDistributed()) { int32_t device = dh::CurrentDevice(); CHECK_EQ(dh::CudaGetPointerDevice(results.data()), device); @@ -218,8 +218,8 @@ double ScaleClasses(common::Span results, common::Span local_are double tp_sum; double auc_sum; thrust::tie(auc_sum, tp_sum) = - thrust::reduce(thrust::cuda::par(alloc), reduce_in, reduce_in + n_classes, - Pair{0.0, 0.0}, PairPlus{}); + thrust::reduce(ctx->CUDACtx()->CTP(), reduce_in, reduce_in + n_classes, Pair{0.0, 0.0}, + PairPlus{}); if (tp_sum != 0 && !std::isnan(auc_sum)) { auc_sum /= tp_sum; } else { @@ -309,10 +309,10 @@ void SegmentedReduceAUC(common::Span d_unique_idx, * up each class in all kernels. */ template -double GPUMultiClassAUCOVR(MetaInfo const &info, DeviceOrd device, +double GPUMultiClassAUCOVR(Context const *ctx, MetaInfo const &info, common::Span d_class_ptr, size_t n_classes, std::shared_ptr cache, Fn area_fn) { - dh::safe_cuda(cudaSetDevice(device.ordinal)); + dh::safe_cuda(cudaSetDevice(ctx->Ordinal())); /** * Sorted idx */ @@ -320,7 +320,7 @@ double GPUMultiClassAUCOVR(MetaInfo const &info, DeviceOrd device, // Index is sorted within class. auto d_sorted_idx = dh::ToSpan(cache->sorted_idx); - auto labels = info.labels.View(device); + auto labels = info.labels.View(ctx->Device()); auto weights = info.weights_.ConstDeviceSpan(); size_t n_samples = labels.Shape(0); @@ -328,12 +328,11 @@ double GPUMultiClassAUCOVR(MetaInfo const &info, DeviceOrd device, if (n_samples == 0) { dh::TemporaryArray resutls(n_classes * 4, 0.0f); auto d_results = dh::ToSpan(resutls); - dh::LaunchN(n_classes * 4, - [=] XGBOOST_DEVICE(size_t i) { d_results[i] = 0.0f; }); + dh::LaunchN(n_classes * 4, [=] XGBOOST_DEVICE(size_t i) { d_results[i] = 0.0f; }); auto local_area = d_results.subspan(0, n_classes); auto tp = d_results.subspan(2 * n_classes, n_classes); auto auc = d_results.subspan(3 * n_classes, n_classes); - return ScaleClasses(d_results, local_area, tp, auc, n_classes); + return ScaleClasses(ctx, d_results, local_area, tp, auc, n_classes); } /** @@ -437,7 +436,7 @@ double GPUMultiClassAUCOVR(MetaInfo const &info, DeviceOrd device, tp[c] = 1.0f; } }); - return ScaleClasses(d_results, local_area, tp, auc, n_classes); + return ScaleClasses(ctx, d_results, local_area, tp, auc, n_classes); } void MultiClassSortedIdx(Context const *ctx, common::Span predts, @@ -472,8 +471,7 @@ double GPUMultiClassROCAUC(Context const *ctx, common::Span predts, size_t /*class_id*/) { return TrapezoidArea(fp_prev, fp, tp_prev, tp); }; - return GPUMultiClassAUCOVR(info, ctx->Device(), dh::ToSpan(class_ptr), n_classes, cache, - fn); + return GPUMultiClassAUCOVR(ctx, info, dh::ToSpan(class_ptr), n_classes, cache, fn); } namespace { @@ -697,7 +695,7 @@ double GPUMultiClassPRAUC(Context const *ctx, common::Span predts, return detail::CalcDeltaPRAUC(fp_prev, fp, tp_prev, tp, d_totals[class_id].first); }; - return GPUMultiClassAUCOVR(info, ctx->Device(), d_class_ptr, n_classes, cache, fn); + return GPUMultiClassAUCOVR(ctx, info, d_class_ptr, n_classes, cache, fn); } template diff --git a/src/metric/elementwise_metric.cu b/src/metric/elementwise_metric.cu index feabedfab..f245f3e06 100644 --- a/src/metric/elementwise_metric.cu +++ b/src/metric/elementwise_metric.cu @@ -215,7 +215,7 @@ struct EvalError { has_param_ = false; } } - const char *Name() const { + [[nodiscard]] const char *Name() const { static thread_local std::string name; if (has_param_) { std::ostringstream os; @@ -228,7 +228,7 @@ struct EvalError { } } - XGBOOST_DEVICE bst_float EvalRow(bst_float label, bst_float pred) const { + [[nodiscard]] XGBOOST_DEVICE bst_float EvalRow(bst_float label, bst_float pred) const { // assume label is in [0,1] return pred > threshold_ ? 1.0f - label : label; } @@ -370,7 +370,7 @@ struct EvalEWiseBase : public MetricNoCache { return Policy::GetFinal(dat[0], dat[1]); } - const char* Name() const override { return policy_.Name(); } + [[nodiscard]] const char* Name() const override { return policy_.Name(); } private: Policy policy_; diff --git a/src/metric/rank_metric.cc b/src/metric/rank_metric.cc index 41495164c..6762aec32 100644 --- a/src/metric/rank_metric.cc +++ b/src/metric/rank_metric.cc @@ -162,7 +162,7 @@ struct EvalRank : public MetricNoCache, public EvalRankConfig { return collective::GlobalRatio(info, sum_metric, static_cast(ngroups)); } - const char* Name() const override { + [[nodiscard]] const char* Name() const override { return name.c_str(); } @@ -294,7 +294,7 @@ class EvalRankWithCache : public Metric { }; namespace { -double Finalize(MetaInfo const& info, double score, double sw) { +double Finalize(Context const*, MetaInfo const& info, double score, double sw) { std::array dat{score, sw}; collective::GlobalSum(info, &dat); std::tie(score, sw) = std::tuple_cat(dat); @@ -323,7 +323,7 @@ class EvalPrecision : public EvalRankWithCache { if (ctx_->IsCUDA()) { auto pre = cuda_impl::PreScore(ctx_, info, predt, p_cache); - return Finalize(info, pre.Residue(), pre.Weights()); + return Finalize(ctx_, info, pre.Residue(), pre.Weights()); } auto gptr = p_cache->DataGroupPtr(ctx_); @@ -352,7 +352,7 @@ class EvalPrecision : public EvalRankWithCache { } auto sum = std::accumulate(pre.cbegin(), pre.cend(), 0.0); - return Finalize(info, sum, sw); + return Finalize(ctx_, info, sum, sw); } }; @@ -369,7 +369,7 @@ class EvalNDCG : public EvalRankWithCache { std::shared_ptr p_cache) override { if (ctx_->IsCUDA()) { auto ndcg = cuda_impl::NDCGScore(ctx_, info, preds, minus_, p_cache); - return Finalize(info, ndcg.Residue(), ndcg.Weights()); + return Finalize(ctx_, info, ndcg.Residue(), ndcg.Weights()); } // group local ndcg @@ -415,7 +415,7 @@ class EvalNDCG : public EvalRankWithCache { sum_w = std::accumulate(weights.weights.cbegin(), weights.weights.cend(), 0.0); } auto ndcg = std::accumulate(linalg::cbegin(ndcg_gloc), linalg::cend(ndcg_gloc), 0.0); - return Finalize(info, ndcg, sum_w); + return Finalize(ctx_, info, ndcg, sum_w); } }; @@ -427,7 +427,7 @@ class EvalMAPScore : public EvalRankWithCache { std::shared_ptr p_cache) override { if (ctx_->IsCUDA()) { auto map = cuda_impl::MAPScore(ctx_, info, predt, minus_, p_cache); - return Finalize(info, map.Residue(), map.Weights()); + return Finalize(ctx_, info, map.Residue(), map.Weights()); } auto gptr = p_cache->DataGroupPtr(ctx_); @@ -469,7 +469,7 @@ class EvalMAPScore : public EvalRankWithCache { sw += weight[i]; } auto sum = std::accumulate(map_gloc.cbegin(), map_gloc.cend(), 0.0); - return Finalize(info, sum, sw); + return Finalize(ctx_, info, sum, sw); } }; diff --git a/src/metric/survival_metric.cu b/src/metric/survival_metric.cu index 0625af25a..c13702a19 100644 --- a/src/metric/survival_metric.cu +++ b/src/metric/survival_metric.cu @@ -217,7 +217,7 @@ struct EvalEWiseSurvivalBase : public MetricNoCache { return Policy::GetFinal(dat[0], dat[1]); } - const char* Name() const override { + [[nodiscard]] const char* Name() const override { return policy_.Name(); } diff --git a/src/predictor/cpu_predictor.cc b/src/predictor/cpu_predictor.cc index 26d8f3440..20305850a 100644 --- a/src/predictor/cpu_predictor.cc +++ b/src/predictor/cpu_predictor.cc @@ -189,7 +189,7 @@ struct SparsePageView { explicit SparsePageView(SparsePage const *p) : base_rowid{p->base_rowid} { view = p->GetView(); } SparsePage::Inst operator[](size_t i) { return view[i]; } - size_t Size() const { return view.Size(); } + [[nodiscard]] size_t Size() const { return view.Size(); } }; struct SingleInstanceView { @@ -250,7 +250,7 @@ struct GHistIndexMatrixView { } return ret; } - size_t Size() const { return page_.Size(); } + [[nodiscard]] size_t Size() const { return page_.Size(); } }; template @@ -290,7 +290,7 @@ class AdapterView { return ret; } - size_t Size() const { return adapter_->NumRows(); } + [[nodiscard]] size_t Size() const { return adapter_->NumRows(); } bst_row_t const static base_rowid = 0; // NOLINT }; @@ -408,31 +408,33 @@ class ColumnSplitHelper { ColumnSplitHelper(ColumnSplitHelper &&) noexcept = delete; ColumnSplitHelper &operator=(ColumnSplitHelper &&) noexcept = delete; - void PredictDMatrix(DMatrix *p_fmat, std::vector *out_preds) { + void PredictDMatrix(Context const *ctx, DMatrix *p_fmat, std::vector *out_preds) { CHECK(xgboost::collective::IsDistributed()) << "column-split prediction is only supported for distributed training"; for (auto const &batch : p_fmat->GetBatches()) { CHECK_EQ(out_preds->size(), p_fmat->Info().num_row_ * model_.learner_model_param->num_output_group); - PredictBatchKernel(SparsePageView{&batch}, out_preds); + PredictBatchKernel(ctx, SparsePageView{&batch}, out_preds); } } - void PredictInstance(SparsePage::Inst const &inst, std::vector *out_preds) { + void PredictInstance(Context const *ctx, SparsePage::Inst const &inst, + std::vector *out_preds) { CHECK(xgboost::collective::IsDistributed()) << "column-split prediction is only supported for distributed training"; - PredictBatchKernel(SingleInstanceView{inst}, out_preds); + PredictBatchKernel(ctx, SingleInstanceView{inst}, out_preds); } - void PredictLeaf(DMatrix *p_fmat, std::vector *out_preds) { + void PredictLeaf(Context const* ctx, DMatrix *p_fmat, std::vector *out_preds) { CHECK(xgboost::collective::IsDistributed()) << "column-split prediction is only supported for distributed training"; for (auto const &batch : p_fmat->GetBatches()) { CHECK_EQ(out_preds->size(), p_fmat->Info().num_row_ * (tree_end_ - tree_begin_)); - PredictBatchKernel(SparsePageView{&batch}, out_preds); + PredictBatchKernel(ctx, SparsePageView{&batch}, + out_preds); } } @@ -453,12 +455,13 @@ class ColumnSplitHelper { std::fill(missing_storage_.begin(), missing_storage_.end(), 0); } - std::size_t BitIndex(std::size_t tree_id, std::size_t row_id, std::size_t node_id) const { + [[nodiscard]] std::size_t BitIndex(std::size_t tree_id, std::size_t row_id, + std::size_t node_id) const { size_t tree_index = tree_id - tree_begin_; return tree_offsets_[tree_index] * n_rows_ + row_id * tree_sizes_[tree_index] + node_id; } - void AllreduceBitVectors() { + void AllreduceBitVectors(Context const*) { collective::Allreduce(decision_storage_.data(), decision_storage_.size()); collective::Allreduce(missing_storage_.data(), @@ -547,7 +550,7 @@ class ColumnSplitHelper { } template - void PredictBatchKernel(DataView batch, std::vector *out_preds) { + void PredictBatchKernel(Context const* ctx, DataView batch, std::vector *out_preds) { auto const num_group = model_.learner_model_param->num_output_group; // parallel over local batch @@ -568,7 +571,7 @@ class ColumnSplitHelper { FVecDrop(block_size, fvec_offset, &feat_vecs_); }); - AllreduceBitVectors(); + AllreduceBitVectors(ctx); // auto block_id has the same type as `n_blocks`. common::ParallelFor(n_blocks, n_threads_, [&](auto block_id) { @@ -646,7 +649,7 @@ class CPUPredictor : public Predictor { << "Predict DMatrix with column split" << MTNotImplemented(); ColumnSplitHelper helper(this->ctx_->Threads(), model, tree_begin, tree_end); - helper.PredictDMatrix(p_fmat, out_preds); + helper.PredictDMatrix(ctx_, p_fmat, out_preds); return; } @@ -779,7 +782,7 @@ class CPUPredictor : public Predictor { << "Predict instance with column split" << MTNotImplemented(); ColumnSplitHelper helper(this->ctx_->Threads(), model, 0, ntree_limit); - helper.PredictInstance(inst, out_preds); + helper.PredictInstance(ctx_, inst, out_preds); return; } @@ -811,7 +814,7 @@ class CPUPredictor : public Predictor { << "Predict leaf with column split" << MTNotImplemented(); ColumnSplitHelper helper(n_threads, model, 0, ntree_limit); - helper.PredictLeaf(p_fmat, &preds); + helper.PredictLeaf(ctx_, p_fmat, &preds); return; } diff --git a/src/predictor/gpu_predictor.cu b/src/predictor/gpu_predictor.cu index e41248e29..7fad07397 100644 --- a/src/predictor/gpu_predictor.cu +++ b/src/predictor/gpu_predictor.cu @@ -62,9 +62,7 @@ struct TreeView { cats.node_ptr = tree_cat_ptrs; } - __device__ bool HasCategoricalSplit() const { - return !cats.categories.empty(); - } + [[nodiscard]] __device__ bool HasCategoricalSplit() const { return !cats.categories.empty(); } }; struct SparsePageView { @@ -77,7 +75,7 @@ struct SparsePageView { common::Span row_ptr, bst_feature_t num_features) : d_data{data}, d_row_ptr{row_ptr}, num_features(num_features) {} - __device__ float GetElement(size_t ridx, size_t fidx) const { + [[nodiscard]] __device__ float GetElement(size_t ridx, size_t fidx) const { // Binary search auto begin_ptr = d_data.begin() + d_row_ptr[ridx]; auto end_ptr = d_data.begin() + d_row_ptr[ridx + 1]; @@ -105,8 +103,8 @@ struct SparsePageView { // Value is missing return nanf(""); } - XGBOOST_DEVICE size_t NumRows() const { return d_row_ptr.size() - 1; } - XGBOOST_DEVICE size_t NumCols() const { return num_features; } + [[nodiscard]] XGBOOST_DEVICE size_t NumRows() const { return d_row_ptr.size() - 1; } + [[nodiscard]] XGBOOST_DEVICE size_t NumCols() const { return num_features; } }; struct SparsePageLoader { @@ -137,7 +135,7 @@ struct SparsePageLoader { __syncthreads(); } } - __device__ float GetElement(size_t ridx, size_t fidx) const { + [[nodiscard]] __device__ float GetElement(size_t ridx, size_t fidx) const { if (use_shared) { return smem[threadIdx.x * data.num_features + fidx]; } else { @@ -151,7 +149,7 @@ struct EllpackLoader { XGBOOST_DEVICE EllpackLoader(EllpackDeviceAccessor const& m, bool, bst_feature_t, bst_row_t, size_t, float) : matrix{m} {} - __device__ __forceinline__ float GetElement(size_t ridx, size_t fidx) const { + [[nodiscard]] __device__ __forceinline__ float GetElement(size_t ridx, size_t fidx) const { auto gidx = matrix.GetBinIndex(ridx, fidx); if (gidx == -1) { return nan(""); diff --git a/src/tree/gpu_hist/evaluate_splits.cu b/src/tree/gpu_hist/evaluate_splits.cu index 627bf4ca4..ceb322c28 100644 --- a/src/tree/gpu_hist/evaluate_splits.cu +++ b/src/tree/gpu_hist/evaluate_splits.cu @@ -395,11 +395,11 @@ void GPUHistEvaluator::CopyToHost(const std::vector &nidx) { } } -void GPUHistEvaluator::EvaluateSplits( - const std::vector &nidx, bst_feature_t max_active_features, - common::Span d_inputs, - EvaluateSplitSharedInputs shared_inputs, - common::Span out_entries) { +void GPUHistEvaluator::EvaluateSplits(Context const *ctx, const std::vector &nidx, + bst_feature_t max_active_features, + common::Span d_inputs, + EvaluateSplitSharedInputs shared_inputs, + common::Span out_entries) { auto evaluator = this->tree_evaluator_.template GetEvaluator(); dh::TemporaryArray splits_out_storage(d_inputs.size()); @@ -417,19 +417,20 @@ void GPUHistEvaluator::EvaluateSplits( out_splits.size() * sizeof(DeviceSplitCandidate)); // Reduce to get the best candidate from all workers. - dh::LaunchN(out_splits.size(), [world_size, all_candidates, out_splits] __device__(size_t i) { - out_splits[i] = all_candidates[i]; - for (auto rank = 1; rank < world_size; rank++) { - out_splits[i] = out_splits[i] + all_candidates[rank * out_splits.size() + i]; - } - }); + dh::LaunchN(out_splits.size(), ctx->CUDACtx()->Stream(), + [world_size, all_candidates, out_splits] __device__(size_t i) { + out_splits[i] = all_candidates[i]; + for (auto rank = 1; rank < world_size; rank++) { + out_splits[i] = out_splits[i] + all_candidates[rank * out_splits.size() + i]; + } + }); } auto d_sorted_idx = this->SortedIdx(d_inputs.size(), shared_inputs.feature_values.size()); auto d_entries = out_entries; auto device_cats_accessor = this->DeviceCatStorage(nidx); // turn candidate into entry, along with handling sort based split. - dh::LaunchN(d_inputs.size(), [=] __device__(size_t i) mutable { + dh::LaunchN(d_inputs.size(), ctx->CUDACtx()->Stream(), [=] __device__(size_t i) mutable { auto const input = d_inputs[i]; auto &split = out_splits[i]; // Subtract parent gain here @@ -464,12 +465,12 @@ void GPUHistEvaluator::EvaluateSplits( this->CopyToHost(nidx); } -GPUExpandEntry GPUHistEvaluator::EvaluateSingleSplit( - EvaluateSplitInputs input, EvaluateSplitSharedInputs shared_inputs) { +GPUExpandEntry GPUHistEvaluator::EvaluateSingleSplit(Context const *ctx, EvaluateSplitInputs input, + EvaluateSplitSharedInputs shared_inputs) { dh::device_vector inputs = std::vector{input}; dh::TemporaryArray out_entries(1); - this->EvaluateSplits({input.nidx}, input.feature_set.size(), dh::ToSpan(inputs), shared_inputs, - dh::ToSpan(out_entries)); + this->EvaluateSplits(ctx, {input.nidx}, input.feature_set.size(), dh::ToSpan(inputs), + shared_inputs, dh::ToSpan(out_entries)); GPUExpandEntry root_entry; dh::safe_cuda(cudaMemcpyAsync(&root_entry, out_entries.data().get(), sizeof(GPUExpandEntry), cudaMemcpyDeviceToHost)); diff --git a/src/tree/gpu_hist/evaluate_splits.cuh b/src/tree/gpu_hist/evaluate_splits.cuh index 7c61099a1..8c387f632 100644 --- a/src/tree/gpu_hist/evaluate_splits.cuh +++ b/src/tree/gpu_hist/evaluate_splits.cuh @@ -193,7 +193,7 @@ class GPUHistEvaluator { /** * \brief Evaluate splits for left and right nodes. */ - void EvaluateSplits(const std::vector &nidx, + void EvaluateSplits(Context const* ctx, const std::vector &nidx, bst_feature_t max_active_features, common::Span d_inputs, EvaluateSplitSharedInputs shared_inputs, @@ -201,7 +201,7 @@ class GPUHistEvaluator { /** * \brief Evaluate splits for root node. */ - GPUExpandEntry EvaluateSingleSplit(EvaluateSplitInputs input, + GPUExpandEntry EvaluateSingleSplit(Context const *ctx, EvaluateSplitInputs input, EvaluateSplitSharedInputs shared_inputs); }; } // namespace tree diff --git a/src/tree/gpu_hist/histogram.cu b/src/tree/gpu_hist/histogram.cu index 22eb7ab81..c473c9269 100644 --- a/src/tree/gpu_hist/histogram.cu +++ b/src/tree/gpu_hist/histogram.cu @@ -16,8 +16,7 @@ #include "row_partitioner.cuh" #include "xgboost/base.h" -namespace xgboost { -namespace tree { +namespace xgboost::tree { namespace { struct Pair { GradientPair first; @@ -53,7 +52,8 @@ struct Clip : public thrust::unary_function { * * to avoid outliers, as the full reduction is reproducible on GPU with reduction tree. */ -GradientQuantiser::GradientQuantiser(common::Span gpair, MetaInfo const& info) { +GradientQuantiser::GradientQuantiser(Context const*, common::Span gpair, + MetaInfo const& info) { using GradientSumT = GradientPairPrecise; using T = typename GradientSumT::ValueT; dh::XGBCachingDeviceAllocator alloc; @@ -99,7 +99,6 @@ GradientQuantiser::GradientQuantiser(common::Span gpair, Met static_cast(1) / to_floating_point_.GetHess()); } - XGBOOST_DEV_INLINE void AtomicAddGpairShared(xgboost::GradientPairInt64 *dest, xgboost::GradientPairInt64 const &gpair) { @@ -314,6 +313,4 @@ void BuildGradientHistogram(CUDAContext const* ctx, EllpackDeviceAccessor const& dh::safe_cuda(cudaGetLastError()); } - -} // namespace tree -} // namespace xgboost +} // namespace xgboost::tree diff --git a/src/tree/gpu_hist/histogram.cuh b/src/tree/gpu_hist/histogram.cuh index c693e2e62..925c54893 100644 --- a/src/tree/gpu_hist/histogram.cuh +++ b/src/tree/gpu_hist/histogram.cuh @@ -39,18 +39,20 @@ private: GradientPairPrecise to_floating_point_; public: - GradientQuantiser(common::Span gpair, MetaInfo const& info); - XGBOOST_DEVICE GradientPairInt64 ToFixedPoint(GradientPair const& gpair) const { + GradientQuantiser(Context const* ctx, common::Span gpair, MetaInfo const& info); + [[nodiscard]] XGBOOST_DEVICE GradientPairInt64 ToFixedPoint(GradientPair const& gpair) const { auto adjusted = GradientPairInt64(gpair.GetGrad() * to_fixed_point_.GetGrad(), - gpair.GetHess() * to_fixed_point_.GetHess()); + gpair.GetHess() * to_fixed_point_.GetHess()); return adjusted; } - XGBOOST_DEVICE GradientPairInt64 ToFixedPoint(GradientPairPrecise const& gpair) const { + [[nodiscard]] XGBOOST_DEVICE GradientPairInt64 + ToFixedPoint(GradientPairPrecise const& gpair) const { auto adjusted = GradientPairInt64(gpair.GetGrad() * to_fixed_point_.GetGrad(), - gpair.GetHess() * to_fixed_point_.GetHess()); + gpair.GetHess() * to_fixed_point_.GetHess()); return adjusted; } - XGBOOST_DEVICE GradientPairPrecise ToFloatingPoint(const GradientPairInt64&gpair) const { + [[nodiscard]] XGBOOST_DEVICE GradientPairPrecise + ToFloatingPoint(const GradientPairInt64& gpair) const { auto g = gpair.GetQuantisedGrad() * to_floating_point_.GetGrad(); auto h = gpair.GetQuantisedHess() * to_floating_point_.GetHess(); return {g,h}; diff --git a/src/tree/hist/histogram.h b/src/tree/hist/histogram.h index f378c7808..033d2221e 100644 --- a/src/tree/hist/histogram.h +++ b/src/tree/hist/histogram.h @@ -171,7 +171,8 @@ class HistogramBuilder { } } - void SyncHistogram(RegTree const *p_tree, std::vector const &nodes_to_build, + void SyncHistogram(Context const *, RegTree const *p_tree, + std::vector const &nodes_to_build, std::vector const &nodes_to_trick) { auto n_total_bins = buffer_.TotalBins(); common::BlockedSpace2d space( @@ -277,14 +278,14 @@ class MultiHistogramBuilder { } for (bst_target_t t = 0; t < p_tree->NumTargets(); ++t) { - this->target_builders_[t].SyncHistogram(p_tree, nodes, dummy_sub); + this->target_builders_[t].SyncHistogram(ctx_, p_tree, nodes, dummy_sub); } } /** * @brief Build histogram for left and right child of valid candidates */ template - void BuildHistLeftRight(DMatrix *p_fmat, RegTree const *p_tree, + void BuildHistLeftRight(Context const *ctx, DMatrix *p_fmat, RegTree const *p_tree, std::vector const &partitioners, std::vector const &valid_candidates, linalg::MatrixView gpair, BatchParam const ¶m, @@ -318,7 +319,7 @@ class MultiHistogramBuilder { } for (bst_target_t t = 0; t < p_tree->NumTargets(); ++t) { - this->target_builders_[t].SyncHistogram(p_tree, nodes_to_build, nodes_to_sub); + this->target_builders_[t].SyncHistogram(ctx, p_tree, nodes_to_build, nodes_to_sub); } } diff --git a/src/tree/hist/param.cc b/src/tree/hist/param.cc index 602566cd3..bd8d7a85c 100644 --- a/src/tree/hist/param.cc +++ b/src/tree/hist/param.cc @@ -12,7 +12,7 @@ namespace xgboost::tree { DMLC_REGISTER_PARAMETER(HistMakerTrainParam); -void HistMakerTrainParam::CheckTreesSynchronized(RegTree const* local_tree) const { +void HistMakerTrainParam::CheckTreesSynchronized(Context const*, RegTree const* local_tree) const { if (!this->debug_synchronize) { return; } diff --git a/src/tree/hist/param.h b/src/tree/hist/param.h index 8757b65e6..aa9d8cedf 100644 --- a/src/tree/hist/param.h +++ b/src/tree/hist/param.h @@ -15,7 +15,7 @@ struct HistMakerTrainParam : public XGBoostParameter { bool debug_synchronize{false}; std::size_t max_cached_hist_node{DefaultNodes()}; - void CheckTreesSynchronized(RegTree const* local_tree) const; + void CheckTreesSynchronized(Context const* ctx, RegTree const* local_tree) const; // declare parameters DMLC_DECLARE_PARAMETER(HistMakerTrainParam) { diff --git a/src/tree/updater_approx.cc b/src/tree/updater_approx.cc index 17e020ced..3c37556e1 100644 --- a/src/tree/updater_approx.cc +++ b/src/tree/updater_approx.cc @@ -140,7 +140,7 @@ class GloablApproxBuilder { std::vector const &gpair, common::Span hess) { monitor_->Start(__func__); this->histogram_builder_.BuildHistLeftRight( - p_fmat, p_tree, partitioner_, valid_candidates, + ctx_, p_fmat, p_tree, partitioner_, valid_candidates, linalg::MakeTensorView(ctx_, gpair, gpair.size(), 1), BatchSpec(*param_, hess)); monitor_->Stop(__func__); } @@ -300,7 +300,7 @@ class GlobalApproxUpdater : public TreeUpdater { std::size_t t_idx = 0; for (auto p_tree : trees) { this->pimpl_->UpdateTree(m, s_gpair, hess, p_tree, &out_position[t_idx]); - hist_param_.CheckTreesSynchronized(p_tree); + hist_param_.CheckTreesSynchronized(ctx_, p_tree); ++t_idx; } } diff --git a/src/tree/updater_gpu_hist.cu b/src/tree/updater_gpu_hist.cu index 6db201dd5..3c9c61f88 100644 --- a/src/tree/updater_gpu_hist.cu +++ b/src/tree/updater_gpu_hist.cu @@ -246,7 +246,7 @@ struct GPUHistMakerDevice { this->evaluator_.Reset(page->Cuts(), feature_types, dmat->Info().num_col_, param, dmat->Info().IsColumnSplit(), ctx_->Device()); - quantiser = std::make_unique(this->gpair, dmat->Info()); + quantiser = std::make_unique(ctx_, this->gpair, dmat->Info()); row_partitioner.reset(); // Release the device memory first before reallocating row_partitioner = std::make_unique(ctx_->Device(), sample.sample_rows); @@ -276,7 +276,7 @@ struct GPUHistMakerDevice { matrix.min_fvalue, matrix.is_dense && !collective::IsDistributed() }; - auto split = this->evaluator_.EvaluateSingleSplit(inputs, shared_inputs); + auto split = this->evaluator_.EvaluateSingleSplit(ctx_, inputs, shared_inputs); return split; } @@ -329,7 +329,7 @@ struct GPUHistMakerDevice { d_node_inputs.data().get(), h_node_inputs.data(), h_node_inputs.size() * sizeof(EvaluateSplitInputs), cudaMemcpyDefault)); - this->evaluator_.EvaluateSplits(nidx, max_active_features, dh::ToSpan(d_node_inputs), + this->evaluator_.EvaluateSplits(ctx_, nidx, max_active_features, dh::ToSpan(d_node_inputs), shared_inputs, dh::ToSpan(entries)); dh::safe_cuda(cudaMemcpyAsync(pinned_candidates_out.data(), entries.data().get(), sizeof(GPUExpandEntry) * entries.size(), @@ -842,7 +842,7 @@ class GPUHistMaker : public TreeUpdater { std::size_t t_idx{0}; for (xgboost::RegTree* tree : trees) { this->UpdateTree(param, gpair_hdv, dmat, tree, &out_position[t_idx]); - this->hist_maker_param_.CheckTreesSynchronized(tree); + this->hist_maker_param_.CheckTreesSynchronized(ctx_, tree); ++t_idx; } dh::safe_cuda(cudaGetLastError()); @@ -985,7 +985,7 @@ class GPUGlobalApproxMaker : public TreeUpdater { std::size_t t_idx{0}; for (xgboost::RegTree* tree : trees) { this->UpdateTree(gpair->Data(), p_fmat, tree, &out_position[t_idx]); - this->hist_maker_param_.CheckTreesSynchronized(tree); + this->hist_maker_param_.CheckTreesSynchronized(ctx_, tree); ++t_idx; } diff --git a/src/tree/updater_quantile_hist.cc b/src/tree/updater_quantile_hist.cc index 50943e1c4..375b24cfa 100644 --- a/src/tree/updater_quantile_hist.cc +++ b/src/tree/updater_quantile_hist.cc @@ -228,8 +228,8 @@ class MultiTargetHistBuilder { std::vector const &valid_candidates, linalg::MatrixView gpair) { monitor_->Start(__func__); - histogram_builder_->BuildHistLeftRight(p_fmat, p_tree, partitioner_, valid_candidates, gpair, - HistBatch(param_)); + histogram_builder_->BuildHistLeftRight(ctx_, p_fmat, p_tree, partitioner_, valid_candidates, + gpair, HistBatch(param_)); monitor_->Stop(__func__); } @@ -436,8 +436,8 @@ class HistUpdater { std::vector const &valid_candidates, linalg::MatrixView gpair) { monitor_->Start(__func__); - this->histogram_builder_->BuildHistLeftRight(p_fmat, p_tree, partitioner_, valid_candidates, - gpair, HistBatch(param_)); + this->histogram_builder_->BuildHistLeftRight(ctx_, p_fmat, p_tree, partitioner_, + valid_candidates, gpair, HistBatch(param_)); monitor_->Stop(__func__); } @@ -537,7 +537,7 @@ class QuantileHistMaker : public TreeUpdater { h_out_position, *tree_it); } - hist_param_.CheckTreesSynchronized(*tree_it); + hist_param_.CheckTreesSynchronized(ctx_, *tree_it); } } diff --git a/tests/cpp/common/test_hist_util.cu b/tests/cpp/common/test_hist_util.cu index 4782f9580..92d8ff753 100644 --- a/tests/cpp/common/test_hist_util.cu +++ b/tests/cpp/common/test_hist_util.cu @@ -360,25 +360,27 @@ TEST(HistUtil, DeviceSketchExternalMemoryWithWeights) { } template -auto MakeUnweightedCutsForTest(Adapter adapter, int32_t num_bins, float missing, size_t batch_size = 0) { +auto MakeUnweightedCutsForTest(Context const* ctx, Adapter adapter, int32_t num_bins, float missing, + size_t batch_size = 0) { common::HistogramCuts batched_cuts; HostDeviceVector ft; SketchContainer sketch_container(ft, num_bins, adapter.NumColumns(), adapter.NumRows(), DeviceOrd::CUDA(0)); MetaInfo info; AdapterDeviceSketch(adapter.Value(), num_bins, info, missing, &sketch_container, batch_size); - sketch_container.MakeCuts(&batched_cuts, info.IsColumnSplit()); + sketch_container.MakeCuts(ctx, &batched_cuts, info.IsColumnSplit()); return batched_cuts; } template -void ValidateBatchedCuts(Adapter adapter, int num_bins, DMatrix* dmat, size_t batch_size = 0) { +void ValidateBatchedCuts(Context const* ctx, Adapter adapter, int num_bins, DMatrix* dmat, size_t batch_size = 0) { common::HistogramCuts batched_cuts = MakeUnweightedCutsForTest( - adapter, num_bins, std::numeric_limits::quiet_NaN(), batch_size); + ctx, adapter, num_bins, std::numeric_limits::quiet_NaN(), batch_size); ValidateCuts(batched_cuts, dmat, num_bins); } TEST(HistUtil, AdapterDeviceSketch) { + auto ctx = MakeCUDACtx(0); int rows = 5; int cols = 1; int num_bins = 4; @@ -391,8 +393,8 @@ TEST(HistUtil, AdapterDeviceSketch) { data::CupyAdapter adapter(str); - auto device_cuts = MakeUnweightedCutsForTest(adapter, num_bins, missing); - Context ctx; + auto device_cuts = MakeUnweightedCutsForTest(&ctx, adapter, num_bins, missing); + ctx = ctx.MakeCPU(); auto host_cuts = GetHostCuts(&ctx, &adapter, num_bins, missing); EXPECT_EQ(device_cuts.Values(), host_cuts.Values()); @@ -401,6 +403,7 @@ TEST(HistUtil, AdapterDeviceSketch) { } TEST(HistUtil, AdapterDeviceSketchMemory) { + auto ctx = MakeCUDACtx(0); int num_columns = 100; int num_rows = 1000; int num_bins = 256; @@ -410,7 +413,8 @@ TEST(HistUtil, AdapterDeviceSketchMemory) { dh::GlobalMemoryLogger().Clear(); ConsoleLogger::Configure({{"verbosity", "3"}}); - auto cuts = MakeUnweightedCutsForTest(adapter, num_bins, std::numeric_limits::quiet_NaN()); + auto cuts = + MakeUnweightedCutsForTest(&ctx, adapter, num_bins, std::numeric_limits::quiet_NaN()); ConsoleLogger::Configure({{"verbosity", "0"}}); size_t bytes_required = detail::RequiredMemory( num_rows, num_columns, num_rows * num_columns, num_bins, false); @@ -419,6 +423,7 @@ TEST(HistUtil, AdapterDeviceSketchMemory) { } TEST(HistUtil, AdapterSketchSlidingWindowMemory) { + auto ctx = MakeCUDACtx(0); int num_columns = 100; int num_rows = 1000; int num_bins = 256; @@ -435,7 +440,7 @@ TEST(HistUtil, AdapterSketchSlidingWindowMemory) { AdapterDeviceSketch(adapter.Value(), num_bins, info, std::numeric_limits::quiet_NaN(), &sketch_container); HistogramCuts cuts; - sketch_container.MakeCuts(&cuts, info.IsColumnSplit()); + sketch_container.MakeCuts(&ctx, &cuts, info.IsColumnSplit()); size_t bytes_required = detail::RequiredMemory( num_rows, num_columns, num_rows * num_columns, num_bins, false); EXPECT_LE(dh::GlobalMemoryLogger().PeakMemory(), bytes_required * 1.05); @@ -444,6 +449,7 @@ TEST(HistUtil, AdapterSketchSlidingWindowMemory) { } TEST(HistUtil, AdapterSketchSlidingWindowWeightedMemory) { + auto ctx = MakeCUDACtx(0); int num_columns = 100; int num_rows = 1000; int num_bins = 256; @@ -465,7 +471,7 @@ TEST(HistUtil, AdapterSketchSlidingWindowWeightedMemory) { &sketch_container); HistogramCuts cuts; - sketch_container.MakeCuts(&cuts, info.IsColumnSplit()); + sketch_container.MakeCuts(&ctx, &cuts, info.IsColumnSplit()); ConsoleLogger::Configure({{"verbosity", "0"}}); size_t bytes_required = detail::RequiredMemory( num_rows, num_columns, num_rows * num_columns, num_bins, true); @@ -475,6 +481,7 @@ TEST(HistUtil, AdapterSketchSlidingWindowWeightedMemory) { void TestCategoricalSketchAdapter(size_t n, size_t num_categories, int32_t num_bins, bool weighted) { + auto ctx = MakeCUDACtx(0); auto h_x = GenerateRandomCategoricalSingleColumn(n, num_categories); thrust::device_vector x(h_x); auto adapter = AdapterFromData(x, n, 1); @@ -498,7 +505,7 @@ void TestCategoricalSketchAdapter(size_t n, size_t num_categories, AdapterDeviceSketch(adapter.Value(), num_bins, info, std::numeric_limits::quiet_NaN(), &container); HistogramCuts cuts; - container.MakeCuts(&cuts, info.IsColumnSplit()); + container.MakeCuts(&ctx, &cuts, info.IsColumnSplit()); thrust::sort(x.begin(), x.end()); auto n_uniques = thrust::unique(x.begin(), x.end()) - x.begin(); @@ -522,6 +529,7 @@ void TestCategoricalSketchAdapter(size_t n, size_t num_categories, TEST(HistUtil, AdapterDeviceSketchCategorical) { auto categorical_sizes = {2, 6, 8, 12}; int num_bins = 256; + auto ctx = MakeCUDACtx(0); auto sizes = {25, 100, 1000}; for (auto n : sizes) { for (auto num_categories : categorical_sizes) { @@ -529,7 +537,7 @@ TEST(HistUtil, AdapterDeviceSketchCategorical) { auto dmat = GetDMatrixFromData(x, n, 1); auto x_device = thrust::device_vector(x); auto adapter = AdapterFromData(x_device, n, 1); - ValidateBatchedCuts(adapter, num_bins, dmat.get()); + ValidateBatchedCuts(&ctx, adapter, num_bins, dmat.get()); TestCategoricalSketchAdapter(n, num_categories, num_bins, true); TestCategoricalSketchAdapter(n, num_categories, num_bins, false); } @@ -540,13 +548,14 @@ TEST(HistUtil, AdapterDeviceSketchMultipleColumns) { auto bin_sizes = {2, 16, 256, 512}; auto sizes = {100, 1000, 1500}; int num_columns = 5; + auto ctx = MakeCUDACtx(0); for (auto num_rows : sizes) { auto x = GenerateRandom(num_rows, num_columns); auto dmat = GetDMatrixFromData(x, num_rows, num_columns); auto x_device = thrust::device_vector(x); for (auto num_bins : bin_sizes) { auto adapter = AdapterFromData(x_device, num_rows, num_columns); - ValidateBatchedCuts(adapter, num_bins, dmat.get()); + ValidateBatchedCuts(&ctx, adapter, num_bins, dmat.get()); } } } @@ -556,12 +565,13 @@ TEST(HistUtil, AdapterDeviceSketchBatches) { int num_rows = 5000; auto batch_sizes = {0, 100, 1500, 6000}; int num_columns = 5; + auto ctx = MakeCUDACtx(0); for (auto batch_size : batch_sizes) { auto x = GenerateRandom(num_rows, num_columns); auto dmat = GetDMatrixFromData(x, num_rows, num_columns); auto x_device = thrust::device_vector(x); auto adapter = AdapterFromData(x_device, num_rows, num_columns); - ValidateBatchedCuts(adapter, num_bins, dmat.get(), batch_size); + ValidateBatchedCuts(&ctx, adapter, num_bins, dmat.get(), batch_size); } } @@ -647,12 +657,12 @@ TEST(HistUtil, SketchingEquivalent) { auto x_device = thrust::device_vector(x); auto adapter = AdapterFromData(x_device, num_rows, num_columns); common::HistogramCuts adapter_cuts = MakeUnweightedCutsForTest( - adapter, num_bins, std::numeric_limits::quiet_NaN()); + &ctx, adapter, num_bins, std::numeric_limits::quiet_NaN()); EXPECT_EQ(dmat_cuts.Values(), adapter_cuts.Values()); EXPECT_EQ(dmat_cuts.Ptrs(), adapter_cuts.Ptrs()); EXPECT_EQ(dmat_cuts.MinValues(), adapter_cuts.MinValues()); - ValidateBatchedCuts(adapter, num_bins, dmat.get()); + ValidateBatchedCuts(&ctx, adapter, num_bins, dmat.get()); } } } @@ -702,7 +712,7 @@ void TestAdapterSketchFromWeights(bool with_group) { .Device(DeviceOrd::CUDA(0)) .GenerateArrayInterface(&storage); MetaInfo info; - Context ctx; + auto ctx = MakeCUDACtx(0); auto& h_weights = info.weights_.HostVector(); if (with_group) { h_weights.resize(kGroups); @@ -731,7 +741,7 @@ void TestAdapterSketchFromWeights(bool with_group) { &sketch_container); common::HistogramCuts cuts; - sketch_container.MakeCuts(&cuts, info.IsColumnSplit()); + sketch_container.MakeCuts(&ctx, &cuts, info.IsColumnSplit()); auto dmat = GetDMatrixFromData(storage.HostVector(), kRows, kCols); if (with_group) { @@ -744,10 +754,9 @@ void TestAdapterSketchFromWeights(bool with_group) { ASSERT_EQ(cuts.Ptrs().size(), kCols + 1); ValidateCuts(cuts, dmat.get(), kBins); - auto cuda_ctx = MakeCUDACtx(0); if (with_group) { dmat->Info().weights_ = decltype(dmat->Info().weights_)(); // remove weight - HistogramCuts non_weighted = DeviceSketch(&cuda_ctx, dmat.get(), kBins, 0); + HistogramCuts non_weighted = DeviceSketch(&ctx, dmat.get(), kBins, 0); for (size_t i = 0; i < cuts.Values().size(); ++i) { ASSERT_EQ(cuts.Values()[i], non_weighted.Values()[i]); } @@ -773,7 +782,7 @@ void TestAdapterSketchFromWeights(bool with_group) { SketchContainer sketch_container{ft, kBins, kCols, kRows, DeviceOrd::CUDA(0)}; AdapterDeviceSketch(adapter.Value(), kBins, info, std::numeric_limits::quiet_NaN(), &sketch_container); - sketch_container.MakeCuts(&weighted, info.IsColumnSplit()); + sketch_container.MakeCuts(&ctx, &weighted, info.IsColumnSplit()); ValidateCuts(weighted, dmat.get(), kBins); } } diff --git a/tests/cpp/common/test_quantile.cc b/tests/cpp/common/test_quantile.cc index 343f59cda..9fa1566ea 100644 --- a/tests/cpp/common/test_quantile.cc +++ b/tests/cpp/common/test_quantile.cc @@ -86,7 +86,7 @@ void DoTestDistributedQuantile(size_t rows, size_t cols) { } HistogramCuts distributed_cuts; - sketch_distributed.MakeCuts(m->Info(), &distributed_cuts); + sketch_distributed.MakeCuts(&ctx, m->Info(), &distributed_cuts); // Generate cuts for single node environment collective::Finalize(); @@ -117,7 +117,7 @@ void DoTestDistributedQuantile(size_t rows, size_t cols) { } HistogramCuts single_node_cuts; - sketch_on_single_node.MakeCuts(m->Info(), &single_node_cuts); + sketch_on_single_node.MakeCuts(&ctx, m->Info(), &single_node_cuts); auto const& sptrs = single_node_cuts.Ptrs(); auto const& dptrs = distributed_cuts.Ptrs(); @@ -220,7 +220,7 @@ void DoTestColSplitQuantile(size_t rows, size_t cols) { } } - sketch_distributed.MakeCuts(m->Info(), &distributed_cuts); + sketch_distributed.MakeCuts(&ctx, m->Info(), &distributed_cuts); } // Generate cuts for single node environment @@ -243,7 +243,7 @@ void DoTestColSplitQuantile(size_t rows, size_t cols) { } } - sketch_on_single_node.MakeCuts(m->Info(), &single_node_cuts); + sketch_on_single_node.MakeCuts(&ctx, m->Info(), &single_node_cuts); } auto const& sptrs = single_node_cuts.Ptrs(); diff --git a/tests/cpp/common/test_quantile.cu b/tests/cpp/common/test_quantile.cu index 49353439f..26bd05524 100644 --- a/tests/cpp/common/test_quantile.cu +++ b/tests/cpp/common/test_quantile.cu @@ -370,6 +370,7 @@ void TestAllReduceBasic() { constexpr size_t kRows = 1000, kCols = 100; RunWithSeedsAndBins(kRows, [=](int32_t seed, size_t n_bins, MetaInfo const& info) { auto const device = DeviceOrd::CUDA(GPUIDX); + auto ctx = MakeCUDACtx(device.ordinal); // Set up single node version; HostDeviceVector ft({}, device); @@ -413,7 +414,7 @@ void TestAllReduceBasic() { AdapterDeviceSketch(adapter.Value(), n_bins, info, std::numeric_limits::quiet_NaN(), &sketch_distributed); - sketch_distributed.AllReduce(false); + sketch_distributed.AllReduce(&ctx, false); sketch_distributed.Unique(); ASSERT_EQ(sketch_distributed.ColumnsPtr().size(), @@ -517,6 +518,7 @@ void TestSameOnAllWorkers() { MetaInfo const &info) { auto const rank = collective::GetRank(); auto const device = DeviceOrd::CUDA(GPUIDX); + Context ctx = MakeCUDACtx(device.ordinal); HostDeviceVector ft({}, device); SketchContainer sketch_distributed(ft, n_bins, kCols, kRows, device); HostDeviceVector storage({}, device); @@ -528,7 +530,7 @@ void TestSameOnAllWorkers() { AdapterDeviceSketch(adapter.Value(), n_bins, info, std::numeric_limits::quiet_NaN(), &sketch_distributed); - sketch_distributed.AllReduce(false); + sketch_distributed.AllReduce(&ctx, false); sketch_distributed.Unique(); TestQuantileElemRank(device, sketch_distributed.Data(), sketch_distributed.ColumnsPtr(), true); diff --git a/tests/cpp/plugin/helpers.h b/tests/cpp/plugin/helpers.h index 3dd0c3a1f..85f2e014b 100644 --- a/tests/cpp/plugin/helpers.h +++ b/tests/cpp/plugin/helpers.h @@ -73,6 +73,7 @@ void RunWithFederatedCommunicator(int32_t world_size, std::string const& server_ auto run = [&](auto rank) { Json config{JsonObject()}; config["xgboost_communicator"] = String("federated"); + config["federated_secure"] = false; config["federated_server_address"] = String(server_address); config["federated_world_size"] = world_size; config["federated_rank"] = rank; diff --git a/tests/cpp/test_learner.cc b/tests/cpp/test_learner.cc index 692257748..04f1d35b4 100644 --- a/tests/cpp/test_learner.cc +++ b/tests/cpp/test_learner.cc @@ -2,6 +2,7 @@ * Copyright (c) 2017-2023, XGBoost contributors */ #include +#include #include // for Learner #include // for LogCheck_NE, CHECK_NE, LogCheck_EQ #include // for ObjFunction @@ -81,7 +82,9 @@ TEST(Learner, ParameterValidation) { // whitespace learner->SetParam("tree method", "exact"); - EXPECT_THROW(learner->Configure(), dmlc::Error); + EXPECT_THAT([&] { learner->Configure(); }, + ::testing::ThrowsMessage( + ::testing::HasSubstr(R"("tree method" contains whitespace)"))); } TEST(Learner, CheckGroup) { diff --git a/tests/cpp/tree/gpu_hist/test_evaluate_splits.cu b/tests/cpp/tree/gpu_hist/test_evaluate_splits.cu index 7d5f15a1c..862bc6bfc 100644 --- a/tests/cpp/tree/gpu_hist/test_evaluate_splits.cu +++ b/tests/cpp/tree/gpu_hist/test_evaluate_splits.cu @@ -19,14 +19,15 @@ auto ZeroParam() { } } // anonymous namespace -inline GradientQuantiser DummyRoundingFactor() { +inline GradientQuantiser DummyRoundingFactor(Context const* ctx) { thrust::device_vector gpair(1); gpair[0] = {1000.f, 1000.f}; // Tests should not exceed sum of 1000 - return {dh::ToSpan(gpair), MetaInfo()}; + return {ctx, dh::ToSpan(gpair), MetaInfo()}; } -thrust::device_vector ConvertToInteger(std::vector x) { - auto r = DummyRoundingFactor(); +thrust::device_vector ConvertToInteger(Context const* ctx, + std::vector x) { + auto r = DummyRoundingFactor(ctx); std::vector y(x.size()); for (std::size_t i = 0; i < x.size(); i++) { y[i] = r.ToFixedPoint(GradientPair(x[i])); @@ -41,11 +42,12 @@ TEST_F(TestCategoricalSplitWithMissing, GPUHistEvaluator) { cuts_.cut_ptrs_.SetDevice(ctx.Device()); cuts_.cut_values_.SetDevice(ctx.Device()); cuts_.min_vals_.SetDevice(ctx.Device()); - thrust::device_vector feature_histogram{ConvertToInteger(feature_histogram_)}; + thrust::device_vector feature_histogram{ + ConvertToInteger(&ctx, feature_histogram_)}; dh::device_vector feature_types(feature_set.size(), FeatureType::kCategorical); auto d_feature_types = dh::ToSpan(feature_types); - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); EvaluateSplitInputs input{1, 0, quantiser.ToFixedPoint(parent_sum_), dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; EvaluateSplitSharedInputs shared_inputs{param, @@ -60,7 +62,7 @@ TEST_F(TestCategoricalSplitWithMissing, GPUHistEvaluator) { evaluator.Reset(cuts_, dh::ToSpan(feature_types), feature_set.size(), param_, false, ctx.Device()); - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; ASSERT_EQ(result.thresh, 1); this->CheckResult(result.loss_chg, result.findex, result.fvalue, result.is_cat, @@ -90,7 +92,7 @@ TEST(GpuHist, PartitionBasic) { *std::max_element(cuts.cut_values_.HostVector().begin(), cuts.cut_values_.HostVector().end()); cuts.SetCategorical(true, max_cat); d_feature_types = dh::ToSpan(feature_types); - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); EvaluateSplitSharedInputs shared_inputs{ param, quantiser, @@ -108,10 +110,10 @@ TEST(GpuHist, PartitionBasic) { // -1.0s go right // -3.0s go left auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-5.0, 3.0}); - auto feature_histogram = ConvertToInteger({{-1.0, 1.0}, {-1.0, 1.0}, {-3.0, 1.0}}); + auto feature_histogram = ConvertToInteger(&ctx, {{-1.0, 1.0}, {-1.0, 1.0}, {-3.0, 1.0}}); EvaluateSplitInputs input{0, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; auto cats = std::bitset<32>(evaluator.GetHostNodeCats(input.nidx)[0]); EXPECT_EQ(result.dir, kLeftDir); EXPECT_EQ(cats, std::bitset<32>("11000000000000000000000000000000")); @@ -122,10 +124,10 @@ TEST(GpuHist, PartitionBasic) { // -1.0s go right // -3.0s go left auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-7.0, 3.0}); - auto feature_histogram = ConvertToInteger({{-1.0, 1.0}, {-3.0, 1.0}, {-3.0, 1.0}}); + auto feature_histogram = ConvertToInteger(&ctx, {{-1.0, 1.0}, {-3.0, 1.0}, {-3.0, 1.0}}); EvaluateSplitInputs input{1, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; auto cats = std::bitset<32>(evaluator.GetHostNodeCats(input.nidx)[0]); EXPECT_EQ(result.dir, kLeftDir); EXPECT_EQ(cats, std::bitset<32>("10000000000000000000000000000000")); @@ -134,10 +136,10 @@ TEST(GpuHist, PartitionBasic) { { // All -1.0, gain from splitting should be 0.0 auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-3.0, 3.0}); - auto feature_histogram = ConvertToInteger({{-1.0, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}}); + auto feature_histogram = ConvertToInteger(&ctx, {{-1.0, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}}); EvaluateSplitInputs input{2, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; EXPECT_EQ(result.dir, kLeftDir); EXPECT_FLOAT_EQ(result.loss_chg, 0.0f); EXPECT_EQ(result.left_sum + result.right_sum, parent_sum); @@ -147,10 +149,10 @@ TEST(GpuHist, PartitionBasic) { // value { auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{0.0, 6.0}); - auto feature_histogram = ConvertToInteger({{-1.0, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}}); + auto feature_histogram = ConvertToInteger(&ctx, {{-1.0, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}}); EvaluateSplitInputs input{3, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; auto cats = std::bitset<32>(evaluator.GetHostNodeCats(input.nidx)[0]); EXPECT_EQ(cats, std::bitset<32>("11000000000000000000000000000000")); EXPECT_EQ(result.dir, kLeftDir); @@ -160,10 +162,10 @@ TEST(GpuHist, PartitionBasic) { // -1.0s go right // -3.0s go left auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-5.0, 3.0}); - auto feature_histogram = ConvertToInteger({{-1.0, 1.0}, {-3.0, 1.0}, {-1.0, 1.0}}); + auto feature_histogram = ConvertToInteger(&ctx, {{-1.0, 1.0}, {-3.0, 1.0}, {-1.0, 1.0}}); EvaluateSplitInputs input{4, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; auto cats = std::bitset<32>(evaluator.GetHostNodeCats(input.nidx)[0]); EXPECT_EQ(result.dir, kLeftDir); EXPECT_EQ(cats, std::bitset<32>("10100000000000000000000000000000")); @@ -173,10 +175,10 @@ TEST(GpuHist, PartitionBasic) { // -1.0s go right // -3.0s go left auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-5.0, 3.0}); - auto feature_histogram = ConvertToInteger({{-3.0, 1.0}, {-1.0, 1.0}, {-3.0, 1.0}}); + auto feature_histogram = ConvertToInteger(&ctx, {{-3.0, 1.0}, {-1.0, 1.0}, {-3.0, 1.0}}); EvaluateSplitInputs input{5, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; auto cats = std::bitset<32>(evaluator.GetHostNodeCats(input.nidx)[0]); EXPECT_EQ(cats, std::bitset<32>("01000000000000000000000000000000")); EXPECT_EQ(result.left_sum + result.right_sum, parent_sum); @@ -205,7 +207,7 @@ TEST(GpuHist, PartitionTwoFeatures) { *std::max_element(cuts.cut_values_.HostVector().begin(), cuts.cut_values_.HostVector().end()); cuts.SetCategorical(true, max_cat); - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); EvaluateSplitSharedInputs shared_inputs{param, quantiser, d_feature_types, @@ -220,10 +222,10 @@ TEST(GpuHist, PartitionTwoFeatures) { { auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-6.0, 3.0}); auto feature_histogram = ConvertToInteger( - {{-2.0, 1.0}, {-2.0, 1.0}, {-2.0, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}, {-4.0, 1.0}}); + &ctx, {{-2.0, 1.0}, {-2.0, 1.0}, {-2.0, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}, {-4.0, 1.0}}); EvaluateSplitInputs input{0, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; auto cats = std::bitset<32>(evaluator.GetHostNodeCats(input.nidx)[0]); EXPECT_EQ(result.findex, 1); EXPECT_EQ(cats, std::bitset<32>("11000000000000000000000000000000")); @@ -233,10 +235,10 @@ TEST(GpuHist, PartitionTwoFeatures) { { auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-6.0, 3.0}); auto feature_histogram = ConvertToInteger( - {{-2.0, 1.0}, {-2.0, 1.0}, {-2.0, 1.0}, {-1.0, 1.0}, {-2.5, 1.0}, {-2.5, 1.0}}); + &ctx, {{-2.0, 1.0}, {-2.0, 1.0}, {-2.0, 1.0}, {-1.0, 1.0}, {-2.5, 1.0}, {-2.5, 1.0}}); EvaluateSplitInputs input{1, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; auto cats = std::bitset<32>(evaluator.GetHostNodeCats(input.nidx)[0]); EXPECT_EQ(result.findex, 1); EXPECT_EQ(cats, std::bitset<32>("10000000000000000000000000000000")); @@ -266,7 +268,7 @@ TEST(GpuHist, PartitionTwoNodes) { *std::max_element(cuts.cut_values_.HostVector().begin(), cuts.cut_values_.HostVector().end()); cuts.SetCategorical(true, max_cat); - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); EvaluateSplitSharedInputs shared_inputs{param, quantiser, d_feature_types, @@ -283,15 +285,16 @@ TEST(GpuHist, PartitionTwoNodes) { { auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{-6.0, 3.0}); auto feature_histogram_a = ConvertToInteger( - {{-1.0, 1.0}, {-2.5, 1.0}, {-2.5, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}, {-4.0, 1.0}}); + &ctx, {{-1.0, 1.0}, {-2.5, 1.0}, {-2.5, 1.0}, {-1.0, 1.0}, {-1.0, 1.0}, {-4.0, 1.0}}); thrust::device_vector inputs(2); inputs[0] = EvaluateSplitInputs{0, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram_a)}; - auto feature_histogram_b = ConvertToInteger({{-1.0, 1.0}, {-1.0, 1.0}, {-4.0, 1.0}}); + auto feature_histogram_b = ConvertToInteger(&ctx, {{-1.0, 1.0}, {-1.0, 1.0}, {-4.0, 1.0}}); inputs[1] = EvaluateSplitInputs{1, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram_b)}; thrust::device_vector results(2); - evaluator.EvaluateSplits({0, 1}, 1, dh::ToSpan(inputs), shared_inputs, dh::ToSpan(results)); + evaluator.EvaluateSplits(&ctx, {0, 1}, 1, dh::ToSpan(inputs), shared_inputs, + dh::ToSpan(results)); EXPECT_EQ(std::bitset<32>(evaluator.GetHostNodeCats(0)[0]), std::bitset<32>("10000000000000000000000000000000")); EXPECT_EQ(std::bitset<32>(evaluator.GetHostNodeCats(1)[0]), @@ -301,7 +304,7 @@ TEST(GpuHist, PartitionTwoNodes) { void TestEvaluateSingleSplit(bool is_categorical) { auto ctx = MakeCUDACtx(0); - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{0.0, 1.0}); TrainParam tparam = ZeroParam(); GPUTrainingParam param{tparam}; @@ -311,7 +314,8 @@ void TestEvaluateSingleSplit(bool is_categorical) { thrust::device_vector feature_set = std::vector{0, 1}; // Setup gradients so that second feature gets higher gain - auto feature_histogram = ConvertToInteger({{-0.5, 0.5}, {0.5, 0.5}, {-1.0, 0.5}, {1.0, 0.5}}); + auto feature_histogram = + ConvertToInteger(&ctx, {{-0.5, 0.5}, {0.5, 0.5}, {-1.0, 0.5}, {1.0, 0.5}}); dh::device_vector feature_types(feature_set.size(), FeatureType::kCategorical); common::Span d_feature_types; @@ -336,7 +340,7 @@ void TestEvaluateSingleSplit(bool is_categorical) { ctx.Device()}; evaluator.Reset(cuts, dh::ToSpan(feature_types), feature_set.size(), tparam, false, ctx.Device()); - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; EXPECT_EQ(result.findex, 1); if (is_categorical) { @@ -352,7 +356,8 @@ TEST(GpuHist, EvaluateSingleSplit) { TestEvaluateSingleSplit(false); } TEST(GpuHist, EvaluateSingleCategoricalSplit) { TestEvaluateSingleSplit(true); } TEST(GpuHist, EvaluateSingleSplitMissing) { - auto quantiser = DummyRoundingFactor(); + auto ctx = MakeCUDACtx(0); + auto quantiser = DummyRoundingFactor(&ctx); auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{1.0, 1.5}); TrainParam tparam = ZeroParam(); GPUTrainingParam param{tparam}; @@ -361,7 +366,7 @@ TEST(GpuHist, EvaluateSingleSplitMissing) { thrust::device_vector feature_segments = std::vector{0, 2}; thrust::device_vector feature_values = std::vector{1.0, 2.0}; thrust::device_vector feature_min_values = std::vector{0.0}; - auto feature_histogram = ConvertToInteger({{-0.5, 0.5}, {0.5, 0.5}}); + auto feature_histogram = ConvertToInteger(&ctx, {{-0.5, 0.5}, {0.5, 0.5}}); EvaluateSplitInputs input{1, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; EvaluateSplitSharedInputs shared_inputs{param, @@ -373,7 +378,7 @@ TEST(GpuHist, EvaluateSingleSplitMissing) { false}; GPUHistEvaluator evaluator(tparam, feature_set.size(), FstCU()); - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; EXPECT_EQ(result.findex, 0); EXPECT_EQ(result.fvalue, 1.0); @@ -383,14 +388,15 @@ TEST(GpuHist, EvaluateSingleSplitMissing) { } TEST(GpuHist, EvaluateSingleSplitEmpty) { + auto ctx = MakeCUDACtx(0); TrainParam tparam = ZeroParam(); GPUHistEvaluator evaluator(tparam, 1, FstCU()); DeviceSplitCandidate result = evaluator .EvaluateSingleSplit( - EvaluateSplitInputs{}, + &ctx, EvaluateSplitInputs{}, EvaluateSplitSharedInputs{ - GPUTrainingParam(tparam), DummyRoundingFactor(), {}, {}, {}, {}, false}) + GPUTrainingParam(tparam), DummyRoundingFactor(&ctx), {}, {}, {}, {}, false}) .split; EXPECT_EQ(result.findex, -1); EXPECT_LT(result.loss_chg, 0.0f); @@ -398,7 +404,8 @@ TEST(GpuHist, EvaluateSingleSplitEmpty) { // Feature 0 has a better split, but the algorithm must select feature 1 TEST(GpuHist, EvaluateSingleSplitFeatureSampling) { - auto quantiser = DummyRoundingFactor(); + auto ctx = MakeCUDACtx(0); + auto quantiser = DummyRoundingFactor(&ctx); auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{0.0, 1.0}); TrainParam tparam = ZeroParam(); tparam.UpdateAllowUnknown(Args{}); @@ -408,7 +415,8 @@ TEST(GpuHist, EvaluateSingleSplitFeatureSampling) { thrust::device_vector feature_segments = std::vector{0, 2, 4}; thrust::device_vector feature_values = std::vector{1.0, 2.0, 11.0, 12.0}; thrust::device_vector feature_min_values = std::vector{0.0, 10.0}; - auto feature_histogram = ConvertToInteger({{-10.0, 0.5}, {10.0, 0.5}, {-0.5, 0.5}, {0.5, 0.5}}); + auto feature_histogram = + ConvertToInteger(&ctx, {{-10.0, 0.5}, {10.0, 0.5}, {-0.5, 0.5}, {0.5, 0.5}}); EvaluateSplitInputs input{1, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; EvaluateSplitSharedInputs shared_inputs{param, @@ -420,7 +428,7 @@ TEST(GpuHist, EvaluateSingleSplitFeatureSampling) { false}; GPUHistEvaluator evaluator(tparam, feature_min_values.size(), FstCU()); - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; EXPECT_EQ(result.findex, 1); EXPECT_EQ(result.fvalue, 11.0); @@ -430,7 +438,8 @@ TEST(GpuHist, EvaluateSingleSplitFeatureSampling) { // Features 0 and 1 have identical gain, the algorithm must select 0 TEST(GpuHist, EvaluateSingleSplitBreakTies) { - auto quantiser = DummyRoundingFactor(); + auto ctx = MakeCUDACtx(0); + auto quantiser = DummyRoundingFactor(&ctx); auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{0.0, 1.0}); TrainParam tparam = ZeroParam(); tparam.UpdateAllowUnknown(Args{}); @@ -440,7 +449,8 @@ TEST(GpuHist, EvaluateSingleSplitBreakTies) { thrust::device_vector feature_segments = std::vector{0, 2, 4}; thrust::device_vector feature_values = std::vector{1.0, 2.0, 11.0, 12.0}; thrust::device_vector feature_min_values = std::vector{0.0, 10.0}; - auto feature_histogram = ConvertToInteger({{-0.5, 0.5}, {0.5, 0.5}, {-0.5, 0.5}, {0.5, 0.5}}); + auto feature_histogram = + ConvertToInteger(&ctx, {{-0.5, 0.5}, {0.5, 0.5}, {-0.5, 0.5}, {0.5, 0.5}}); EvaluateSplitInputs input{1, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram)}; EvaluateSplitSharedInputs shared_inputs{param, @@ -452,15 +462,16 @@ TEST(GpuHist, EvaluateSingleSplitBreakTies) { false}; GPUHistEvaluator evaluator(tparam, feature_min_values.size(), FstCU()); - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; EXPECT_EQ(result.findex, 0); EXPECT_EQ(result.fvalue, 1.0); } TEST(GpuHist, EvaluateSplits) { + auto ctx = MakeCUDACtx(0); thrust::device_vector out_splits(2); - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{0.0, 1.0}); TrainParam tparam = ZeroParam(); tparam.UpdateAllowUnknown(Args{}); @@ -471,9 +482,9 @@ TEST(GpuHist, EvaluateSplits) { thrust::device_vector feature_values = std::vector{1.0, 2.0, 11.0, 12.0}; thrust::device_vector feature_min_values = std::vector{0.0, 0.0}; auto feature_histogram_left = - ConvertToInteger({{-0.5, 0.5}, {0.5, 0.5}, {-1.0, 0.5}, {1.0, 0.5}}); + ConvertToInteger(&ctx, {{-0.5, 0.5}, {0.5, 0.5}, {-1.0, 0.5}, {1.0, 0.5}}); auto feature_histogram_right = - ConvertToInteger({{-1.0, 0.5}, {1.0, 0.5}, {-0.5, 0.5}, {0.5, 0.5}}); + ConvertToInteger(&ctx, {{-1.0, 0.5}, {1.0, 0.5}, {-0.5, 0.5}, {0.5, 0.5}}); EvaluateSplitInputs input_left{1, 0, parent_sum, dh::ToSpan(feature_set), dh::ToSpan(feature_histogram_left)}; EvaluateSplitInputs input_right{2, 0, parent_sum, dh::ToSpan(feature_set), @@ -514,7 +525,7 @@ TEST_F(TestPartitionBasedSplit, GpuHist) { evaluator.Reset(cuts_, dh::ToSpan(ft), info_.num_col_, param_, false, ctx.Device()); // Convert the sample histogram to fixed point - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); thrust::host_vector h_hist; for (auto e : hist_[0]) { h_hist.push_back(quantiser.ToFixedPoint(e)); @@ -531,7 +542,7 @@ TEST_F(TestPartitionBasedSplit, GpuHist) { cuts_.cut_values_.ConstDeviceSpan(), cuts_.min_vals_.ConstDeviceSpan(), false}; - auto split = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + auto split = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; ASSERT_NEAR(split.loss_chg, best_score_, 1e-2); } @@ -541,7 +552,7 @@ namespace { void VerifyColumnSplitEvaluateSingleSplit(bool is_categorical) { auto ctx = MakeCUDACtx(GPUIDX); auto rank = collective::GetRank(); - auto quantiser = DummyRoundingFactor(); + auto quantiser = DummyRoundingFactor(&ctx); auto parent_sum = quantiser.ToFixedPoint(GradientPairPrecise{0.0, 1.0}); TrainParam tparam = ZeroParam(); GPUTrainingParam param{tparam}; @@ -552,8 +563,8 @@ void VerifyColumnSplitEvaluateSingleSplit(bool is_categorical) { thrust::device_vector feature_set = std::vector{0, 1}; // Setup gradients so that second feature gets higher gain - auto feature_histogram = rank == 0 ? ConvertToInteger({{-0.5, 0.5}, {0.5, 0.5}}) - : ConvertToInteger({{-1.0, 0.5}, {1.0, 0.5}}); + auto feature_histogram = rank == 0 ? ConvertToInteger(&ctx, {{-0.5, 0.5}, {0.5, 0.5}}) + : ConvertToInteger(&ctx, {{-1.0, 0.5}, {1.0, 0.5}}); dh::device_vector feature_types(feature_set.size(), FeatureType::kCategorical); common::Span d_feature_types; @@ -576,7 +587,7 @@ void VerifyColumnSplitEvaluateSingleSplit(bool is_categorical) { GPUHistEvaluator evaluator{tparam, static_cast(feature_set.size()), ctx.Device()}; evaluator.Reset(cuts, dh::ToSpan(feature_types), feature_set.size(), tparam, true, ctx.Device()); - DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(input, shared_inputs).split; + DeviceSplitCandidate result = evaluator.EvaluateSingleSplit(&ctx, input, shared_inputs).split; EXPECT_EQ(result.findex, 1) << "rank: " << rank; if (is_categorical) { diff --git a/tests/cpp/tree/gpu_hist/test_histogram.cu b/tests/cpp/tree/gpu_hist/test_histogram.cu index 0c91cf21e..f7f2e27ea 100644 --- a/tests/cpp/tree/gpu_hist/test_histogram.cu +++ b/tests/cpp/tree/gpu_hist/test_histogram.cu @@ -37,7 +37,7 @@ void TestDeterministicHistogram(bool is_dense, int shm_size) { FeatureGroups feature_groups(page->Cuts(), page->is_dense, shm_size, sizeof(GradientPairInt64)); - auto quantiser = GradientQuantiser(gpair.DeviceSpan(), MetaInfo()); + auto quantiser = GradientQuantiser(&ctx, gpair.DeviceSpan(), MetaInfo()); BuildGradientHistogram(ctx.CUDACtx(), page->GetDeviceAccessor(FstCU()), feature_groups.DeviceAccessor(FstCU()), gpair.DeviceSpan(), ridx, d_histogram, quantiser); @@ -51,7 +51,7 @@ void TestDeterministicHistogram(bool is_dense, int shm_size) { dh::device_vector new_histogram(num_bins); auto d_new_histogram = dh::ToSpan(new_histogram); - auto quantiser = GradientQuantiser(gpair.DeviceSpan(), MetaInfo()); + auto quantiser = GradientQuantiser(&ctx, gpair.DeviceSpan(), MetaInfo()); BuildGradientHistogram(ctx.CUDACtx(), page->GetDeviceAccessor(FstCU()), feature_groups.DeviceAccessor(FstCU()), gpair.DeviceSpan(), ridx, d_new_histogram, quantiser); @@ -129,7 +129,7 @@ void TestGPUHistogramCategorical(size_t num_categories) { dh::device_vector cat_hist(num_categories); auto gpair = GenerateRandomGradients(kRows, 0, 2); gpair.SetDevice(DeviceOrd::CUDA(0)); - auto quantiser = GradientQuantiser(gpair.DeviceSpan(), MetaInfo()); + auto quantiser = GradientQuantiser(&ctx, gpair.DeviceSpan(), MetaInfo()); /** * Generate hist with cat data. */ diff --git a/tests/cpp/tree/hist/test_histogram.cc b/tests/cpp/tree/hist/test_histogram.cc index 8949b5f4b..25a800367 100644 --- a/tests/cpp/tree/hist/test_histogram.cc +++ b/tests/cpp/tree/hist/test_histogram.cc @@ -181,7 +181,7 @@ void TestSyncHist(bool is_distributed) { histogram.Buffer().Reset(1, n_nodes, space, target_hists); // sync hist - histogram.SyncHistogram(&tree, nodes_for_explicit_hist_build, nodes_for_subtraction_trick); + histogram.SyncHistogram(&ctx, &tree, nodes_for_explicit_hist_build, nodes_for_subtraction_trick); using GHistRowT = common::GHistRow; auto check_hist = [](const GHistRowT parent, const GHistRowT left, const GHistRowT right, @@ -266,7 +266,7 @@ void TestBuildHistogram(bool is_distributed, bool force_read_by_column, bool is_ histogram.BuildHist(0, space, gidx, row_set_collection, nodes_to_build, linalg::MakeTensorView(&ctx, gpair, gpair.size()), force_read_by_column); } - histogram.SyncHistogram(&tree, nodes_to_build, {}); + histogram.SyncHistogram(&ctx, &tree, nodes_to_build, {}); // Check if number of histogram bins is correct ASSERT_EQ(histogram.Histogram()[nid].size(), gmat.cut.Ptrs().back()); @@ -366,7 +366,7 @@ void TestHistogramCategorical(size_t n_categories, bool force_read_by_column) { linalg::MakeTensorView(&ctx, gpair.ConstHostSpan(), gpair.Size()), force_read_by_column); } - cat_hist.SyncHistogram(&tree, nodes_to_build, {}); + cat_hist.SyncHistogram(&ctx, &tree, nodes_to_build, {}); /** * Generate hist with one hot encoded data. @@ -382,7 +382,7 @@ void TestHistogramCategorical(size_t n_categories, bool force_read_by_column) { linalg::MakeTensorView(&ctx, gpair.ConstHostSpan(), gpair.Size()), force_read_by_column); } - onehot_hist.SyncHistogram(&tree, nodes_to_build, {}); + onehot_hist.SyncHistogram(&ctx, &tree, nodes_to_build, {}); auto cat = cat_hist.Histogram()[0]; auto onehot = onehot_hist.Histogram()[0]; @@ -451,7 +451,7 @@ void TestHistogramExternalMemory(Context const *ctx, BatchParam batch_param, boo force_read_by_column); ++page_idx; } - multi_build.SyncHistogram(&tree, nodes, {}); + multi_build.SyncHistogram(ctx, &tree, nodes, {}); multi_page = multi_build.Histogram()[RegTree::kRoot]; } @@ -480,7 +480,7 @@ void TestHistogramExternalMemory(Context const *ctx, BatchParam batch_param, boo single_build.BuildHist(0, space, gmat, row_set_collection, nodes, linalg::MakeTensorView(ctx, h_gpair, h_gpair.size()), force_read_by_column); - single_build.SyncHistogram(&tree, nodes, {}); + single_build.SyncHistogram(ctx, &tree, nodes, {}); single_page = single_build.Histogram()[RegTree::kRoot]; } @@ -570,7 +570,7 @@ class OverflowTest : public ::testing::TestWithParam> { CHECK_NE(partitioners.front()[tree.RightChild(best.nid)].Size(), 0); hist_builder.BuildHistLeftRight( - Xy.get(), &tree, partitioners, valid_candidates, + &ctx, Xy.get(), &tree, partitioners, valid_candidates, linalg::MakeTensorView(&ctx, gpair.ConstHostSpan(), gpair.Size(), 1), batch); if (limit) { diff --git a/tests/cpp/tree/test_gpu_hist.cu b/tests/cpp/tree/test_gpu_hist.cu index accfbae08..6f937351e 100644 --- a/tests/cpp/tree/test_gpu_hist.cu +++ b/tests/cpp/tree/test_gpu_hist.cu @@ -111,7 +111,7 @@ void TestBuildHist(bool use_shared_memory_histograms) { maker.hist.AllocateHistograms({0}); maker.gpair = gpair.DeviceSpan(); - maker.quantiser = std::make_unique(maker.gpair, MetaInfo()); + maker.quantiser = std::make_unique(&ctx, maker.gpair, MetaInfo()); maker.page = page.get(); maker.InitFeatureGroupsOnce(); @@ -162,12 +162,6 @@ HistogramCutsWrapper GetHostCutMatrix () { return cmat; } -inline GradientQuantiser DummyRoundingFactor() { - thrust::device_vector gpair(1); - gpair[0] = {1000.f, 1000.f}; // Tests should not exceed sum of 1000 - return {dh::ToSpan(gpair), MetaInfo()}; -} - void TestHistogramIndexImpl() { // Test if the compressed histogram index matches when using a sparse // dmatrix with and without using external memory From 44099f585dcb3f54567aad03c5d136614d553dc3 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 8 Nov 2023 18:17:14 +0800 Subject: [PATCH 007/109] [coll] Add C API for the tracker. (#9773) --- include/xgboost/c_api.h | 77 +++++++++++++++ src/c_api/coll_c_api.cc | 119 ++++++++++++++++++++++++ src/collective/tracker.h | 3 + src/common/error_msg.h | 2 + tests/cpp/collective/test_coll_c_api.cc | 63 +++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 src/c_api/coll_c_api.cc create mode 100644 tests/cpp/collective/test_coll_c_api.cc diff --git a/include/xgboost/c_api.h b/include/xgboost/c_api.h index d28b5098b..ffa3a6c79 100644 --- a/include/xgboost/c_api.h +++ b/include/xgboost/c_api.h @@ -1508,6 +1508,83 @@ XGB_DLL int XGBoosterFeatureScore(BoosterHandle handle, const char *config, * @{ */ +/** + * @brief Handle to tracker. + * + * There are currently two types of tracker in XGBoost, first one is `rabit`, while the + * other one is `federated`. + * + * This is still under development. + */ +typedef void *TrackerHandle; /* NOLINT */ + +/** + * @brief Create a new tracker. + * + * @param config JSON encoded parameters. + * + * - dmlc_communicator: String, the type of tracker to create. Available options are `rabit` + * and `federated`. + * - n_workers: Integer, the number of workers. + * - port: (Optional) Integer, the port this tracker should listen to. + * - timeout: (Optional) Integer, timeout in seconds for various networking operations. + * + * Some configurations are `rabit` specific: + * - host: (Optional) String, Used by the the `rabit` tracker to specify the address of the host. + * + * Some `federated` specific configurations: + * - federated_secure: Boolean, whether this is a secure server. + * - server_key_path: Path to the server key. Used only if this is a secure server. + * - server_cert_path: Path to the server certificate. Used only if this is a secure server. + * - client_cert_path: Path to the client certificate. Used only if this is a secure server. + * + * @param handle The handle to the created tracker. + * + * @return 0 for success, -1 for failure. + */ +XGB_DLL int XGTrackerCreate(char const *config, TrackerHandle *handle); + +/** + * @brief Get the arguments needed for running workers. This should be called after + * XGTrackerRun() and XGTrackerWait() + * + * @param handle The handle to the tracker. + * @param args The arguments returned as a JSON document. + * + * @return 0 for success, -1 for failure. + */ +XGB_DLL int XGTrackerWorkerArgs(TrackerHandle handle, char const **args); + +/** + * @brief Run the tracker. + * + * @param handle The handle to the tracker. + * + * @return 0 for success, -1 for failure. + */ +XGB_DLL int XGTrackerRun(TrackerHandle handle); + +/** + * @brief Wait for the tracker to finish, should be called after XGTrackerRun(). + * + * @param handle The handle to the tracker. + * @param config JSON encoded configuration. No argument is required yet, preserved for + * the future. + * + * @return 0 for success, -1 for failure. + */ +XGB_DLL int XGTrackerWait(TrackerHandle handle, char const *config); + +/** + * @brief Free a tracker instance. XGTrackerWait() is called internally. If the tracker + * cannot close properly, manual interruption is required. + * + * @param handle The handle to the tracker. + * + * @return 0 for success, -1 for failure. + */ +XGB_DLL int XGTrackerFree(TrackerHandle handle); + /*! * \brief Initialize the collective communicator. * diff --git a/src/c_api/coll_c_api.cc b/src/c_api/coll_c_api.cc new file mode 100644 index 000000000..01713dbad --- /dev/null +++ b/src/c_api/coll_c_api.cc @@ -0,0 +1,119 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#include // for seconds +#include // for size_t +#include // for future +#include // for unique_ptr +#include // for string +#include // for is_same_v, remove_pointer_t +#include // for pair + +#include "../collective/tracker.h" // for RabitTracker +#include "c_api_error.h" // for API_BEGIN +#include "xgboost/c_api.h" +#include "xgboost/collective/result.h" // for Result +#include "xgboost/json.h" // for Json +#include "xgboost/string_view.h" // for StringView + +#if defined(XGBOOST_USE_FEDERATED) +#include "../../plugin/federated/federated_tracker.h" // for FederatedTracker +#else +#include "../common/error_msg.h" // for NoFederated +#endif + +using namespace xgboost; // NOLINT + +namespace { +using TrackerHandleT = + std::pair, std::shared_future>; + +TrackerHandleT *GetTrackerHandle(TrackerHandle handle) { + xgboost_CHECK_C_ARG_PTR(handle); + auto *ptr = static_cast(handle); + CHECK(ptr); + return ptr; +} + +struct CollAPIEntry { + std::string ret_str; +}; +using CollAPIThreadLocalStore = dmlc::ThreadLocalStore; + +void WaitImpl(TrackerHandleT *ptr) { + std::chrono::seconds wait_for{100}; + auto fut = ptr->second; + while (fut.valid()) { + auto res = fut.wait_for(wait_for); + CHECK(res != std::future_status::deferred); + if (res == std::future_status::ready) { + auto const &rc = ptr->second.get(); + CHECK(rc.OK()) << rc.Report(); + break; + } + } +} +} // namespace + +XGB_DLL int XGTrackerCreate(char const *config, TrackerHandle *handle) { + API_BEGIN(); + xgboost_CHECK_C_ARG_PTR(config); + + Json jconfig = Json::Load(config); + + auto type = RequiredArg(jconfig, "dmlc_communicator", __func__); + std::unique_ptr tptr; + if (type == "federated") { +#if defined(XGBOOST_USE_FEDERATED) + tptr = std::make_unique(jconfig); +#else + LOG(FATAL) << error::NoFederated(); +#endif // defined(XGBOOST_USE_FEDERATED) + } else if (type == "rabit") { + tptr = std::make_unique(jconfig); + } else { + LOG(FATAL) << "Unknown communicator:" << type; + } + + auto ptr = new TrackerHandleT{std::move(tptr), std::future{}}; + static_assert(std::is_same_v, TrackerHandleT>); + + xgboost_CHECK_C_ARG_PTR(handle); + *handle = ptr; + API_END(); +} + +XGB_DLL int XGTrackerWorkerArgs(TrackerHandle handle, char const **args) { + API_BEGIN(); + auto *ptr = GetTrackerHandle(handle); + auto &local = *CollAPIThreadLocalStore::Get(); + local.ret_str = Json::Dump(ptr->first->WorkerArgs()); + xgboost_CHECK_C_ARG_PTR(args); + *args = local.ret_str.c_str(); + API_END(); +} + +XGB_DLL int XGTrackerRun(TrackerHandle handle) { + API_BEGIN(); + auto *ptr = GetTrackerHandle(handle); + CHECK(!ptr->second.valid()) << "Tracker is already running."; + ptr->second = ptr->first->Run(); + API_END(); +} + +XGB_DLL int XGTrackerWait(TrackerHandle handle, char const *config) { + API_BEGIN(); + auto *ptr = GetTrackerHandle(handle); + xgboost_CHECK_C_ARG_PTR(config); + auto jconfig = Json::Load(StringView{config}); + WaitImpl(ptr); + API_END(); +} + +XGB_DLL int XGTrackerFree(TrackerHandle handle) { + API_BEGIN(); + auto *ptr = GetTrackerHandle(handle); + WaitImpl(ptr); + delete ptr; + API_END(); +} diff --git a/src/collective/tracker.h b/src/collective/tracker.h index 24e47bb4e..f336a82f9 100644 --- a/src/collective/tracker.h +++ b/src/collective/tracker.h @@ -114,6 +114,9 @@ class RabitTracker : public Tracker { // record for how to reach out to workers if error happens. std::vector> worker_error_handles_; // listening socket for incoming workers. + // + // At the moment, the listener calls accept without first polling. We can add an + // additional unix domain socket to allow cancelling the accept. TCPSocket listener_; Result Bootstrap(std::vector* p_workers); diff --git a/src/common/error_msg.h b/src/common/error_msg.h index 94703fd15..995fe11d5 100644 --- a/src/common/error_msg.h +++ b/src/common/error_msg.h @@ -97,5 +97,7 @@ constexpr StringView InvalidCUDAOrdinal() { } void MismatchedDevices(Context const* booster, Context const* data); + +inline auto NoFederated() { return "XGBoost is not compiled with federated learning support."; } } // namespace xgboost::error #endif // XGBOOST_COMMON_ERROR_MSG_H_ diff --git a/tests/cpp/collective/test_coll_c_api.cc b/tests/cpp/collective/test_coll_c_api.cc new file mode 100644 index 000000000..d80fbc140 --- /dev/null +++ b/tests/cpp/collective/test_coll_c_api.cc @@ -0,0 +1,63 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#include +#include + +#include // for ""s +#include // for thread + +#include "../../../src/collective/tracker.h" +#include "test_worker.h" // for SocketTest +#include "xgboost/json.h" // for Json + +namespace xgboost::collective { +namespace { +class TrackerAPITest : public SocketTest {}; +} // namespace + +TEST_F(TrackerAPITest, CAPI) { + TrackerHandle handle; + Json config{Object{}}; + config["dmlc_communicator"] = String{"rabit"}; + config["n_workers"] = 2; + config["timeout"] = 1; + auto config_str = Json::Dump(config); + auto rc = XGTrackerCreate(config_str.c_str(), &handle); + ASSERT_EQ(rc, 0); + rc = XGTrackerRun(handle); + ASSERT_EQ(rc, 0); + + std::thread bg_wait{[&] { + Json config{Object{}}; + auto config_str = Json::Dump(config); + auto rc = XGTrackerWait(handle, config_str.c_str()); + ASSERT_EQ(rc, 0); + }}; + + char const* cargs; + rc = XGTrackerWorkerArgs(handle, &cargs); + ASSERT_EQ(rc, 0); + auto args = Json::Load(StringView{cargs}); + + std::string host; + ASSERT_TRUE(GetHostAddress(&host).OK()); + ASSERT_EQ(host, get(args["DMLC_TRACKER_URI"])); + auto port = get(args["DMLC_TRACKER_PORT"]); + ASSERT_NE(port, 0); + + std::vector workers; + using namespace std::chrono_literals; // NOLINT + for (std::int32_t r = 0; r < 2; ++r) { + workers.emplace_back([=] { WorkerForTest w{host, static_cast(port), 1s, 2, r}; }); + } + for (auto& w : workers) { + w.join(); + } + + rc = XGTrackerFree(handle); + ASSERT_EQ(rc, 0); + + bg_wait.join(); +} +} // namespace xgboost::collective From 6fd4a306670fa82da06b42ef217705eefd97cbcd Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 9 Nov 2023 05:26:40 +0800 Subject: [PATCH 008/109] [coll] Increase timeout for allgather test. (#9777) --- tests/cpp/collective/test_allgather.cc | 2 +- tests/cpp/collective/test_worker.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/cpp/collective/test_allgather.cc b/tests/cpp/collective/test_allgather.cc index bdfadc0c7..decad8786 100644 --- a/tests/cpp/collective/test_allgather.cc +++ b/tests/cpp/collective/test_allgather.cc @@ -47,7 +47,7 @@ class Worker : public WorkerForTest { std::size_t n = 8192; // n_bytes = 8192 * sizeof(int) std::vector data(comm_.World() * n, 0); - auto s_data = common::Span{data.data(), data.size()}; + auto s_data = common::Span{data}; auto seg = s_data.subspan(comm_.Rank() * n, n); std::iota(seg.begin(), seg.end(), comm_.Rank()); diff --git a/tests/cpp/collective/test_worker.h b/tests/cpp/collective/test_worker.h index ad3213e81..490cdf13c 100644 --- a/tests/cpp/collective/test_worker.h +++ b/tests/cpp/collective/test_worker.h @@ -92,11 +92,12 @@ class TrackerTest : public SocketTest { template void TestDistributed(std::int32_t n_workers, WorkerFn worker_fn) { - std::chrono::seconds timeout{1}; + std::chrono::seconds timeout{2}; std::string host; auto rc = GetHostAddress(&host); ASSERT_TRUE(rc.OK()) << rc.Report(); + LOG(INFO) << "Using " << n_workers << " workers for test."; RabitTracker tracker{StringView{host}, n_workers, 0, timeout}; auto fut = tracker.Run(); From 162da7b52b6b88de7b95319b4ded746b148b9030 Mon Sep 17 00:00:00 2001 From: Ken Geis Date: Sun, 12 Nov 2023 11:09:06 -0800 Subject: [PATCH 009/109] fix typo in Parameters doc (#9781) --- doc/parameter.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/parameter.rst b/doc/parameter.rst index 88a712a5a..d2471dfd9 100644 --- a/doc/parameter.rst +++ b/doc/parameter.rst @@ -470,7 +470,7 @@ Parameter for using Pseudo-Huber (``reg:pseudohubererror``) Parameter for using Quantile Loss (``reg:quantileerror``) ========================================================= -* ``quantile_alpha``: A scala or a list of targeted quantiles. +* ``quantile_alpha``: A scalar or a list of targeted quantiles. .. versionadded:: 2.0.0 From 36a552ac981f4936714c4cd9128ba33f2a63d2eb Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Tue, 14 Nov 2023 08:59:45 +0800 Subject: [PATCH 010/109] [jvm-packages] support stage-level scheduling (#9775) --- .../rapids/spark/GpuXGBoostGeneralSuite.scala | 2 +- .../dmlc/xgboost4j/scala/spark/XGBoost.scala | 153 ++++++++++++++++-- .../scala/spark/params/BoosterParams.scala | 6 +- .../xgboost4j/scala/spark/XGBoostSuite.scala | 150 +++++++++++++++++ 4 files changed, 298 insertions(+), 13 deletions(-) create mode 100644 jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostSuite.scala diff --git a/jvm-packages/xgboost4j-spark-gpu/src/test/scala/ml/dmlc/xgboost4j/scala/rapids/spark/GpuXGBoostGeneralSuite.scala b/jvm-packages/xgboost4j-spark-gpu/src/test/scala/ml/dmlc/xgboost4j/scala/rapids/spark/GpuXGBoostGeneralSuite.scala index c731afb1d..746e03bb6 100644 --- a/jvm-packages/xgboost4j-spark-gpu/src/test/scala/ml/dmlc/xgboost4j/scala/rapids/spark/GpuXGBoostGeneralSuite.scala +++ b/jvm-packages/xgboost4j-spark-gpu/src/test/scala/ml/dmlc/xgboost4j/scala/rapids/spark/GpuXGBoostGeneralSuite.scala @@ -206,7 +206,7 @@ class GpuXGBoostGeneralSuite extends GpuTestSuite { .setDevice("cuda:1") .fit(trainingDf) } - assert(thrown.getMessage.contains("`cuda` or `gpu`")) + assert(thrown.getMessage.contains("device given invalid value cuda:1")) } } } diff --git a/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/XGBoost.scala b/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/XGBoost.scala index d12431479..5a1af886f 100644 --- a/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/XGBoost.scala +++ b/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/XGBoost.scala @@ -31,7 +31,8 @@ import org.apache.commons.logging.LogFactory import org.apache.hadoop.fs.FileSystem import org.apache.spark.rdd.RDD -import org.apache.spark.{SparkContext, TaskContext} +import org.apache.spark.resource.{ResourceProfileBuilder, TaskResourceRequests} +import org.apache.spark.{SparkConf, SparkContext, TaskContext} import org.apache.spark.sql.SparkSession /** @@ -72,7 +73,8 @@ private[scala] case class XGBoostExecutionParams( device: Option[String], isLocal: Boolean, featureNames: Option[Array[String]], - featureTypes: Option[Array[String]]) { + featureTypes: Option[Array[String]], + runOnGpu: Boolean) { private var rawParamMap: Map[String, Any] = _ @@ -186,14 +188,15 @@ private[this] class XGBoostExecutionParamsFactory(rawParams: Map[String, Any], s .asInstanceOf[Boolean] val treeMethod: Option[String] = overridedParams.get("tree_method").map(_.toString) - // back-compatible with "gpu_hist" - val device: Option[String] = if (treeMethod.exists(_ == "gpu_hist")) { - Some("cuda") - } else overridedParams.get("device").map(_.toString) + val device: Option[String] = overridedParams.get("device").map(_.toString) + val deviceIsGpu = device.exists(_ == "cuda") - require(!(treeMethod.exists(_ == "approx") && device.exists(_ == "cuda")), + require(!(treeMethod.exists(_ == "approx") && deviceIsGpu), "The tree method \"approx\" is not yet supported for Spark GPU cluster") + // back-compatible with "gpu_hist" + val runOnGpu = treeMethod.exists(_ == "gpu_hist") || deviceIsGpu + val trackerConf = overridedParams.get("tracker_conf") match { case None => TrackerConf() case Some(conf: TrackerConf) => conf @@ -228,7 +231,8 @@ private[this] class XGBoostExecutionParamsFactory(rawParams: Map[String, Any], s device, isLocal, featureNames, - featureTypes + featureTypes, + runOnGpu ) xgbExecParam.setRawParamMap(overridedParams) xgbExecParam @@ -253,7 +257,132 @@ private[this] class XGBoostExecutionParamsFactory(rawParams: Map[String, Any], s ) } -object XGBoost extends Serializable { +/** + * A trait to manage stage-level scheduling + */ +private[spark] trait XGBoostStageLevel extends Serializable { + private val logger = LogFactory.getLog("XGBoostSpark") + + private[spark] def isStandaloneOrLocalCluster(conf: SparkConf): Boolean = { + val master = conf.get("spark.master") + master != null && (master.startsWith("spark://") || master.startsWith("local-cluster")) + } + + /** + * To determine if stage-level scheduling should be skipped according to the spark version + * and spark configurations + * + * @param sparkVersion spark version + * @param runOnGpu if xgboost training run on GPUs + * @param conf spark configurations + * @return Boolean to skip stage-level scheduling or not + */ + private[spark] def skipStageLevelScheduling( + sparkVersion: String, + runOnGpu: Boolean, + conf: SparkConf): Boolean = { + if (runOnGpu) { + if (sparkVersion < "3.4.0") { + logger.info("Stage-level scheduling in xgboost requires spark version 3.4.0+") + return true + } + + if (!isStandaloneOrLocalCluster(conf)) { + logger.info("Stage-level scheduling in xgboost requires spark standalone or " + + "local-cluster mode") + return true + } + + val executorCores = conf.getInt("spark.executor.cores", -1) + val executorGpus = conf.getInt("spark.executor.resource.gpu.amount", -1) + if (executorCores == -1 || executorGpus == -1) { + logger.info("Stage-level scheduling in xgboost requires spark.executor.cores, " + + "spark.executor.resource.gpu.amount to be set.") + return true + } + + if (executorCores == 1) { + logger.info("Stage-level scheduling in xgboost requires spark.executor.cores > 1") + return true + } + + if (executorGpus > 1) { + logger.info("Stage-level scheduling in xgboost will not work " + + "when spark.executor.resource.gpu.amount > 1") + return true + } + + val taskGpuAmount = conf.getDouble("spark.task.resource.gpu.amount", -1.0).toFloat + + if (taskGpuAmount == -1.0) { + // The ETL tasks will not grab a gpu when spark.task.resource.gpu.amount is not set, + // but with stage-level scheduling, we can make training task grab the gpu. + return false + } + + if (taskGpuAmount == executorGpus.toFloat) { + // spark.executor.resource.gpu.amount = spark.task.resource.gpu.amount + // results in only 1 task running at a time, which may cause perf issue. + return true + } + // We can enable stage-level scheduling + false + } else true // Skip stage-level scheduling for cpu training. + } + + /** + * Attempt to modify the task resources so that only one task can be executed + * on a single executor simultaneously. + * + * @param sc the spark context + * @param rdd which rdd to be applied with new resource profile + * @return the original rdd or the changed rdd + */ + private[spark] def tryStageLevelScheduling( + sc: SparkContext, + xgbExecParams: XGBoostExecutionParams, + rdd: RDD[(Booster, Map[String, Array[Float]])] + ): RDD[(Booster, Map[String, Array[Float]])] = { + + val conf = sc.getConf + if (skipStageLevelScheduling(sc.version, xgbExecParams.runOnGpu, conf)) { + return rdd + } + + // Ensure executor_cores is not None + val executor_cores = conf.getInt("spark.executor.cores", -1) + if (executor_cores == -1) { + throw new RuntimeException("Wrong spark.executor.cores") + } + + // Spark-rapids is a GPU-acceleration project for Spark SQL. + // When spark-rapids is enabled, we prevent concurrent execution of other ETL tasks + // that utilize GPUs alongside training tasks in order to avoid GPU out-of-memory errors. + val spark_plugins = conf.get("spark.plugins", " ") + val spark_rapids_sql_enabled = conf.get("spark.rapids.sql.enabled", "true") + + // Determine the number of cores required for each task. + val task_cores = if (spark_plugins.contains("com.nvidia.spark.SQLPlugin") && + spark_rapids_sql_enabled.toLowerCase == "true") { + executor_cores + } else { + (executor_cores / 2) + 1 + } + + // Each training task requires cpu cores > total executor cores//2 + 1 to + // ensure tasks are sent to different executors. + // Note: We cannot use GPUs to limit concurrent tasks + // due to https://issues.apache.org/jira/browse/SPARK-45527. + val task_gpus = 1.0 + val treqs = new TaskResourceRequests().cpus(task_cores).resource("gpu", task_gpus) + val rp = new ResourceProfileBuilder().require(treqs).build() + + logger.info(s"XGBoost training tasks require the resource(cores=$task_cores, gpu=$task_gpus).") + rdd.withResources(rp) + } +} + +object XGBoost extends XGBoostStageLevel { private val logger = LogFactory.getLog("XGBoostSpark") def getGPUAddrFromResources: Int = { @@ -315,7 +444,7 @@ object XGBoost extends Serializable { val externalCheckpointParams = xgbExecutionParam.checkpointParam var params = xgbExecutionParam.toMap - if (xgbExecutionParam.device.exists(m => (m == "cuda" || m == "gpu"))) { + if (xgbExecutionParam.runOnGpu) { val gpuId = if (xgbExecutionParam.isLocal) { // For local mode, force gpu id to primary device 0 @@ -413,10 +542,12 @@ object XGBoost extends Serializable { }} + val boostersAndMetricsWithRes = tryStageLevelScheduling(sc, xgbExecParams, + boostersAndMetrics) // The repartition step is to make training stage as ShuffleMapStage, so that when one // of the training task fails the training stage can retry. ResultStage won't retry when // it fails. - val (booster, metrics) = boostersAndMetrics.repartition(1).collect()(0) + val (booster, metrics) = boostersAndMetricsWithRes.repartition(1).collect()(0) val trackerReturnVal = tracker.waitFor(0L) logger.info(s"Rabit returns with exit code $trackerReturnVal") if (trackerReturnVal != 0) { diff --git a/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/params/BoosterParams.scala b/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/params/BoosterParams.scala index 61efc2865..b64ad9385 100644 --- a/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/params/BoosterParams.scala +++ b/jvm-packages/xgboost4j-spark/src/main/scala/ml/dmlc/xgboost4j/scala/spark/params/BoosterParams.scala @@ -154,11 +154,13 @@ private[spark] trait BoosterParams extends Params { (value: String) => BoosterParams.supportedTreeMethods.contains(value)) final def getTreeMethod: String = $(treeMethod) + /** * The device for running XGBoost algorithms, options: cpu, cuda */ final val device = new Param[String]( - this, "device", "The device for running XGBoost algorithms, options: cpu, cuda" + this, "device", "The device for running XGBoost algorithms, options: cpu, cuda", + (value: String) => BoosterParams.supportedDevices.contains(value) ) final def getDevice: String = $(device) @@ -288,4 +290,6 @@ private[scala] object BoosterParams { val supportedSampleType = HashSet("uniform", "weighted") val supportedNormalizeType = HashSet("tree", "forest") + + val supportedDevices = HashSet("cpu", "cuda") } diff --git a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostSuite.scala b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostSuite.scala new file mode 100644 index 000000000..9622c9b2d --- /dev/null +++ b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostSuite.scala @@ -0,0 +1,150 @@ +/* + Copyright (c) 2023 by Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package ml.dmlc.xgboost4j.scala.spark + +import ml.dmlc.xgboost4j.scala.Booster +import org.apache.spark.SparkConf +import org.apache.spark.rdd.RDD +import org.apache.spark.sql.SparkSession +import org.scalatest.funsuite.AnyFunSuite + +class XGBoostSuite extends AnyFunSuite with PerTest { + + // Do not create spark context + override def beforeEach(): Unit = {} + + test("XGBoost execution parameters") { + var xgbExecutionParams = new XGBoostExecutionParamsFactory( + Map("device" -> "cpu", "num_workers" -> 1, "num_round" -> 1), sc) + .buildXGBRuntimeParams + assert(!xgbExecutionParams.runOnGpu) + + xgbExecutionParams = new XGBoostExecutionParamsFactory( + Map("device" -> "cuda", "num_workers" -> 1, "num_round" -> 1), sc) + .buildXGBRuntimeParams + assert(xgbExecutionParams.runOnGpu) + + xgbExecutionParams = new XGBoostExecutionParamsFactory( + Map("device" -> "cpu", "tree_method" -> "gpu_hist", "num_workers" -> 1, "num_round" -> 1), sc) + .buildXGBRuntimeParams + assert(xgbExecutionParams.runOnGpu) + + xgbExecutionParams = new XGBoostExecutionParamsFactory( + Map("device" -> "cuda", "tree_method" -> "gpu_hist", + "num_workers" -> 1, "num_round" -> 1), sc) + .buildXGBRuntimeParams + assert(xgbExecutionParams.runOnGpu) + } + + test("skip stage-level scheduling") { + val conf = new SparkConf() + .setMaster("spark://foo") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + .set("spark.task.resource.gpu.amount", "0.08") + + // the correct configurations should not skip stage-level scheduling + assert(!XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, conf)) + + // spark version < 3.4.0 + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.3.0", runOnGpu = true, conf)) + + // not run on GPU + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = false, conf)) + + // spark.executor.cores is not set + var badConf = conf.clone().remove("spark.executor.cores") + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + + // spark.executor.cores=1 + badConf = conf.clone().set("spark.executor.cores", "1") + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + + // spark.executor.resource.gpu.amount is not set + badConf = conf.clone().remove("spark.executor.resource.gpu.amount") + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + + // spark.executor.resource.gpu.amount>1 + badConf = conf.clone().set("spark.executor.resource.gpu.amount", "2") + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + + // spark.task.resource.gpu.amount is not set + badConf = conf.clone().remove("spark.task.resource.gpu.amount") + assert(!XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + + // spark.task.resource.gpu.amount=1 + badConf = conf.clone().set("spark.task.resource.gpu.amount", "1") + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + + // yarn + badConf = conf.clone().setMaster("yarn") + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + + // k8s + badConf = conf.clone().setMaster("k8s://") + assert(XGBoost.skipStageLevelScheduling(sparkVersion = "3.4.0", runOnGpu = true, badConf)) + } + + + object FakedXGBoost extends XGBoostStageLevel { + + // Do not skip stage-level scheduling for testing purposes. + override private[spark] def skipStageLevelScheduling( + sparkVersion: String, + runOnGpu: Boolean, + conf: SparkConf) = false + } + + test("try stage-level scheduling without spark-rapids") { + + val builder = SparkSession.builder() + .master(s"local-cluster[1, 4, 1024]") + .appName("XGBoostSuite") + .config("spark.ui.enabled", false) + .config("spark.driver.memory", "512m") + .config("spark.barrier.sync.timeout", 10) + .config("spark.task.cpus", 1) + .config("spark.executor.cores", 4) + .config("spark.executor.resource.gpu.amount", 1) + .config("spark.task.resource.gpu.amount", 0.25) + + val ss = builder.getOrCreate() + + try { + val df = ss.range(1, 10) + val rdd = df.rdd + + val xgbExecutionParams = new XGBoostExecutionParamsFactory( + Map("device" -> "cuda", "num_workers" -> 1, "num_round" -> 1), sc) + .buildXGBRuntimeParams + assert(xgbExecutionParams.runOnGpu) + + val finalRDD = FakedXGBoost.tryStageLevelScheduling(ss.sparkContext, xgbExecutionParams, + rdd.asInstanceOf[RDD[(Booster, Map[String, Array[Float]])]]) + + val taskResources = finalRDD.getResourceProfile().taskResources + assert(taskResources.contains("cpus")) + assert(taskResources.get("cpus").get.amount == 3) + + assert(taskResources.contains("gpu")) + assert(taskResources.get("gpu").get.amount == 1.0) + } finally { + ss.stop() + } + } +} From ada377c57eec006889484d10e5ce83e4ac46c971 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 15 Nov 2023 14:16:19 +0800 Subject: [PATCH 011/109] [coll] Reduce the scope of lock in the event loop. (#9784) --- include/xgboost/collective/socket.h | 23 ++++--- rabit/src/allreduce_base.cc | 4 +- src/collective/allreduce.cc | 11 ++-- src/collective/comm.cc | 35 ++++++---- src/collective/loop.cc | 88 ++++++++++++++++++-------- src/collective/loop.h | 17 ++--- tests/cpp/collective/test_allreduce.cc | 9 ++- 7 files changed, 117 insertions(+), 70 deletions(-) diff --git a/include/xgboost/collective/socket.h b/include/xgboost/collective/socket.h index 5dd1b9ffa..844534110 100644 --- a/include/xgboost/collective/socket.h +++ b/include/xgboost/collective/socket.h @@ -412,19 +412,24 @@ class TCPSocket { return Success(); } - void SetKeepAlive() { + [[nodiscard]] Result SetKeepAlive() { std::int32_t keepalive = 1; - xgboost_CHECK_SYS_CALL(setsockopt(handle_, SOL_SOCKET, SO_KEEPALIVE, - reinterpret_cast(&keepalive), sizeof(keepalive)), - 0); + auto rc = setsockopt(handle_, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&keepalive), + sizeof(keepalive)); + if (rc != 0) { + return system::FailWithCode("Failed to set TCP keeaplive."); + } + return Success(); } - void SetNoDelay() { + [[nodiscard]] Result SetNoDelay() { std::int32_t tcp_no_delay = 1; - xgboost_CHECK_SYS_CALL( - setsockopt(handle_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&tcp_no_delay), - sizeof(tcp_no_delay)), - 0); + auto rc = setsockopt(handle_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&tcp_no_delay), + sizeof(tcp_no_delay)); + if (rc != 0) { + return system::FailWithCode("Failed to set TCP no delay."); + } + return Success(); } /** diff --git a/rabit/src/allreduce_base.cc b/rabit/src/allreduce_base.cc index 5cab4ae32..b99eb3763 100644 --- a/rabit/src/allreduce_base.cc +++ b/rabit/src/allreduce_base.cc @@ -417,9 +417,9 @@ void AllreduceBase::SetParam(const char *name, const char *val) { utils::Assert(!all_link.sock.BadSocket(), "ReConnectLink: bad socket"); // set the socket to non-blocking mode, enable TCP keepalive CHECK(all_link.sock.NonBlocking(true).OK()); - all_link.sock.SetKeepAlive(); + CHECK(all_link.sock.SetKeepAlive().OK()); if (rabit_enable_tcp_no_delay) { - all_link.sock.SetNoDelay(); + CHECK(all_link.sock.SetNoDelay().OK()); } if (tree_neighbors.count(all_link.rank) != 0) { if (all_link.rank == parent_rank) { diff --git a/src/collective/allreduce.cc b/src/collective/allreduce.cc index 65c066868..f95a9a9f1 100644 --- a/src/collective/allreduce.cc +++ b/src/collective/allreduce.cc @@ -6,6 +6,7 @@ #include // for min #include // for size_t #include // for int32_t, int8_t +#include // for move #include // for vector #include "../data/array_interface.h" // for Type, DispatchDType @@ -47,7 +48,7 @@ Result RingScatterReduceTyped(Comm const& comm, common::Span data, auto seg = s_buf.subspan(0, recv_seg.size()); prev_ch->RecvAll(seg); - auto rc = prev_ch->Block(); + auto rc = comm.Block(); if (!rc.OK()) { return rc; } @@ -83,11 +84,9 @@ Result RingAllreduce(Comm const& comm, common::Span data, Func cons auto prev_ch = comm.Chan(prev); auto next_ch = comm.Chan(next); - rc = RingAllgather(comm, data, n_bytes_in_seg, 1, prev_ch, next_ch); - if (!rc.OK()) { - return rc; - } - return comm.Block(); + return std::move(rc) << [&] { + return RingAllgather(comm, data, n_bytes_in_seg, 1, prev_ch, next_ch); + } << [&] { return comm.Block(); }; }); } } // namespace xgboost::collective::cpu_impl diff --git a/src/collective/comm.cc b/src/collective/comm.cc index 964137ff1..9da9083f8 100644 --- a/src/collective/comm.cc +++ b/src/collective/comm.cc @@ -33,19 +33,28 @@ Comm::Comm(std::string const& host, std::int32_t port, std::chrono::seconds time Result ConnectTrackerImpl(proto::PeerInfo info, std::chrono::seconds timeout, std::int32_t retry, std::string const& task_id, TCPSocket* out, std::int32_t rank, std::int32_t world) { - // get information from tracker + // Get information from the tracker CHECK(!info.host.empty()); - auto rc = Connect(info.host, info.port, retry, timeout, out); - if (!rc.OK()) { - return Fail("Failed to connect to the tracker.", std::move(rc)); - } - TCPSocket& tracker = *out; - return std::move(rc) - << [&] { return tracker.NonBlocking(false); } - << [&] { return tracker.RecvTimeout(timeout); } - << [&] { return proto::Magic{}.Verify(&tracker); } - << [&] { return proto::Connect{}.WorkerSend(&tracker, world, rank, task_id); }; + return Success() << [&] { + auto rc = Connect(info.host, info.port, retry, timeout, out); + if (rc.OK()) { + return rc; + } else { + return Fail("Failed to connect to the tracker.", std::move(rc)); + } + } << [&] { + return tracker.NonBlocking(false); + } << [&] { + return tracker.RecvTimeout(timeout); + } << [&] { + return proto::Magic{}.Verify(&tracker); + } << [&] { + return proto::Connect{}.WorkerSend(&tracker, world, rank, task_id); + } << [&] { + LOG(INFO) << "Task " << task_id << " connected to the tracker"; + return Success(); + }; } [[nodiscard]] Result Comm::ConnectTracker(TCPSocket* out) const { @@ -257,8 +266,8 @@ RabitComm::RabitComm(std::string const& host, std::int32_t port, std::chrono::se CHECK(this->channels_.empty()); for (auto& w : workers) { if (w) { - w->SetNoDelay(); - rc = w->NonBlocking(true); + rc = std::move(rc) << [&] { return w->SetNoDelay(); } << [&] { return w->NonBlocking(true); } + << [&] { return w->SetKeepAlive(); }; } if (!rc.OK()) { return rc; diff --git a/src/collective/loop.cc b/src/collective/loop.cc index 95a1019ac..10fce0516 100644 --- a/src/collective/loop.cc +++ b/src/collective/loop.cc @@ -10,21 +10,26 @@ #include "xgboost/logging.h" // for CHECK namespace xgboost::collective { -Result Loop::EmptyQueue() { +Result Loop::EmptyQueue(std::queue* p_queue) const { timer_.Start(__func__); - auto error = [this] { - this->stop_ = true; + auto error = [this] { timer_.Stop(__func__); }; + + if (stop_) { timer_.Stop(__func__); - }; + return Success(); + } - while (!queue_.empty() && !stop_) { - std::queue qcopy; + auto& qcopy = *p_queue; + + // clear the copied queue + while (!qcopy.empty()) { rabit::utils::PollHelper poll; + std::size_t n_ops = qcopy.size(); - // watch all ops - while (!queue_.empty()) { - auto op = queue_.front(); - queue_.pop(); + // Iterate through all the ops for poll + for (std::size_t i = 0; i < n_ops; ++i) { + auto op = qcopy.front(); + qcopy.pop(); switch (op.code) { case Op::kRead: { @@ -40,6 +45,7 @@ Result Loop::EmptyQueue() { return Fail("Invalid socket operation."); } } + qcopy.push(op); } @@ -51,10 +57,12 @@ Result Loop::EmptyQueue() { error(); return rc; } + // we wonldn't be here if the queue is empty. CHECK(!qcopy.empty()); - while (!qcopy.empty() && !stop_) { + // Iterate through all the ops for performing the operations + for (std::size_t i = 0; i < n_ops; ++i) { auto op = qcopy.front(); qcopy.pop(); @@ -81,20 +89,21 @@ Result Loop::EmptyQueue() { } if (n_bytes_done == -1 && !system::LastErrorWouldBlock()) { - stop_ = true; auto rc = system::FailWithCode("Invalid socket output."); error(); return rc; } + op.off += n_bytes_done; CHECK_LE(op.off, op.n); if (op.off != op.n) { // not yet finished, push back to queue for next round. - queue_.push(op); + qcopy.push(op); } } } + timer_.Stop(__func__); return Success(); } @@ -107,22 +116,42 @@ void Loop::Process() { if (stop_) { break; } - CHECK(!mu_.try_lock()); - this->rc_ = this->EmptyQueue(); - if (!rc_.OK()) { - stop_ = true; + auto unlock_notify = [&](bool is_blocking) { + if (!is_blocking) { + return; + } + lock.unlock(); cv_.notify_one(); - break; + }; + + // move the queue + std::queue qcopy; + bool is_blocking = false; + while (!queue_.empty()) { + auto op = queue_.front(); + queue_.pop(); + if (op.code == Op::kBlock) { + is_blocking = true; + } else { + qcopy.push(op); + } + } + // unblock the queue + if (!is_blocking) { + lock.unlock(); + } + // clear the queue + auto rc = this->EmptyQueue(&qcopy); + // Handle error + if (!rc.OK()) { + this->rc_ = std::move(rc); + unlock_notify(is_blocking); + return; } - CHECK(queue_.empty()); - CHECK(!mu_.try_lock()); - cv_.notify_one(); - } - - if (rc_.OK()) { - CHECK(queue_.empty()); + CHECK(qcopy.empty()); + unlock_notify(is_blocking); } } @@ -140,6 +169,15 @@ Result Loop::Stop() { return Success(); } +[[nodiscard]] Result Loop::Block() { + this->Submit(Op{Op::kBlock}); + { + std::unique_lock lock{mu_}; + cv_.wait(lock, [this] { return (this->queue_.empty()) || stop_; }); + } + return std::move(rc_); +} + Loop::Loop(std::chrono::seconds timeout) : timeout_{timeout} { timer_.Init(__func__); worker_ = std::thread{[this] { diff --git a/src/collective/loop.h b/src/collective/loop.h index 0bccbc0d0..4f5cb12b3 100644 --- a/src/collective/loop.h +++ b/src/collective/loop.h @@ -20,13 +20,14 @@ namespace xgboost::collective { class Loop { public: struct Op { - enum Code : std::int8_t { kRead = 0, kWrite = 1 } code; + enum Code : std::int8_t { kRead = 0, kWrite = 1, kBlock = 2 } code; std::int32_t rank{-1}; std::int8_t* ptr{nullptr}; std::size_t n{0}; TCPSocket* sock{nullptr}; std::size_t off{0}; + explicit Op(Code c) : code{c} { CHECK(c == kBlock); } Op(Code c, std::int32_t rank, std::int8_t* ptr, std::size_t n, TCPSocket* sock, std::size_t off) : code{c}, rank{rank}, ptr{ptr}, n{n}, sock{sock}, off{off} {} Op(Op const&) = default; @@ -44,9 +45,9 @@ class Loop { Result rc_; bool stop_{false}; std::exception_ptr curr_exce_{nullptr}; - common::Monitor timer_; + common::Monitor mutable timer_; - Result EmptyQueue(); + Result EmptyQueue(std::queue* p_queue) const; void Process(); public: @@ -60,15 +61,7 @@ class Loop { cv_.notify_one(); } - [[nodiscard]] Result Block() { - { - std::unique_lock lock{mu_}; - cv_.notify_all(); - } - std::unique_lock lock{mu_}; - cv_.wait(lock, [this] { return this->queue_.empty() || stop_; }); - return std::move(rc_); - } + [[nodiscard]] Result Block(); explicit Loop(std::chrono::seconds timeout); diff --git a/tests/cpp/collective/test_allreduce.cc b/tests/cpp/collective/test_allreduce.cc index 744608dec..21b4d9fd0 100644 --- a/tests/cpp/collective/test_allreduce.cc +++ b/tests/cpp/collective/test_allreduce.cc @@ -18,31 +18,34 @@ class AllreduceWorker : public WorkerForTest { void Basic() { { std::vector data(13, 0.0); - Allreduce(comm_, common::Span{data.data(), data.size()}, [](auto lhs, auto rhs) { + auto rc = Allreduce(comm_, common::Span{data.data(), data.size()}, [](auto lhs, auto rhs) { for (std::size_t i = 0; i < rhs.size(); ++i) { rhs[i] += lhs[i]; } }); + ASSERT_TRUE(rc.OK()); ASSERT_EQ(std::accumulate(data.cbegin(), data.cend(), 0.0), 0.0); } { std::vector data(1, 1.0); - Allreduce(comm_, common::Span{data.data(), data.size()}, [](auto lhs, auto rhs) { + auto rc = Allreduce(comm_, common::Span{data.data(), data.size()}, [](auto lhs, auto rhs) { for (std::size_t i = 0; i < rhs.size(); ++i) { rhs[i] += lhs[i]; } }); + ASSERT_TRUE(rc.OK()); ASSERT_EQ(data[0], static_cast(comm_.World())); } } void Acc() { std::vector data(314, 1.5); - Allreduce(comm_, common::Span{data.data(), data.size()}, [](auto lhs, auto rhs) { + auto rc = Allreduce(comm_, common::Span{data.data(), data.size()}, [](auto lhs, auto rhs) { for (std::size_t i = 0; i < rhs.size(); ++i) { rhs[i] += lhs[i]; } }); + ASSERT_TRUE(rc.OK()); for (std::size_t i = 0; i < data.size(); ++i) { auto v = data[i]; ASSERT_EQ(v, 1.5 * static_cast(comm_.World())) << i; From 178cfe70a8bf6ca0081032cf0de47e5219d0cf08 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 16 Nov 2023 18:15:59 +0800 Subject: [PATCH 012/109] [pyspark][doc] Test and doc for stage-level scheduling. (#9786) --- doc/jvm/xgboost4j_spark_gpu_tutorial.rst | 20 +++- python-package/xgboost/spark/core.py | 31 +++-- python-package/xgboost/spark/utils.py | 6 +- .../test_with_spark/test_spark_local.py | 108 ++++++++++++++++++ 4 files changed, 144 insertions(+), 21 deletions(-) diff --git a/doc/jvm/xgboost4j_spark_gpu_tutorial.rst b/doc/jvm/xgboost4j_spark_gpu_tutorial.rst index 3b2f92c6f..edabe8a92 100644 --- a/doc/jvm/xgboost4j_spark_gpu_tutorial.rst +++ b/doc/jvm/xgboost4j_spark_gpu_tutorial.rst @@ -215,6 +215,22 @@ and the prediction for each instance. Submit the application ********************** +Assuming you have configured the Spark standalone cluster with GPU support. Otherwise, please +refer to `spark standalone configuration with GPU support `_. + +Starting from XGBoost 2.1.0, stage-level scheduling is automatically enabled. Therefore, +if you are using Spark standalone cluster version 3.4.0 or higher, we strongly recommend +configuring the ``"spark.task.resource.gpu.amount"`` as a fractional value. This will +enable running multiple tasks in parallel during the ETL phase. An example configuration +would be ``"spark.task.resource.gpu.amount=1/spark.executor.cores"``. However, if you are +using a XGBoost version earlier than 2.1.0 or a Spark standalone cluster version below 3.4.0, +you still need to set ``"spark.task.resource.gpu.amount"`` equal to ``"spark.executor.resource.gpu.amount"``. + +.. note:: + + As of now, the stage-level scheduling feature in XGBoost is limited to the Spark standalone cluster mode. + However, we have plans to expand its compatibility to YARN and Kubernetes once Spark 3.5.1 is officially released. + Assuming that the application main class is "Iris" and the application jar is "iris-1.0.0.jar",` provided below is an instance demonstrating how to submit the xgboost application to an Apache Spark Standalone cluster. @@ -230,9 +246,9 @@ Spark Standalone cluster. --master $master \ --packages com.nvidia:rapids-4-spark_2.12:${rapids_version},ml.dmlc:xgboost4j-gpu_2.12:${xgboost_version},ml.dmlc:xgboost4j-spark-gpu_2.12:${xgboost_version} \ --conf spark.executor.cores=12 \ - --conf spark.task.cpus=12 \ + --conf spark.task.cpus=1 \ --conf spark.executor.resource.gpu.amount=1 \ - --conf spark.task.resource.gpu.amount=1 \ + --conf spark.task.resource.gpu.amount=0.08 \ --conf spark.rapids.sql.csv.read.double.enabled=true \ --conf spark.rapids.sql.hasNans=false \ --conf spark.plugins=com.nvidia.spark.SQLPlugin \ diff --git a/python-package/xgboost/spark/core.py b/python-package/xgboost/spark/core.py index bad3a2382..aa8c5b998 100644 --- a/python-package/xgboost/spark/core.py +++ b/python-package/xgboost/spark/core.py @@ -22,7 +22,7 @@ from typing import ( import numpy as np import pandas as pd -from pyspark import RDD, SparkContext, cloudpickle +from pyspark import RDD, SparkConf, SparkContext, cloudpickle from pyspark.ml import Estimator, Model from pyspark.ml.functions import array_to_vector, vector_to_array from pyspark.ml.linalg import VectorUDT @@ -368,7 +368,10 @@ class _SparkXGBParams( " on GPU." ) - if not (ss.version >= "3.4.0" and _is_standalone_or_localcluster(sc)): + if not ( + ss.version >= "3.4.0" + and _is_standalone_or_localcluster(sc.getConf()) + ): # We will enable stage-level scheduling in spark 3.4.0+ which doesn't # require spark.task.resource.gpu.amount to be set explicitly gpu_per_task = sc.getConf().get("spark.task.resource.gpu.amount") @@ -907,30 +910,27 @@ class _SparkXGBEstimator(Estimator, _SparkXGBParams, MLReadable, MLWritable): return booster_params, train_call_kwargs_params, dmatrix_kwargs - def _skip_stage_level_scheduling(self) -> bool: + def _skip_stage_level_scheduling(self, spark_version: str, conf: SparkConf) -> bool: # pylint: disable=too-many-return-statements """Check if stage-level scheduling is not needed, return true to skip stage-level scheduling""" if self._run_on_gpu(): - ss = _get_spark_session() - sc = ss.sparkContext - - if ss.version < "3.4.0": + if spark_version < "3.4.0": self.logger.info( "Stage-level scheduling in xgboost requires spark version 3.4.0+" ) return True - if not _is_standalone_or_localcluster(sc): + if not _is_standalone_or_localcluster(conf): self.logger.info( "Stage-level scheduling in xgboost requires spark standalone or " "local-cluster mode" ) return True - executor_cores = sc.getConf().get("spark.executor.cores") - executor_gpus = sc.getConf().get("spark.executor.resource.gpu.amount") + executor_cores = conf.get("spark.executor.cores") + executor_gpus = conf.get("spark.executor.resource.gpu.amount") if executor_cores is None or executor_gpus is None: self.logger.info( "Stage-level scheduling in xgboost requires spark.executor.cores, " @@ -955,7 +955,7 @@ class _SparkXGBEstimator(Estimator, _SparkXGBParams, MLReadable, MLWritable): ) return True - task_gpu_amount = sc.getConf().get("spark.task.resource.gpu.amount") + task_gpu_amount = conf.get("spark.task.resource.gpu.amount") if task_gpu_amount is None: # The ETL tasks will not grab a gpu when spark.task.resource.gpu.amount is not set, @@ -975,14 +975,13 @@ class _SparkXGBEstimator(Estimator, _SparkXGBParams, MLReadable, MLWritable): def _try_stage_level_scheduling(self, rdd: RDD) -> RDD: """Try to enable stage-level scheduling""" - - if self._skip_stage_level_scheduling(): + ss = _get_spark_session() + conf = ss.sparkContext.getConf() + if self._skip_stage_level_scheduling(ss.version, conf): return rdd - ss = _get_spark_session() - # executor_cores will not be None - executor_cores = ss.sparkContext.getConf().get("spark.executor.cores") + executor_cores = conf.get("spark.executor.cores") assert executor_cores is not None # Spark-rapids is a project to leverage GPUs to accelerate spark SQL. diff --git a/python-package/xgboost/spark/utils.py b/python-package/xgboost/spark/utils.py index 395865386..805aa5c10 100644 --- a/python-package/xgboost/spark/utils.py +++ b/python-package/xgboost/spark/utils.py @@ -10,7 +10,7 @@ from threading import Thread from typing import Any, Callable, Dict, Optional, Set, Type import pyspark -from pyspark import BarrierTaskContext, SparkContext, SparkFiles, TaskContext +from pyspark import BarrierTaskContext, SparkConf, SparkContext, SparkFiles, TaskContext from pyspark.sql.session import SparkSession from xgboost import Booster, XGBModel, collective @@ -129,8 +129,8 @@ def _is_local(spark_context: SparkContext) -> bool: return spark_context._jsc.sc().isLocal() -def _is_standalone_or_localcluster(spark_context: SparkContext) -> bool: - master = spark_context.getConf().get("spark.master") +def _is_standalone_or_localcluster(conf: SparkConf) -> bool: + master = conf.get("spark.master") return master is not None and ( master.startswith("spark://") or master.startswith("local-cluster") ) diff --git a/tests/test_distributed/test_with_spark/test_spark_local.py b/tests/test_distributed/test_with_spark/test_spark_local.py index 2c5ee3690..406174542 100644 --- a/tests/test_distributed/test_with_spark/test_spark_local.py +++ b/tests/test_distributed/test_with_spark/test_spark_local.py @@ -8,6 +8,7 @@ from typing import Generator, Sequence, Type import numpy as np import pytest +from pyspark import SparkConf import xgboost as xgb from xgboost import testing as tm @@ -932,6 +933,113 @@ class TestPySparkLocal: model_loaded.set_device("cuda") assert model_loaded._run_on_gpu() + def test_skip_stage_level_scheduling(self) -> None: + conf = ( + SparkConf() + .setMaster("spark://foo") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + .set("spark.task.resource.gpu.amount", "0.08") + ) + + classifer_on_cpu = SparkXGBClassifier(use_gpu=False) + classifer_on_gpu = SparkXGBClassifier(use_gpu=True) + + # the correct configurations should not skip stage-level scheduling + assert not classifer_on_gpu._skip_stage_level_scheduling("3.4.0", conf) + + # spark version < 3.4.0 + assert classifer_on_gpu._skip_stage_level_scheduling("3.3.0", conf) + + # not run on GPU + assert classifer_on_cpu._skip_stage_level_scheduling("3.4.0", conf) + + # spark.executor.cores is not set + badConf = ( + SparkConf() + .setMaster("spark://foo") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + .set("spark.task.resource.gpu.amount", "0.08") + ) + assert classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + + # spark.executor.cores=1 + badConf = ( + SparkConf() + .setMaster("spark://foo") + .set("spark.executor.cores", "1") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + .set("spark.task.resource.gpu.amount", "0.08") + ) + assert classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + + # spark.executor.resource.gpu.amount is not set + badConf = ( + SparkConf() + .setMaster("spark://foo") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.task.resource.gpu.amount", "0.08") + ) + assert classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + + # spark.executor.resource.gpu.amount>1 + badConf = ( + SparkConf() + .setMaster("spark://foo") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "2") + .set("spark.task.resource.gpu.amount", "0.08") + ) + assert classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + + # spark.task.resource.gpu.amount is not set + badConf = ( + SparkConf() + .setMaster("spark://foo") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + ) + assert not classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + + # spark.task.resource.gpu.amount=1 + badConf = ( + SparkConf() + .setMaster("spark://foo") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + .set("spark.task.resource.gpu.amount", "1") + ) + assert classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + + # yarn + badConf = ( + SparkConf() + .setMaster("yarn") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + .set("spark.task.resource.gpu.amount", "1") + ) + assert classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + + # k8s + badConf = ( + SparkConf() + .setMaster("k8s://") + .set("spark.executor.cores", "12") + .set("spark.task.cpus", "1") + .set("spark.executor.resource.gpu.amount", "1") + .set("spark.task.resource.gpu.amount", "1") + ) + assert classifer_on_gpu._skip_stage_level_scheduling("3.4.0", badConf) + class XgboostLocalTest(SparkTestCase): def setUp(self): From fedd9674c8441a26b94f9d4061b11a1be4cb0e7c Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 17 Nov 2023 04:29:08 +0800 Subject: [PATCH 013/109] Implement column sampler in CUDA. (#9785) - CUDA implementation. - Extract the broadcasting logic, we will need the context parameter after revamping the collective implementation. - Some changes to the event loop for fixing a deadlock in CI. - Move argsort into algorithms.cuh, add support for cuda stream. --- src/collective/loop.cc | 25 +++-- src/collective/loop.h | 3 + src/common/algorithm.cuh | 91 +++++++++++++++-- src/common/device_helpers.cuh | 82 +++------------ src/common/random.cc | 43 +++++--- src/common/random.cu | 106 ++++++++++++++++++++ src/common/random.h | 76 ++++++++------ src/metric/auc.cc | 15 +-- src/metric/auc.cu | 52 +++++----- src/metric/auc.h | 20 ++-- src/objective/adaptive.cu | 10 +- src/tree/gpu_hist/evaluator.cu | 2 +- src/tree/updater_approx.cc | 6 +- src/tree/updater_colmaker.cc | 13 ++- src/tree/updater_quantile_hist.cc | 9 +- tests/cpp/common/test_algorithm.cu | 8 +- tests/cpp/common/test_hist_util.cu | 3 +- tests/cpp/common/test_random.cc | 91 ++++++++++++----- tests/cpp/tree/hist/test_evaluate_splits.cc | 12 +-- tests/cpp/tree/test_constraints.cc | 12 +-- 20 files changed, 447 insertions(+), 232 deletions(-) create mode 100644 src/common/random.cu diff --git a/src/collective/loop.cc b/src/collective/loop.cc index 10fce0516..5cfb0034d 100644 --- a/src/collective/loop.cc +++ b/src/collective/loop.cc @@ -117,11 +117,14 @@ void Loop::Process() { break; } - auto unlock_notify = [&](bool is_blocking) { + auto unlock_notify = [&](bool is_blocking, bool stop) { if (!is_blocking) { - return; + std::lock_guard guard{mu_}; + stop_ = stop; + } else { + stop_ = stop; + lock.unlock(); } - lock.unlock(); cv_.notify_one(); }; @@ -145,13 +148,14 @@ void Loop::Process() { auto rc = this->EmptyQueue(&qcopy); // Handle error if (!rc.OK()) { + unlock_notify(is_blocking, true); + std::lock_guard guard{rc_lock_}; this->rc_ = std::move(rc); - unlock_notify(is_blocking); return; } CHECK(qcopy.empty()); - unlock_notify(is_blocking); + unlock_notify(is_blocking, false); } } @@ -170,12 +174,21 @@ Result Loop::Stop() { } [[nodiscard]] Result Loop::Block() { + { + std::lock_guard guard{rc_lock_}; + if (!rc_.OK()) { + return std::move(rc_); + } + } this->Submit(Op{Op::kBlock}); { std::unique_lock lock{mu_}; cv_.wait(lock, [this] { return (this->queue_.empty()) || stop_; }); } - return std::move(rc_); + { + std::lock_guard lock{rc_lock_}; + return std::move(rc_); + } } Loop::Loop(std::chrono::seconds timeout) : timeout_{timeout} { diff --git a/src/collective/loop.h b/src/collective/loop.h index 4f5cb12b3..0c1fdcbfe 100644 --- a/src/collective/loop.h +++ b/src/collective/loop.h @@ -42,7 +42,10 @@ class Loop { std::mutex mu_; std::queue queue_; std::chrono::seconds timeout_; + Result rc_; + std::mutex rc_lock_; // lock for transferring error info. + bool stop_{false}; std::exception_ptr curr_exce_{nullptr}; common::Monitor mutable timer_; diff --git a/src/common/algorithm.cuh b/src/common/algorithm.cuh index 53acc65e1..5f0986d5b 100644 --- a/src/common/algorithm.cuh +++ b/src/common/algorithm.cuh @@ -23,8 +23,7 @@ #include "xgboost/logging.h" // CHECK #include "xgboost/span.h" // Span,byte -namespace xgboost { -namespace common { +namespace xgboost::common { namespace detail { // Wrapper around cub sort to define is_decending template group_ptr, template void SegmentedArgSort(Context const *ctx, Span values, Span group_ptr, Span sorted_idx) { + auto cuctx = ctx->CUDACtx(); CHECK_GE(group_ptr.size(), 1ul); std::size_t n_groups = group_ptr.size() - 1; std::size_t bytes = 0; if (per_seg_index) { SegmentedSequence(ctx, group_ptr, sorted_idx); } else { - dh::Iota(sorted_idx); + dh::Iota(sorted_idx, cuctx->Stream()); } dh::TemporaryArray> values_out(values.size()); dh::TemporaryArray> sorted_idx_out(sorted_idx.size()); @@ -141,15 +141,16 @@ void SegmentedArgSort(Context const *ctx, Span values, Span group_ptr, detail::DeviceSegmentedRadixSortPair( nullptr, bytes, values.data(), values_out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), n_groups, group_ptr.data(), - group_ptr.data() + 1, ctx->CUDACtx()->Stream()); + group_ptr.data() + 1, cuctx->Stream()); dh::TemporaryArray temp_storage(bytes); detail::DeviceSegmentedRadixSortPair( temp_storage.data().get(), bytes, values.data(), values_out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), n_groups, group_ptr.data(), - group_ptr.data() + 1, ctx->CUDACtx()->Stream()); + group_ptr.data() + 1, cuctx->Stream()); dh::safe_cuda(cudaMemcpyAsync(sorted_idx.data(), sorted_idx_out.data().get(), - sorted_idx.size_bytes(), cudaMemcpyDeviceToDevice)); + sorted_idx.size_bytes(), cudaMemcpyDeviceToDevice, + cuctx->Stream())); } /** @@ -159,11 +160,12 @@ void SegmentedArgSort(Context const *ctx, Span values, Span group_ptr, template void SegmentedArgMergeSort(Context const *ctx, SegIt seg_begin, SegIt seg_end, ValIt val_begin, ValIt val_end, dh::device_vector *p_sorted_idx) { + auto cuctx = ctx->CUDACtx(); using Tup = thrust::tuple; auto &sorted_idx = *p_sorted_idx; std::size_t n = std::distance(val_begin, val_end); sorted_idx.resize(n); - dh::Iota(dh::ToSpan(sorted_idx)); + dh::Iota(dh::ToSpan(sorted_idx), cuctx->Stream()); dh::device_vector keys(sorted_idx.size()); auto key_it = dh::MakeTransformIterator(thrust::make_counting_iterator(0ul), [=] XGBOOST_DEVICE(std::size_t i) -> Tup { @@ -177,7 +179,7 @@ void SegmentedArgMergeSort(Context const *ctx, SegIt seg_begin, SegIt seg_end, V return thrust::make_tuple(seg_idx, residue); }); thrust::copy(ctx->CUDACtx()->CTP(), key_it, key_it + keys.size(), keys.begin()); - thrust::stable_sort_by_key(ctx->CUDACtx()->TP(), keys.begin(), keys.end(), sorted_idx.begin(), + thrust::stable_sort_by_key(cuctx->TP(), keys.begin(), keys.end(), sorted_idx.begin(), [=] XGBOOST_DEVICE(Tup const &l, Tup const &r) { if (thrust::get<0>(l) != thrust::get<0>(r)) { return thrust::get<0>(l) < thrust::get<0>(r); // segment index @@ -185,6 +187,75 @@ void SegmentedArgMergeSort(Context const *ctx, SegIt seg_begin, SegIt seg_end, V return thrust::get<1>(l) < thrust::get<1>(r); // residue }); } -} // namespace common -} // namespace xgboost + +template +void ArgSort(xgboost::Context const *ctx, xgboost::common::Span keys, + xgboost::common::Span sorted_idx) { + std::size_t bytes = 0; + auto cuctx = ctx->CUDACtx(); + dh::Iota(sorted_idx, cuctx->Stream()); + + using KeyT = typename decltype(keys)::value_type; + using ValueT = std::remove_const_t; + + dh::TemporaryArray out(keys.size()); + cub::DoubleBuffer d_keys(const_cast(keys.data()), out.data().get()); + dh::TemporaryArray sorted_idx_out(sorted_idx.size()); + cub::DoubleBuffer d_values(const_cast(sorted_idx.data()), + sorted_idx_out.data().get()); + + // track https://github.com/NVIDIA/cub/pull/340 for 64bit length support + using OffsetT = std::conditional_t; + CHECK_LE(sorted_idx.size(), std::numeric_limits::max()); + if (accending) { + void *d_temp_storage = nullptr; +#if THRUST_MAJOR_VERSION >= 2 + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + cuctx->Stream()))); +#else + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + nullptr, false))); +#endif + dh::TemporaryArray storage(bytes); + d_temp_storage = storage.data().get(); +#if THRUST_MAJOR_VERSION >= 2 + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + cuctx->Stream()))); +#else + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + nullptr, false))); +#endif + } else { + void *d_temp_storage = nullptr; +#if THRUST_MAJOR_VERSION >= 2 + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + cuctx->Stream()))); +#else + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + nullptr, false))); +#endif + dh::TemporaryArray storage(bytes); + d_temp_storage = storage.data().get(); +#if THRUST_MAJOR_VERSION >= 2 + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + cuctx->Stream()))); +#else + dh::safe_cuda((cub::DispatchRadixSort::Dispatch( + d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, sizeof(KeyT) * 8, false, + nullptr, false))); +#endif + } + + dh::safe_cuda(cudaMemcpyAsync(sorted_idx.data(), sorted_idx_out.data().get(), + sorted_idx.size_bytes(), cudaMemcpyDeviceToDevice, + cuctx->Stream())); +} +} // namespace xgboost::common #endif // XGBOOST_COMMON_ALGORITHM_CUH_ diff --git a/src/common/device_helpers.cuh b/src/common/device_helpers.cuh index 74336ac61..066f8a3e6 100644 --- a/src/common/device_helpers.cuh +++ b/src/common/device_helpers.cuh @@ -313,8 +313,8 @@ inline void LaunchN(size_t n, L lambda) { } template -void Iota(Container array) { - LaunchN(array.size(), [=] __device__(size_t i) { array[i] = i; }); +void Iota(Container array, cudaStream_t stream) { + LaunchN(array.size(), stream, [=] __device__(size_t i) { array[i] = i; }); } namespace detail { @@ -597,6 +597,16 @@ class DoubleBuffer { T *Other() { return buff.Alternate(); } }; +template +xgboost::common::Span LazyResize(xgboost::Context const *ctx, + xgboost::HostDeviceVector *buffer, std::size_t n) { + buffer->SetDevice(ctx->Device()); + if (buffer->Size() < n) { + buffer->Resize(n); + } + return buffer->DeviceSpan().subspan(0, n); +} + /** * \brief Copies device span to std::vector. * @@ -1060,74 +1070,6 @@ void InclusiveSum(InputIteratorT d_in, OutputIteratorT d_out, OffsetT num_items) InclusiveScan(d_in, d_out, cub::Sum(), num_items); } -template -void ArgSort(xgboost::common::Span keys, xgboost::common::Span sorted_idx) { - size_t bytes = 0; - Iota(sorted_idx); - - using KeyT = typename decltype(keys)::value_type; - using ValueT = std::remove_const_t; - - TemporaryArray out(keys.size()); - cub::DoubleBuffer d_keys(const_cast(keys.data()), - out.data().get()); - TemporaryArray sorted_idx_out(sorted_idx.size()); - cub::DoubleBuffer d_values(const_cast(sorted_idx.data()), - sorted_idx_out.data().get()); - - // track https://github.com/NVIDIA/cub/pull/340 for 64bit length support - using OffsetT = std::conditional_t; - CHECK_LE(sorted_idx.size(), std::numeric_limits::max()); - if (accending) { - void *d_temp_storage = nullptr; -#if THRUST_MAJOR_VERSION >= 2 - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr))); -#else - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr, false))); -#endif - TemporaryArray storage(bytes); - d_temp_storage = storage.data().get(); -#if THRUST_MAJOR_VERSION >= 2 - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr))); -#else - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr, false))); -#endif - } else { - void *d_temp_storage = nullptr; -#if THRUST_MAJOR_VERSION >= 2 - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr))); -#else - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr, false))); -#endif - TemporaryArray storage(bytes); - d_temp_storage = storage.data().get(); -#if THRUST_MAJOR_VERSION >= 2 - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr))); -#else - safe_cuda((cub::DispatchRadixSort::Dispatch( - d_temp_storage, bytes, d_keys, d_values, sorted_idx.size(), 0, - sizeof(KeyT) * 8, false, nullptr, false))); -#endif - } - - safe_cuda(cudaMemcpyAsync(sorted_idx.data(), sorted_idx_out.data().get(), - sorted_idx.size_bytes(), cudaMemcpyDeviceToDevice)); -} - class CUDAStreamView; class CUDAEvent { diff --git a/src/common/random.cc b/src/common/random.cc index d0e75729d..e0d1a2255 100644 --- a/src/common/random.cc +++ b/src/common/random.cc @@ -1,32 +1,50 @@ -/*! - * Copyright 2020 by XGBoost Contributors - * \file random.cc +/** + * Copyright 2020-2023, XGBoost Contributors */ #include "random.h" -namespace xgboost { -namespace common { +#include // for sort, max, copy +#include // for shared_ptr + +#include "xgboost/host_device_vector.h" // for HostDeviceVector + +namespace xgboost::common { std::shared_ptr> ColumnSampler::ColSample( std::shared_ptr> p_features, float colsample) { if (colsample == 1.0f) { return p_features; } + + int n = std::max(1, static_cast(colsample * p_features->Size())); + auto p_new_features = std::make_shared>(); + + if (ctx_->IsCUDA()) { +#if defined(XGBOOST_USE_CUDA) + cuda_impl::SampleFeature(ctx_, n, p_features, p_new_features, this->feature_weights_, + &this->weight_buffer_, &this->idx_buffer_, &rng_); + return p_new_features; +#else + AssertGPUSupport(); + return nullptr; +#endif // defined(XGBOOST_USE_CUDA) + } + const auto &features = p_features->HostVector(); CHECK_GT(features.size(), 0); - int n = std::max(1, static_cast(colsample * features.size())); - auto p_new_features = std::make_shared>(); auto &new_features = *p_new_features; - if (feature_weights_.size() != 0) { + if (!feature_weights_.Empty()) { auto const &h_features = p_features->HostVector(); - std::vector weights(h_features.size()); + auto const &h_feature_weight = feature_weights_.ConstHostVector(); + auto &weight = this->weight_buffer_.HostVector(); + weight.resize(h_features.size()); for (size_t i = 0; i < h_features.size(); ++i) { - weights[i] = feature_weights_[h_features[i]]; + weight[i] = h_feature_weight[h_features[i]]; } CHECK(ctx_); new_features.HostVector() = - WeightedSamplingWithoutReplacement(ctx_, p_features->HostVector(), weights, n); + WeightedSamplingWithoutReplacement(ctx_, p_features->HostVector(), weight, n); } else { new_features.Resize(features.size()); std::copy(features.begin(), features.end(), new_features.HostVector().begin()); @@ -36,5 +54,4 @@ std::shared_ptr> ColumnSampler::ColSample( std::sort(new_features.HostVector().begin(), new_features.HostVector().end()); return p_new_features; } -} // namespace common -} // namespace xgboost +} // namespace xgboost::common diff --git a/src/common/random.cu b/src/common/random.cu new file mode 100644 index 000000000..f5811d924 --- /dev/null +++ b/src/common/random.cu @@ -0,0 +1,106 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#include // for shuffle + +#include // for shared_ptr + +#include "algorithm.cuh" // for ArgSort +#include "cuda_context.cuh" // for CUDAContext +#include "device_helpers.cuh" +#include "random.h" +#include "xgboost/base.h" // for bst_feature_t +#include "xgboost/context.h" // for Context +#include "xgboost/host_device_vector.h" // for HostDeviceVector + +namespace xgboost::common::cuda_impl { +// GPU implementation for sampling without replacement, see the CPU version for references. +void WeightedSamplingWithoutReplacement(Context const *ctx, common::Span array, + common::Span weights, + common::Span results, + HostDeviceVector *sorted_idx, + GlobalRandomEngine *grng) { + CUDAContext const *cuctx = ctx->CUDACtx(); + CHECK_EQ(array.size(), weights.size()); + // Sampling keys + dh::caching_device_vector keys(weights.size()); + + auto d_keys = dh::ToSpan(keys); + + auto seed = (*grng)(); + constexpr auto kEps = kRtEps; // avoid CUDA compilation error + thrust::for_each_n(cuctx->CTP(), thrust::make_counting_iterator(0ul), array.size(), + [=] XGBOOST_DEVICE(std::size_t i) { + thrust::default_random_engine rng; + rng.seed(seed); + rng.discard(i); + thrust::uniform_real_distribution dist; + + auto w = std::max(weights[i], kEps); + auto u = dist(rng); + auto k = std::log(u) / w; + d_keys[i] = k; + }); + // Allocate buffer for sorted index. + auto d_idx = dh::LazyResize(ctx, sorted_idx, keys.size()); + + ArgSort(ctx, d_keys, d_idx); + + // Filter the result according to sorted index. + auto it = thrust::make_permutation_iterator(dh::tbegin(array), dh::tbegin(d_idx)); + // |array| == |weights| == |keys| == |sorted_idx| >= |results| + for (auto size : {array.size(), weights.size(), keys.size()}) { + CHECK_EQ(size, d_idx.size()); + } + CHECK_GE(array.size(), results.size()); + thrust::copy_n(cuctx->CTP(), it, results.size(), dh::tbegin(results)); +} + +void SampleFeature(Context const *ctx, bst_feature_t n_features, + std::shared_ptr> p_features, + std::shared_ptr> p_new_features, + HostDeviceVector const &feature_weights, + HostDeviceVector *weight_buffer, + HostDeviceVector *idx_buffer, GlobalRandomEngine *grng) { + CUDAContext const *cuctx = ctx->CUDACtx(); + auto &new_features = *p_new_features; + new_features.SetDevice(ctx->Device()); + p_features->SetDevice(ctx->Device()); + CHECK_LE(n_features, p_features->Size()); + + if (!feature_weights.Empty()) { + CHECK_LE(p_features->Size(), feature_weights.Size()); + idx_buffer->SetDevice(ctx->Device()); + feature_weights.SetDevice(ctx->Device()); + + auto d_old_features = p_features->DeviceSpan(); + auto d_weight_buffer = dh::LazyResize(ctx, weight_buffer, d_old_features.size()); + // Filter weights according to the existing feature index. + auto d_feature_weight = feature_weights.ConstDeviceSpan(); + auto it = thrust::make_permutation_iterator(dh::tcbegin(d_feature_weight), + dh::tcbegin(d_old_features)); + thrust::copy_n(cuctx->CTP(), it, d_old_features.size(), dh::tbegin(d_weight_buffer)); + new_features.Resize(n_features); + WeightedSamplingWithoutReplacement(ctx, d_old_features, d_weight_buffer, + new_features.DeviceSpan(), idx_buffer, grng); + } else { + new_features.Resize(p_features->Size()); + new_features.Copy(*p_features); + auto d_feat = new_features.DeviceSpan(); + thrust::default_random_engine rng; + rng.seed((*grng)()); + thrust::shuffle(cuctx->CTP(), dh::tbegin(d_feat), dh::tend(d_feat), rng); + new_features.Resize(n_features); + } + + auto d_new_features = new_features.DeviceSpan(); + thrust::sort(cuctx->CTP(), dh::tbegin(d_new_features), dh::tend(d_new_features)); +} + +void InitFeatureSet(Context const *ctx, + std::shared_ptr> p_features) { + CUDAContext const *cuctx = ctx->CUDACtx(); + auto d_features = p_features->DeviceSpan(); + thrust::sequence(cuctx->CTP(), dh::tbegin(d_features), dh::tend(d_features), 0); +} +} // namespace xgboost::common::cuda_impl diff --git a/src/common/random.h b/src/common/random.h index 5efdb486d..2a94123a3 100644 --- a/src/common/random.h +++ b/src/common/random.h @@ -1,5 +1,5 @@ -/*! - * Copyright 2015-2020 by Contributors +/** + * Copyright 2015-2020, XGBoost Contributors * \file random.h * \brief Utility related to random. * \author Tianqi Chen @@ -25,8 +25,7 @@ #include "xgboost/context.h" // Context #include "xgboost/host_device_vector.h" -namespace xgboost { -namespace common { +namespace xgboost::common { /*! * \brief Define mt19937 as default type Random Engine. */ @@ -113,6 +112,18 @@ std::vector WeightedSamplingWithoutReplacement(Context const* ctx, std::vecto return results; } +namespace cuda_impl { +void SampleFeature(Context const* ctx, bst_feature_t n_features, + std::shared_ptr> p_features, + std::shared_ptr> p_new_features, + HostDeviceVector const& feature_weights, + HostDeviceVector* weight_buffer, + HostDeviceVector* idx_buffer, GlobalRandomEngine* grng); + +void InitFeatureSet(Context const* ctx, + std::shared_ptr> p_features); +} // namespace cuda_impl + /** * \class ColumnSampler * @@ -123,46 +134,37 @@ std::vector WeightedSamplingWithoutReplacement(Context const* ctx, std::vecto class ColumnSampler { std::shared_ptr> feature_set_tree_; std::map>> feature_set_level_; - std::vector feature_weights_; + HostDeviceVector feature_weights_; float colsample_bylevel_{1.0f}; float colsample_bytree_{1.0f}; float colsample_bynode_{1.0f}; GlobalRandomEngine rng_; Context const* ctx_; + // Used for weighted sampling. + HostDeviceVector idx_buffer_; + HostDeviceVector weight_buffer_; + public: std::shared_ptr> ColSample( std::shared_ptr> p_features, float colsample); /** - * \brief Column sampler constructor. - * \note This constructor manually sets the rng seed + * @brief Column sampler constructor. + * @note This constructor manually sets the rng seed */ - explicit ColumnSampler(uint32_t seed) { - rng_.seed(seed); - } + explicit ColumnSampler(std::uint32_t seed) { rng_.seed(seed); } /** - * \brief Column sampler constructor. - * \note This constructor synchronizes the RNG seed across processes. - */ - ColumnSampler() { - uint32_t seed = common::GlobalRandom()(); - collective::Broadcast(&seed, sizeof(seed), 0); - rng_.seed(seed); - } - - /** - * \brief Initialise this object before use. + * @brief Initialise this object before use. * - * \param num_col - * \param colsample_bynode - * \param colsample_bylevel - * \param colsample_bytree - * \param skip_index_0 (Optional) True to skip index 0. + * @param num_col + * @param colsample_bynode Sampling rate for node. + * @param colsample_bylevel Sampling rate for tree level. + * @param colsample_bytree Sampling rate for tree. */ void Init(Context const* ctx, int64_t num_col, std::vector feature_weights, float colsample_bynode, float colsample_bylevel, float colsample_bytree) { - feature_weights_ = std::move(feature_weights); + feature_weights_.HostVector() = std::move(feature_weights); colsample_bylevel_ = colsample_bylevel; colsample_bytree_ = colsample_bytree; colsample_bynode_ = colsample_bynode; @@ -173,8 +175,17 @@ class ColumnSampler { } Reset(); + feature_set_tree_->SetDevice(ctx->Device()); feature_set_tree_->Resize(num_col); - std::iota(feature_set_tree_->HostVector().begin(), feature_set_tree_->HostVector().end(), 0); + if (ctx->IsCPU()) { + std::iota(feature_set_tree_->HostVector().begin(), feature_set_tree_->HostVector().end(), 0); + } else { +#if defined(XGBOOST_USE_CUDA) + cuda_impl::InitFeatureSet(ctx, feature_set_tree_); +#else + AssertGPUSupport(); +#endif + } feature_set_tree_ = ColSample(feature_set_tree_, colsample_bytree_); } @@ -216,6 +227,11 @@ class ColumnSampler { } }; -} // namespace common -} // namespace xgboost +inline auto MakeColumnSampler(Context const*) { + std::uint32_t seed = common::GlobalRandomEngine()(); + collective::Broadcast(&seed, sizeof(seed), 0); + auto cs = std::make_shared(seed); + return cs; +} +} // namespace xgboost::common #endif // XGBOOST_COMMON_RANDOM_H_ diff --git a/src/metric/auc.cc b/src/metric/auc.cc index 2e5c88174..4a8aa8a4b 100644 --- a/src/metric/auc.cc +++ b/src/metric/auc.cc @@ -360,7 +360,7 @@ class EvalROCAUC : public EvalAUC { common::OptionalWeights{info.weights_.ConstHostSpan()}); } else { std::tie(fp, tp, auc) = - GPUBinaryROCAUC(predts.ConstDeviceSpan(), info, ctx_->Device(), &this->d_cache_); + GPUBinaryROCAUC(ctx_, predts.ConstDeviceSpan(), info, &this->d_cache_); } return std::make_tuple(fp, tp, auc); } @@ -376,8 +376,9 @@ XGBOOST_REGISTER_METRIC(EvalAUC, "auc") .set_body([](const char*) { return new EvalROCAUC(); }); #if !defined(XGBOOST_USE_CUDA) -std::tuple GPUBinaryROCAUC(common::Span, MetaInfo const &, - DeviceOrd, std::shared_ptr *) { +std::tuple GPUBinaryROCAUC(Context const *, common::Span, + MetaInfo const &, + std::shared_ptr *) { common::AssertGPUSupport(); return {}; } @@ -409,8 +410,7 @@ class EvalPRAUC : public EvalAUC { BinaryPRAUC(ctx_, predts.ConstHostSpan(), info.labels.HostView().Slice(linalg::All(), 0), common::OptionalWeights{info.weights_.ConstHostSpan()}); } else { - std::tie(pr, re, auc) = - GPUBinaryPRAUC(predts.ConstDeviceSpan(), info, ctx_->Device(), &this->d_cache_); + std::tie(pr, re, auc) = GPUBinaryPRAUC(ctx_, predts.ConstDeviceSpan(), info, &this->d_cache_); } return std::make_tuple(pr, re, auc); } @@ -453,8 +453,9 @@ XGBOOST_REGISTER_METRIC(AUCPR, "aucpr") .set_body([](char const *) { return new EvalPRAUC{}; }); #if !defined(XGBOOST_USE_CUDA) -std::tuple GPUBinaryPRAUC(common::Span, MetaInfo const &, - DeviceOrd, std::shared_ptr *) { +std::tuple GPUBinaryPRAUC(Context const *, common::Span, + MetaInfo const &, + std::shared_ptr *) { common::AssertGPUSupport(); return {}; } diff --git a/src/metric/auc.cu b/src/metric/auc.cu index 8b8349e1b..4ce10d094 100644 --- a/src/metric/auc.cu +++ b/src/metric/auc.cu @@ -83,13 +83,14 @@ void InitCacheOnce(common::Span predts, std::shared_ptr -std::tuple -GPUBinaryAUC(common::Span predts, MetaInfo const &info, - DeviceOrd device, common::Span d_sorted_idx, - Fn area_fn, std::shared_ptr cache) { - auto labels = info.labels.View(device); +std::tuple GPUBinaryAUC(Context const *ctx, + common::Span predts, + MetaInfo const &info, + common::Span d_sorted_idx, Fn area_fn, + std::shared_ptr cache) { + auto labels = info.labels.View(ctx->Device()); auto weights = info.weights_.ConstDeviceSpan(); - dh::safe_cuda(cudaSetDevice(device.ordinal)); + dh::safe_cuda(cudaSetDevice(ctx->Ordinal())); CHECK_NE(labels.Size(), 0); CHECK_EQ(labels.Size(), predts.size()); @@ -115,7 +116,7 @@ GPUBinaryAUC(common::Span predts, MetaInfo const &info, dh::XGBDeviceAllocator alloc; auto d_unique_idx = dh::ToSpan(cache->unique_idx); - dh::Iota(d_unique_idx); + dh::Iota(d_unique_idx, ctx->CUDACtx()->Stream()); auto uni_key = dh::MakeTransformIterator( thrust::make_counting_iterator(0), @@ -167,8 +168,9 @@ GPUBinaryAUC(common::Span predts, MetaInfo const &info, return std::make_tuple(last.first, last.second, auc); } -std::tuple GPUBinaryROCAUC(common::Span predts, - MetaInfo const &info, DeviceOrd device, +std::tuple GPUBinaryROCAUC(Context const *ctx, + common::Span predts, + MetaInfo const &info, std::shared_ptr *p_cache) { auto &cache = *p_cache; InitCacheOnce(predts, p_cache); @@ -177,10 +179,10 @@ std::tuple GPUBinaryROCAUC(common::Span pre * Create sorted index for each class */ auto d_sorted_idx = dh::ToSpan(cache->sorted_idx); - dh::ArgSort(predts, d_sorted_idx); + common::ArgSort(ctx, predts, d_sorted_idx); // Create lambda to avoid pass function pointer. return GPUBinaryAUC( - predts, info, device, d_sorted_idx, + ctx, predts, info, d_sorted_idx, [] XGBOOST_DEVICE(double x0, double x1, double y0, double y1) -> double { return TrapezoidArea(x0, x1, y0, y1); }, @@ -361,7 +363,7 @@ double GPUMultiClassAUCOVR(Context const *ctx, MetaInfo const &info, */ dh::XGBDeviceAllocator alloc; auto d_unique_idx = dh::ToSpan(cache->unique_idx); - dh::Iota(d_unique_idx); + dh::Iota(d_unique_idx, ctx->CUDACtx()->Stream()); auto uni_key = dh::MakeTransformIterator>( thrust::make_counting_iterator(0), [=] XGBOOST_DEVICE(size_t i) { uint32_t class_id = i / n_samples; @@ -603,8 +605,9 @@ std::pair GPURankingAUC(Context const *ctx, common::Span< return std::make_pair(auc, n_valid); } -std::tuple GPUBinaryPRAUC(common::Span predts, - MetaInfo const &info, DeviceOrd device, +std::tuple GPUBinaryPRAUC(Context const *ctx, + common::Span predts, + MetaInfo const &info, std::shared_ptr *p_cache) { auto& cache = *p_cache; InitCacheOnce(predts, p_cache); @@ -613,9 +616,9 @@ std::tuple GPUBinaryPRAUC(common::Span pred * Create sorted index for each class */ auto d_sorted_idx = dh::ToSpan(cache->sorted_idx); - dh::ArgSort(predts, d_sorted_idx); + common::ArgSort(ctx, predts, d_sorted_idx); - auto labels = info.labels.View(device); + auto labels = info.labels.View(ctx->Device()); auto d_weights = info.weights_.ConstDeviceSpan(); auto get_weight = common::OptionalWeights{d_weights}; auto it = dh::MakeTransformIterator( @@ -639,7 +642,7 @@ std::tuple GPUBinaryPRAUC(common::Span pred return detail::CalcDeltaPRAUC(fp_prev, fp, tp_prev, tp, total_pos); }; double fp, tp, auc; - std::tie(fp, tp, auc) = GPUBinaryAUC(predts, info, device, d_sorted_idx, fn, cache); + std::tie(fp, tp, auc) = GPUBinaryAUC(ctx, predts, info, d_sorted_idx, fn, cache); return std::make_tuple(1.0, 1.0, auc); } @@ -699,16 +702,17 @@ double GPUMultiClassPRAUC(Context const *ctx, common::Span predts, } template -std::pair -GPURankingPRAUCImpl(common::Span predts, MetaInfo const &info, - common::Span d_group_ptr, DeviceOrd device, - std::shared_ptr cache, Fn area_fn) { +std::pair GPURankingPRAUCImpl(Context const *ctx, + common::Span predts, + MetaInfo const &info, + common::Span d_group_ptr, + std::shared_ptr cache, Fn area_fn) { /** * Sorted idx */ auto d_sorted_idx = dh::ToSpan(cache->sorted_idx); - auto labels = info.labels.View(device); + auto labels = info.labels.View(ctx->Device()); auto weights = info.weights_.ConstDeviceSpan(); uint32_t n_groups = static_cast(info.group_ptr_.size() - 1); @@ -739,7 +743,7 @@ GPURankingPRAUCImpl(common::Span predts, MetaInfo const &info, */ dh::XGBDeviceAllocator alloc; auto d_unique_idx = dh::ToSpan(cache->unique_idx); - dh::Iota(d_unique_idx); + dh::Iota(d_unique_idx, ctx->CUDACtx()->Stream()); auto uni_key = dh::MakeTransformIterator>( thrust::make_counting_iterator(0), [=] XGBOOST_DEVICE(size_t i) { auto idx = d_sorted_idx[i]; @@ -882,7 +886,7 @@ std::pair GPURankingPRAUC(Context const *ctx, return detail::CalcDeltaPRAUC(fp_prev, fp, tp_prev, tp, d_totals[group_id].first); }; - return GPURankingPRAUCImpl(predts, info, d_group_ptr, ctx->Device(), cache, fn); + return GPURankingPRAUCImpl(ctx, predts, info, d_group_ptr, cache, fn); } } // namespace metric } // namespace xgboost diff --git a/src/metric/auc.h b/src/metric/auc.h index fce1cc757..4fe2ecec4 100644 --- a/src/metric/auc.h +++ b/src/metric/auc.h @@ -1,5 +1,5 @@ -/*! - * Copyright 2021 by XGBoost Contributors +/** + * Copyright 2021-2023, XGBoost Contributors */ #ifndef XGBOOST_METRIC_AUC_H_ #define XGBOOST_METRIC_AUC_H_ @@ -18,8 +18,7 @@ #include "xgboost/metric.h" #include "xgboost/span.h" -namespace xgboost { -namespace metric { +namespace xgboost::metric { /*********** * ROC AUC * ***********/ @@ -29,8 +28,9 @@ XGBOOST_DEVICE inline double TrapezoidArea(double x0, double x1, double y0, doub struct DeviceAUCCache; -std::tuple GPUBinaryROCAUC(common::Span predts, - MetaInfo const &info, DeviceOrd, +std::tuple GPUBinaryROCAUC(Context const *ctx, + common::Span predts, + MetaInfo const &info, std::shared_ptr *p_cache); double GPUMultiClassROCAUC(Context const *ctx, common::Span predts, @@ -44,8 +44,9 @@ std::pair GPURankingAUC(Context const *ctx, common::Span< /********** * PR AUC * **********/ -std::tuple GPUBinaryPRAUC(common::Span predts, - MetaInfo const &info, DeviceOrd, +std::tuple GPUBinaryPRAUC(Context const *ctx, + common::Span predts, + MetaInfo const &info, std::shared_ptr *p_cache); double GPUMultiClassPRAUC(Context const *ctx, common::Span predts, @@ -111,6 +112,5 @@ struct PRAUCLabelInvalid { inline void InvalidLabels() { LOG(FATAL) << "PR-AUC supports only binary relevance for learning to rank."; } -} // namespace metric -} // namespace xgboost +} // namespace xgboost::metric #endif // XGBOOST_METRIC_AUC_H_ diff --git a/src/objective/adaptive.cu b/src/objective/adaptive.cu index cea211622..07644146b 100644 --- a/src/objective/adaptive.cu +++ b/src/objective/adaptive.cu @@ -13,9 +13,7 @@ #include "adaptive.h" #include "xgboost/context.h" -namespace xgboost { -namespace obj { -namespace detail { +namespace xgboost::obj::detail { void EncodeTreeLeafDevice(Context const* ctx, common::Span position, dh::device_vector* p_ridx, HostDeviceVector* p_nptr, HostDeviceVector* p_nidx, RegTree const& tree) { @@ -28,7 +26,7 @@ void EncodeTreeLeafDevice(Context const* ctx, common::Span pos position.size_bytes(), cudaMemcpyDeviceToDevice, cuctx->Stream())); p_ridx->resize(position.size()); - dh::Iota(dh::ToSpan(*p_ridx)); + dh::Iota(dh::ToSpan(*p_ridx), cuctx->Stream()); // sort row index according to node index thrust::stable_sort_by_key(cuctx->TP(), sorted_position.begin(), sorted_position.begin() + n_samples, p_ridx->begin()); @@ -190,6 +188,4 @@ void UpdateTreeLeafDevice(Context const* ctx, common::Span pos }); UpdateLeafValues(&quantiles.HostVector(), nidx.ConstHostVector(), info, learning_rate, p_tree); } -} // namespace detail -} // namespace obj -} // namespace xgboost +} // namespace xgboost::obj::detail diff --git a/src/tree/gpu_hist/evaluator.cu b/src/tree/gpu_hist/evaluator.cu index f862e048e..6eed74c56 100644 --- a/src/tree/gpu_hist/evaluator.cu +++ b/src/tree/gpu_hist/evaluator.cu @@ -72,7 +72,7 @@ common::Span GPUHistEvaluator::SortHistogram( TreeEvaluator::SplitEvaluator evaluator) { dh::XGBCachingDeviceAllocator alloc; auto sorted_idx = this->SortedIdx(d_inputs.size(), shared_inputs.feature_values.size()); - dh::Iota(sorted_idx); + dh::Iota(sorted_idx, dh::DefaultStream()); auto data = this->SortInput(d_inputs.size(), shared_inputs.feature_values.size()); auto it = thrust::make_counting_iterator(0u); auto d_feature_idx = dh::ToSpan(feature_idx_); diff --git a/src/tree/updater_approx.cc b/src/tree/updater_approx.cc index 3c37556e1..94e7547ee 100644 --- a/src/tree/updater_approx.cc +++ b/src/tree/updater_approx.cc @@ -248,8 +248,7 @@ class GlobalApproxUpdater : public TreeUpdater { std::unique_ptr pimpl_; // pointer to the last DMatrix, used for update prediction cache. DMatrix *cached_{nullptr}; - std::shared_ptr column_sampler_ = - std::make_shared(); + std::shared_ptr column_sampler_; ObjInfo const *task_; HistMakerTrainParam hist_param_; @@ -284,6 +283,9 @@ class GlobalApproxUpdater : public TreeUpdater { common::Span> out_position, const std::vector &trees) override { CHECK(hist_param_.GetInitialised()); + if (!column_sampler_) { + column_sampler_ = common::MakeColumnSampler(ctx_); + } pimpl_ = std::make_unique(param, &hist_param_, m->Info(), ctx_, column_sampler_, task_, &monitor_); diff --git a/src/tree/updater_colmaker.cc b/src/tree/updater_colmaker.cc index 7a88bd30e..e366811f7 100644 --- a/src/tree/updater_colmaker.cc +++ b/src/tree/updater_colmaker.cc @@ -225,9 +225,12 @@ class ColMaker: public TreeUpdater { } } { - column_sampler_.Init(ctx_, fmat.Info().num_col_, - fmat.Info().feature_weights.ConstHostVector(), param_.colsample_bynode, - param_.colsample_bylevel, param_.colsample_bytree); + if (!column_sampler_) { + column_sampler_ = common::MakeColumnSampler(ctx_); + } + column_sampler_->Init( + ctx_, fmat.Info().num_col_, fmat.Info().feature_weights.ConstHostVector(), + param_.colsample_bynode, param_.colsample_bylevel, param_.colsample_bytree); } { // setup temp space for each thread @@ -467,7 +470,7 @@ class ColMaker: public TreeUpdater { RegTree *p_tree) { auto evaluator = tree_evaluator_.GetEvaluator(); - auto feat_set = column_sampler_.GetFeatureSet(depth); + auto feat_set = column_sampler_->GetFeatureSet(depth); for (const auto &batch : p_fmat->GetBatches(ctx_)) { this->UpdateSolution(batch, feat_set->HostVector(), gpair, p_fmat); } @@ -586,7 +589,7 @@ class ColMaker: public TreeUpdater { const ColMakerTrainParam& colmaker_train_param_; // number of omp thread used during training Context const* ctx_; - common::ColumnSampler column_sampler_; + std::shared_ptr column_sampler_; // Instance Data: current node position in the tree of each instance std::vector position_; // PerThread x PerTreeNode: statistics for per thread construction diff --git a/src/tree/updater_quantile_hist.cc b/src/tree/updater_quantile_hist.cc index 375b24cfa..2bb5b0b49 100644 --- a/src/tree/updater_quantile_hist.cc +++ b/src/tree/updater_quantile_hist.cc @@ -1,5 +1,5 @@ /** - * Copyright 2017-2023 by XGBoost Contributors + * Copyright 2017-2023, XGBoost Contributors * \file updater_quantile_hist.cc * \brief use quantized feature values to construct a tree * \author Philip Cho, Tianqi Checn, Egor Smirnov @@ -470,8 +470,7 @@ class HistUpdater { class QuantileHistMaker : public TreeUpdater { std::unique_ptr p_impl_{nullptr}; std::unique_ptr p_mtimpl_{nullptr}; - std::shared_ptr column_sampler_ = - std::make_shared(); + std::shared_ptr column_sampler_; common::Monitor monitor_; ObjInfo const *task_{nullptr}; HistMakerTrainParam hist_param_; @@ -495,6 +494,10 @@ class QuantileHistMaker : public TreeUpdater { void Update(TrainParam const *param, linalg::Matrix *gpair, DMatrix *p_fmat, common::Span> out_position, const std::vector &trees) override { + if (!column_sampler_) { + column_sampler_ = common::MakeColumnSampler(ctx_); + } + if (trees.front()->IsMultiTarget()) { CHECK(hist_param_.GetInitialised()); CHECK(param->monotone_constraints.empty()) << "monotone constraint" << MTNotImplemented(); diff --git a/tests/cpp/common/test_algorithm.cu b/tests/cpp/common/test_algorithm.cu index c36073397..8f857ff50 100644 --- a/tests/cpp/common/test_algorithm.cu +++ b/tests/cpp/common/test_algorithm.cu @@ -57,13 +57,13 @@ TEST(Algorithm, GpuArgSort) { auto ctx = MakeCUDACtx(0); dh::device_vector values(20); - dh::Iota(dh::ToSpan(values)); // accending + dh::Iota(dh::ToSpan(values), ctx.CUDACtx()->Stream()); // accending dh::device_vector sorted_idx(20); - dh::ArgSort(dh::ToSpan(values), dh::ToSpan(sorted_idx)); // sort to descending - ASSERT_TRUE(thrust::is_sorted(thrust::device, sorted_idx.begin(), sorted_idx.end(), + ArgSort(&ctx, dh::ToSpan(values), dh::ToSpan(sorted_idx)); // sort to descending + ASSERT_TRUE(thrust::is_sorted(ctx.CUDACtx()->CTP(), sorted_idx.begin(), sorted_idx.end(), thrust::greater{})); - dh::Iota(dh::ToSpan(values)); + dh::Iota(dh::ToSpan(values), ctx.CUDACtx()->Stream()); dh::device_vector groups(3); groups[0] = 0; groups[1] = 10; diff --git a/tests/cpp/common/test_hist_util.cu b/tests/cpp/common/test_hist_util.cu index 92d8ff753..c0d5c5ddc 100644 --- a/tests/cpp/common/test_hist_util.cu +++ b/tests/cpp/common/test_hist_util.cu @@ -16,6 +16,7 @@ #include // for vector #include "../../../include/xgboost/logging.h" +#include "../../../src/common/cuda_context.cuh" #include "../../../src/common/device_helpers.cuh" #include "../../../src/common/hist_util.cuh" #include "../../../src/common/hist_util.h" @@ -211,7 +212,7 @@ TEST(HistUtil, RemoveDuplicatedCategories) { cuts_ptr.SetDevice(DeviceOrd::CUDA(0)); dh::device_vector weight(n_samples * n_features, 0); - dh::Iota(dh::ToSpan(weight)); + dh::Iota(dh::ToSpan(weight), ctx.CUDACtx()->Stream()); dh::caching_device_vector columns_ptr(4); for (std::size_t i = 0; i < columns_ptr.size(); ++i) { diff --git a/tests/cpp/common/test_random.cc b/tests/cpp/common/test_random.cc index e2ecd0990..a51776475 100644 --- a/tests/cpp/common/test_random.cc +++ b/tests/cpp/common/test_random.cc @@ -1,19 +1,20 @@ -#include +/** + * Copyright 2018-2023, XGBoost Contributors + */ #include "../../../src/common/random.h" #include "../helpers.h" #include "gtest/gtest.h" -#include "xgboost/context.h" // Context +#include "xgboost/context.h" // for Context -namespace xgboost { -namespace common { -TEST(ColumnSampler, Test) { - Context ctx; +namespace xgboost::common { +namespace { +void TestBasic(Context const* ctx) { int n = 128; - ColumnSampler cs; + ColumnSampler cs{1u}; std::vector feature_weights; // No node sampling - cs.Init(&ctx, n, feature_weights, 1.0f, 0.5f, 0.5f); + cs.Init(ctx, n, feature_weights, 1.0f, 0.5f, 0.5f); auto set0 = cs.GetFeatureSet(0); ASSERT_EQ(set0->Size(), 32); @@ -26,7 +27,7 @@ TEST(ColumnSampler, Test) { ASSERT_EQ(set2->Size(), 32); // Node sampling - cs.Init(&ctx, n, feature_weights, 0.5f, 1.0f, 0.5f); + cs.Init(ctx, n, feature_weights, 0.5f, 1.0f, 0.5f); auto set3 = cs.GetFeatureSet(0); ASSERT_EQ(set3->Size(), 32); @@ -36,21 +37,33 @@ TEST(ColumnSampler, Test) { ASSERT_EQ(set4->Size(), 32); // No level or node sampling, should be the same at different depth - cs.Init(&ctx, n, feature_weights, 1.0f, 1.0f, 0.5f); - ASSERT_EQ(cs.GetFeatureSet(0)->HostVector(), - cs.GetFeatureSet(1)->HostVector()); + cs.Init(ctx, n, feature_weights, 1.0f, 1.0f, 0.5f); + ASSERT_EQ(cs.GetFeatureSet(0)->HostVector(), cs.GetFeatureSet(1)->HostVector()); - cs.Init(&ctx, n, feature_weights, 1.0f, 1.0f, 1.0f); + cs.Init(ctx, n, feature_weights, 1.0f, 1.0f, 1.0f); auto set5 = cs.GetFeatureSet(0); ASSERT_EQ(set5->Size(), n); - cs.Init(&ctx, n, feature_weights, 1.0f, 1.0f, 1.0f); + cs.Init(ctx, n, feature_weights, 1.0f, 1.0f, 1.0f); auto set6 = cs.GetFeatureSet(0); ASSERT_EQ(set5->HostVector(), set6->HostVector()); // Should always be a minimum of one feature - cs.Init(&ctx, n, feature_weights, 1e-16f, 1e-16f, 1e-16f); + cs.Init(ctx, n, feature_weights, 1e-16f, 1e-16f, 1e-16f); ASSERT_EQ(cs.GetFeatureSet(0)->Size(), 1); } +} // namespace + +TEST(ColumnSampler, Test) { + Context ctx; + TestBasic(&ctx); +} + +#if defined(XGBOOST_USE_CUDA) +TEST(ColumnSampler, GPUTest) { + auto ctx = MakeCUDACtx(0); + TestBasic(&ctx); +} +#endif // defined(XGBOOST_USE_CUDA) // Test if different threads using the same seed produce the same result TEST(ColumnSampler, ThreadSynchronisation) { @@ -81,16 +94,16 @@ TEST(ColumnSampler, ThreadSynchronisation) { ASSERT_TRUE(success); } -TEST(ColumnSampler, WeightedSampling) { - auto test_basic = [](int first) { - Context ctx; +namespace { +void TestWeightedSampling(Context const* ctx) { + auto test_basic = [ctx](int first) { std::vector feature_weights(2); feature_weights[0] = std::abs(first - 1.0f); feature_weights[1] = first - 0.0f; ColumnSampler cs{0}; - cs.Init(&ctx, 2, feature_weights, 1.0, 1.0, 0.5); + cs.Init(ctx, 2, feature_weights, 1.0, 1.0, 0.5); auto feature_sets = cs.GetFeatureSet(0); - auto const &h_feat_set = feature_sets->HostVector(); + auto const& h_feat_set = feature_sets->HostVector(); ASSERT_EQ(h_feat_set.size(), 1); ASSERT_EQ(h_feat_set[0], first - 0); }; @@ -104,8 +117,7 @@ TEST(ColumnSampler, WeightedSampling) { SimpleRealUniformDistribution dist(.0f, 12.0f); std::generate(feature_weights.begin(), feature_weights.end(), [&]() { return dist(&rng); }); ColumnSampler cs{0}; - Context ctx; - cs.Init(&ctx, kCols, feature_weights, 0.5f, 1.0f, 1.0f); + cs.Init(ctx, kCols, feature_weights, 0.5f, 1.0f, 1.0f); std::vector features(kCols); std::iota(features.begin(), features.end(), 0); std::vector freq(kCols, 0); @@ -131,8 +143,22 @@ TEST(ColumnSampler, WeightedSampling) { EXPECT_NEAR(freq[i], feature_weights[i], 1e-2); } } +} // namespace -TEST(ColumnSampler, WeightedMultiSampling) { +TEST(ColumnSampler, WeightedSampling) { + Context ctx; + TestWeightedSampling(&ctx); +} + +#if defined(XGBOOST_USE_CUDA) +TEST(ColumnSampler, GPUWeightedSampling) { + auto ctx = MakeCUDACtx(0); + TestWeightedSampling(&ctx); +} +#endif // defined(XGBOOST_USE_CUDA) + +namespace { +void TestWeightedMultiSampling(Context const* ctx) { size_t constexpr kCols = 32; std::vector feature_weights(kCols, 0); for (size_t i = 0; i < feature_weights.size(); ++i) { @@ -140,13 +166,24 @@ TEST(ColumnSampler, WeightedMultiSampling) { } ColumnSampler cs{0}; float bytree{0.5}, bylevel{0.5}, bynode{0.5}; - Context ctx; - cs.Init(&ctx, feature_weights.size(), feature_weights, bytree, bylevel, bynode); + cs.Init(ctx, feature_weights.size(), feature_weights, bytree, bylevel, bynode); auto feature_set = cs.GetFeatureSet(0); size_t n_sampled = kCols * bytree * bylevel * bynode; ASSERT_EQ(feature_set->Size(), n_sampled); feature_set = cs.GetFeatureSet(1); ASSERT_EQ(feature_set->Size(), n_sampled); } -} // namespace common -} // namespace xgboost +} // namespace + +TEST(ColumnSampler, WeightedMultiSampling) { + Context ctx; + TestWeightedMultiSampling(&ctx); +} + +#if defined(XGBOOST_USE_CUDA) +TEST(ColumnSampler, GPUWeightedMultiSampling) { + auto ctx = MakeCUDACtx(0); + TestWeightedMultiSampling(&ctx); +} +#endif // defined(XGBOOST_USE_CUDA) +} // namespace xgboost::common diff --git a/tests/cpp/tree/hist/test_evaluate_splits.cc b/tests/cpp/tree/hist/test_evaluate_splits.cc index 78fda5ce5..329379b5b 100644 --- a/tests/cpp/tree/hist/test_evaluate_splits.cc +++ b/tests/cpp/tree/hist/test_evaluate_splits.cc @@ -28,7 +28,7 @@ void TestEvaluateSplits(bool force_read_by_column) { Context ctx; ctx.nthread = 4; int static constexpr kRows = 8, kCols = 16; - auto sampler = std::make_shared(); + auto sampler = std::make_shared(1u); TrainParam param; param.UpdateAllowUnknown(Args{{"min_child_weight", "0"}, {"reg_lambda", "0"}}); @@ -102,7 +102,7 @@ TEST(HistMultiEvaluator, Evaluate) { TrainParam param; param.Init(Args{{"min_child_weight", "0"}, {"reg_lambda", "0"}}); - auto sampler = std::make_shared(); + auto sampler = std::make_shared(1u); std::size_t n_samples = 3; bst_feature_t n_features = 2; @@ -166,7 +166,7 @@ TEST(HistEvaluator, Apply) { TrainParam param; param.UpdateAllowUnknown(Args{{"min_child_weight", "0"}, {"reg_lambda", "0.0"}}); auto dmat = RandomDataGenerator(kNRows, kNCols, 0).Seed(3).GenerateDMatrix(); - auto sampler = std::make_shared(); + auto sampler = std::make_shared(1u); auto evaluator_ = HistEvaluator{&ctx, ¶m, dmat->Info(), sampler}; CPUExpandEntry entry{0, 0}; @@ -194,7 +194,7 @@ TEST_F(TestPartitionBasedSplit, CPUHist) { Context ctx; // check the evaluator is returning the optimal split std::vector ft{FeatureType::kCategorical}; - auto sampler = std::make_shared(); + auto sampler = std::make_shared(1u); HistEvaluator evaluator{&ctx, ¶m_, info_, sampler}; evaluator.InitRoot(GradStats{total_gpair_}); RegTree tree; @@ -224,7 +224,7 @@ auto CompareOneHotAndPartition(bool onehot) { auto dmat = RandomDataGenerator(kRows, kCols, 0).Seed(3).Type(ft).MaxCategory(n_cats).GenerateDMatrix(); - auto sampler = std::make_shared(); + auto sampler = std::make_shared(1u); auto evaluator = HistEvaluator{&ctx, ¶m, dmat->Info(), sampler}; std::vector entries(1); HistMakerTrainParam hist_param; @@ -271,7 +271,7 @@ TEST_F(TestCategoricalSplitWithMissing, HistEvaluator) { ASSERT_EQ(node_hist.size(), feature_histogram_.size()); std::copy(feature_histogram_.cbegin(), feature_histogram_.cend(), node_hist.begin()); - auto sampler = std::make_shared(); + auto sampler = std::make_shared(1u); MetaInfo info; info.num_col_ = 1; info.feature_types = {FeatureType::kCategorical}; diff --git a/tests/cpp/tree/test_constraints.cc b/tests/cpp/tree/test_constraints.cc index 912d608a3..4f810102d 100644 --- a/tests/cpp/tree/test_constraints.cc +++ b/tests/cpp/tree/test_constraints.cc @@ -1,3 +1,6 @@ +/** + * Copyright 2019-2023, XGBoost Contributors + */ #include #include #include @@ -9,9 +12,7 @@ #include "../../../src/tree/hist/evaluate_splits.h" #include "../helpers.h" -namespace xgboost { -namespace tree { - +namespace xgboost::tree { TEST(CPUFeatureInteractionConstraint, Empty) { TrainParam param; param.UpdateAllowUnknown(Args{}); @@ -77,7 +78,7 @@ TEST(CPUMonoConstraint, Basic) { param.UpdateAllowUnknown(Args{{"monotone_constraints", str_mono}}); auto Xy = RandomDataGenerator{kRows, kCols, 0.0}.GenerateDMatrix(true); - auto sampler = std::make_shared(); + auto sampler = std::make_shared(1u); HistEvaluator evalutor{&ctx, ¶m, Xy->Info(), sampler}; evalutor.InitRoot(GradStats{2.0, 2.0}); @@ -90,5 +91,4 @@ TEST(CPUMonoConstraint, Basic) { ASSERT_TRUE(evalutor.Evaluator().has_constraint); } -} // namespace tree -} // namespace xgboost +} // namespace xgboost::tree From 0715ab3c1094651fb371199d1a5be288e62dd748 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 22 Nov 2023 19:27:31 +0800 Subject: [PATCH 014/109] Use `dlopen` to load NCCL. (#9796) This PR adds optional support for loading nccl with `dlopen` as an alternative of compile time linking. This is to address the size bloat issue with the PyPI binary release. - Add CMake option to load `nccl` at runtime. - Add an NCCL stub. After this, `nccl` will be fetched from PyPI when using pip to install XGBoost, either by a user or by `pyproject.toml`. Others who want to link the nccl at compile time can continue to do so without any change. At the moment, this is Linux only since we only support MNMG on Linux. --- CMakeLists.txt | 10 ++ cmake/Utils.cmake | 17 ++- cmake/modules/FindNccl.cmake | 29 +++-- doc/tutorials/dask.rst | 31 +++++ include/xgboost/c_api.h | 2 + include/xgboost/string_view.h | 50 ++++---- jvm-packages/create_jni.py | 1 + plugin/federated/federated_comm.cuh | 6 + plugin/federated/federated_comm.h | 6 +- python-package/packager/build_config.py | 2 + python-package/pyproject.toml | 3 +- python-package/xgboost/collective.py | 30 ++++- python-package/xgboost/core.py | 12 +- src/c_api/c_api.cc | 6 +- src/c_api/c_api.cu | 8 ++ src/collective/coll.cu | 62 +++++----- src/collective/coll.cuh | 3 +- src/collective/comm.cc | 24 ++-- src/collective/comm.cu | 29 +++-- src/collective/comm.cuh | 47 +++++++- src/collective/comm.h | 22 +++- src/collective/comm_group.cc | 15 +-- src/collective/comm_group.h | 6 +- src/collective/communicator.cc | 5 + src/collective/communicator.cu | 6 +- src/collective/communicator.h | 1 + src/collective/nccl_device_communicator.cu | 49 +++++--- src/collective/nccl_device_communicator.cuh | 8 +- src/collective/nccl_stub.cc | 109 ++++++++++++++++++ src/collective/nccl_stub.h | 94 +++++++++++++++ src/common/device_helpers.cuh | 24 ---- tests/buildkite/build-cuda-with-rmm.sh | 17 ++- tests/buildkite/build-cuda.sh | 16 ++- tests/buildkite/test-cpp-gpu.sh | 1 + tests/buildkite/test-cpp-mgpu.sh | 1 + tests/buildkite/test-python-gpu.sh | 3 +- tests/ci_build/Dockerfile.gpu | 5 +- tests/ci_build/Dockerfile.gpu_build_centos7 | 2 +- tests/ci_build/prune_libnccl.sh | 35 ------ tests/ci_build/rename_whl.py | 40 +++---- tests/cpp/collective/test_allgather.cu | 6 +- tests/cpp/collective/test_allreduce.cu | 8 +- .../test_nccl_device_communicator.cu | 13 +-- tests/cpp/collective/test_worker.h | 2 +- .../test_gpu_with_dask/test_gpu_with_dask.py | 60 ++++++++++ 45 files changed, 658 insertions(+), 268 deletions(-) create mode 100644 src/collective/nccl_stub.cc create mode 100644 src/collective/nccl_stub.h delete mode 100755 tests/ci_build/prune_libnccl.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index e93427ed9..bf8f0cf62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,10 @@ option(KEEP_BUILD_ARTIFACTS_IN_BINARY_DIR "Output build artifacts in CMake binar option(USE_CUDA "Build with GPU acceleration" OFF) option(USE_PER_THREAD_DEFAULT_STREAM "Build with per-thread default stream" ON) option(USE_NCCL "Build with NCCL to enable distributed GPU support." OFF) +# This is specifically designed for PyPI binary release and should be disabled for most of the cases. +option(USE_DLOPEN_NCCL "Whether to load nccl dynamically." OFF) option(BUILD_WITH_SHARED_NCCL "Build with shared NCCL library." OFF) + if(USE_CUDA) if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES AND NOT DEFINED ENV{CUDAARCHS}) set(GPU_COMPUTE_VER "" CACHE STRING @@ -80,6 +83,7 @@ if(USE_CUDA) unset(GPU_COMPUTE_VER CACHE) endif() endif() + # CUDA device LTO was introduced in CMake v3.25 and requires host LTO to also be enabled but can still # be explicitly disabled allowing for LTO on host only, host and device, or neither, but device-only LTO # is not a supproted configuration @@ -115,6 +119,12 @@ endif() if(BUILD_WITH_SHARED_NCCL AND (NOT USE_NCCL)) message(SEND_ERROR "Build XGBoost with -DUSE_NCCL=ON to enable BUILD_WITH_SHARED_NCCL.") endif() +if(USE_DLOPEN_NCCL AND (NOT USE_NCCL)) + message(SEND_ERROR "Build XGBoost with -DUSE_NCCL=ON to enable USE_DLOPEN_NCCL.") +endif() +if(USE_DLOPEN_NCCL AND (NOT (CMAKE_SYSTEM_NAME STREQUAL "Linux"))) + message(SEND_ERROR "`USE_DLOPEN_NCCL` supports only Linux at the moment.") +endif() if(JVM_BINDINGS AND R_LIB) message(SEND_ERROR "`R_LIB' is not compatible with `JVM_BINDINGS' as they both have customized configurations.") endif() diff --git a/cmake/Utils.cmake b/cmake/Utils.cmake index eafd829fc..9c373bb01 100644 --- a/cmake/Utils.cmake +++ b/cmake/Utils.cmake @@ -171,17 +171,24 @@ function(xgboost_set_cuda_flags target) endif() endfunction() -macro(xgboost_link_nccl target) +function(xgboost_link_nccl target) + set(xgboost_nccl_flags -DXGBOOST_USE_NCCL=1) + if(USE_DLOPEN_NCCL) + list(APPEND xgboost_nccl_flags -DXGBOOST_USE_DLOPEN_NCCL=1) + endif() + if(BUILD_STATIC_LIB) target_include_directories(${target} PUBLIC ${NCCL_INCLUDE_DIR}) - target_compile_definitions(${target} PUBLIC -DXGBOOST_USE_NCCL=1) + target_compile_definitions(${target} PUBLIC ${xgboost_nccl_flags}) target_link_libraries(${target} PUBLIC ${NCCL_LIBRARY}) else() target_include_directories(${target} PRIVATE ${NCCL_INCLUDE_DIR}) - target_compile_definitions(${target} PRIVATE -DXGBOOST_USE_NCCL=1) - target_link_libraries(${target} PRIVATE ${NCCL_LIBRARY}) + target_compile_definitions(${target} PRIVATE ${xgboost_nccl_flags}) + if(NOT USE_DLOPEN_NCCL) + target_link_libraries(${target} PRIVATE ${NCCL_LIBRARY}) + endif() endif() -endmacro() +endfunction() # compile options macro(xgboost_target_properties target) diff --git a/cmake/modules/FindNccl.cmake b/cmake/modules/FindNccl.cmake index 02ee731a1..fa3ed0866 100644 --- a/cmake/modules/FindNccl.cmake +++ b/cmake/modules/FindNccl.cmake @@ -54,17 +54,24 @@ find_path(NCCL_INCLUDE_DIR NAMES nccl.h HINTS ${NCCL_ROOT}/include $ENV{NCCL_ROOT}/include) -find_library(NCCL_LIBRARY - NAMES ${NCCL_LIB_NAME} - HINTS ${NCCL_ROOT}/lib $ENV{NCCL_ROOT}/lib/) +if(USE_DLOPEN_NCCL) + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Nccl DEFAULT_MSG NCCL_INCLUDE_DIR) -message(STATUS "Using nccl library: ${NCCL_LIBRARY}") + mark_as_advanced(NCCL_INCLUDE_DIR) +else() + find_library(NCCL_LIBRARY + NAMES ${NCCL_LIB_NAME} + HINTS ${NCCL_ROOT}/lib $ENV{NCCL_ROOT}/lib/) -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Nccl DEFAULT_MSG - NCCL_INCLUDE_DIR NCCL_LIBRARY) + message(STATUS "Using nccl library: ${NCCL_LIBRARY}") -mark_as_advanced( - NCCL_INCLUDE_DIR - NCCL_LIBRARY -) + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Nccl DEFAULT_MSG + NCCL_INCLUDE_DIR NCCL_LIBRARY) + + mark_as_advanced( + NCCL_INCLUDE_DIR + NCCL_LIBRARY + ) +endif() diff --git a/doc/tutorials/dask.rst b/doc/tutorials/dask.rst index 148230fe6..4b145f9a9 100644 --- a/doc/tutorials/dask.rst +++ b/doc/tutorials/dask.rst @@ -536,6 +536,37 @@ Troubleshooting - MIG (Multi-Instance GPU) is not yet supported by NCCL. You will receive an error message that includes `Multiple processes within a communication group ...` upon initialization. +.. _nccl-load: + +- Starting from version 2.1.0, to reduce the size of the binary wheel, the XGBoost package + (installed using pip) loads NCCL from the environment instead of bundling it + directly. This means that if you encounter an error message like + "Failed to load nccl ...", it indicates that NCCL is not installed or properly + configured in your environment. + + To resolve this issue, you can install NCCL using pip: + + .. code-block:: sh + + pip install nvidia-nccl-cu12 # (or with any compatible CUDA version) + + The default conda installation of XGBoost should not encounter this error. If you are + using a customized XGBoost, please make sure one of the followings is true: + + + XGBoost is NOT compiled with the `USE_DLOPEN_NCCL` flag. + + The `dmlc_nccl_path` parameter is set to full NCCL path when initializing the collective. + + Here are some additional tips for troubleshooting NCCL dependency issues: + + + Check the NCCL installation path and verify that it's installed correctly. We try to + find NCCL by using ``from nvidia.nccl import lib`` in Python when XGBoost is installed + using pip. + + Ensure that you have the correct CUDA version installed. NCCL requires a compatible + CUDA version to function properly. + + If you are not using distributed training with XGBoost and yet see this error, please + open an issue on GitHub. + + If you continue to encounter NCCL dependency issues, please open an issue on GitHub. + ************ IPv6 Support ************ diff --git a/include/xgboost/c_api.h b/include/xgboost/c_api.h index ffa3a6c79..59d4d0881 100644 --- a/include/xgboost/c_api.h +++ b/include/xgboost/c_api.h @@ -1613,6 +1613,8 @@ XGB_DLL int XGTrackerFree(TrackerHandle handle); * - DMLC_TRACKER_PORT: Port number of the tracker. * - DMLC_TASK_ID: ID of the current task, can be used to obtain deterministic rank assignment. * - DMLC_WORKER_CONNECT_RETRY: Number of retries to connect to the tracker. + * - dmlc_nccl_path: The path to NCCL shared object. Only used if XGBoost is compiled with + * `USE_DLOPEN_NCCL`. * Only applicable to the Federated communicator (use upper case for environment variables, use * lower case for runtime configuration): * - federated_server_address: Address of the federated server. diff --git a/include/xgboost/string_view.h b/include/xgboost/string_view.h index ba0d9f368..463558363 100644 --- a/include/xgboost/string_view.h +++ b/include/xgboost/string_view.h @@ -1,23 +1,24 @@ /** - * Copyright 2021-2023 by XGBoost Contributors + * Copyright 2021-2023, XGBoost Contributors */ #ifndef XGBOOST_STRING_VIEW_H_ #define XGBOOST_STRING_VIEW_H_ #include // CHECK_LT #include // Span -#include // std::equal,std::min -#include // std::reverse_iterator -#include // std::ostream -#include // std::char_traits,std::string +#include // for equal, min +#include // for size_t +#include // for reverse_iterator +#include // for ostream +#include // for char_traits, string namespace xgboost { struct StringView { private: - using CharT = char; // unsigned char + using CharT = char; using Traits = std::char_traits; CharT const* str_{nullptr}; - size_t size_{0}; + std::size_t size_{0}; public: using value_type = CharT; // NOLINT @@ -28,40 +29,41 @@ struct StringView { public: constexpr StringView() = default; - constexpr StringView(CharT const* str, std::size_t size) : str_{str}, size_{size} {} + constexpr StringView(value_type const* str, std::size_t size) : str_{str}, size_{size} {} StringView(std::string const& str) : str_{str.c_str()}, size_{str.size()} {} // NOLINT - constexpr StringView(CharT const* str) // NOLINT + constexpr StringView(value_type const* str) // NOLINT : str_{str}, size_{str == nullptr ? 0ul : Traits::length(str)} {} - CharT const& operator[](size_t p) const { return str_[p]; } - CharT const& at(size_t p) const { // NOLINT + [[nodiscard]] value_type const& operator[](std::size_t p) const { return str_[p]; } + [[nodiscard]] explicit operator std::string() const { return {this->c_str(), this->size()}; } + [[nodiscard]] value_type const& at(std::size_t p) const { // NOLINT CHECK_LT(p, size_); return str_[p]; } - constexpr std::size_t size() const { return size_; } // NOLINT - constexpr bool empty() const { return size() == 0; } // NOLINT - StringView substr(size_t beg, size_t n) const { // NOLINT + [[nodiscard]] constexpr std::size_t size() const { return size_; } // NOLINT + [[nodiscard]] constexpr bool empty() const { return size() == 0; } // NOLINT + [[nodiscard]] StringView substr(std::size_t beg, std::size_t n) const { // NOLINT CHECK_LE(beg, size_); - size_t len = std::min(n, size_ - beg); + std::size_t len = std::min(n, size_ - beg); return {str_ + beg, len}; } - CharT const* c_str() const { return str_; } // NOLINT + [[nodiscard]] value_type const* c_str() const { return str_; } // NOLINT - constexpr CharT const* cbegin() const { return str_; } // NOLINT - constexpr CharT const* cend() const { return str_ + size(); } // NOLINT - constexpr CharT const* begin() const { return str_; } // NOLINT - constexpr CharT const* end() const { return str_ + size(); } // NOLINT + [[nodiscard]] constexpr const_iterator cbegin() const { return str_; } // NOLINT + [[nodiscard]] constexpr const_iterator cend() const { return str_ + size(); } // NOLINT + [[nodiscard]] constexpr iterator begin() const { return str_; } // NOLINT + [[nodiscard]] constexpr iterator end() const { return str_ + size(); } // NOLINT - const_reverse_iterator rbegin() const noexcept { // NOLINT + [[nodiscard]] const_reverse_iterator rbegin() const noexcept { // NOLINT return const_reverse_iterator(this->end()); } - const_reverse_iterator crbegin() const noexcept { // NOLINT + [[nodiscard]] const_reverse_iterator crbegin() const noexcept { // NOLINT return const_reverse_iterator(this->end()); } - const_reverse_iterator rend() const noexcept { // NOLINT + [[nodiscard]] const_reverse_iterator rend() const noexcept { // NOLINT return const_reverse_iterator(this->begin()); } - const_reverse_iterator crend() const noexcept { // NOLINT + [[nodiscard]] const_reverse_iterator crend() const noexcept { // NOLINT return const_reverse_iterator(this->begin()); } }; diff --git a/jvm-packages/create_jni.py b/jvm-packages/create_jni.py index 18908fc1c..3692cb13c 100755 --- a/jvm-packages/create_jni.py +++ b/jvm-packages/create_jni.py @@ -103,6 +103,7 @@ if __name__ == "__main__": if cli_args.use_cuda == 'ON': CONFIG['USE_CUDA'] = 'ON' CONFIG['USE_NCCL'] = 'ON' + CONFIG["USE_DLOPEN_NCCL"] = "OFF" args = ["-D{0}:BOOL={1}".format(k, v) for k, v in CONFIG.items()] diff --git a/plugin/federated/federated_comm.cuh b/plugin/federated/federated_comm.cuh index df9127644..58c52f67e 100644 --- a/plugin/federated/federated_comm.cuh +++ b/plugin/federated/federated_comm.cuh @@ -5,9 +5,11 @@ #include // for shared_ptr +#include "../../src/collective/coll.h" // for Coll #include "../../src/common/device_helpers.cuh" // for CUDAStreamView #include "federated_comm.h" // for FederatedComm #include "xgboost/context.h" // for Context +#include "xgboost/logging.h" namespace xgboost::collective { class CUDAFederatedComm : public FederatedComm { @@ -16,5 +18,9 @@ class CUDAFederatedComm : public FederatedComm { public: explicit CUDAFederatedComm(Context const* ctx, std::shared_ptr impl); [[nodiscard]] auto Stream() const { return stream_; } + Comm* MakeCUDAVar(Context const*, std::shared_ptr) const override { + LOG(FATAL) << "[Internal Error]: Invalid request for CUDA variant."; + return nullptr; + } }; } // namespace xgboost::collective diff --git a/plugin/federated/federated_comm.h b/plugin/federated/federated_comm.h index a24798626..750d94abd 100644 --- a/plugin/federated/federated_comm.h +++ b/plugin/federated/federated_comm.h @@ -10,12 +10,12 @@ #include // for unique_ptr #include // for string -#include "../../src/collective/comm.h" // for Comm +#include "../../src/collective/comm.h" // for HostComm #include "../../src/common/json_utils.h" // for OptionalArg #include "xgboost/json.h" namespace xgboost::collective { -class FederatedComm : public Comm { +class FederatedComm : public HostComm { std::shared_ptr stub_; void Init(std::string const& host, std::int32_t port, std::int32_t world, std::int32_t rank, @@ -64,6 +64,6 @@ class FederatedComm : public Comm { [[nodiscard]] bool IsFederated() const override { return true; } [[nodiscard]] federated::Federated::Stub* Handle() const { return stub_.get(); } - Comm* MakeCUDAVar(Context const* ctx, std::shared_ptr pimpl) const override; + [[nodiscard]] Comm* MakeCUDAVar(Context const* ctx, std::shared_ptr pimpl) const override; }; } // namespace xgboost::collective diff --git a/python-package/packager/build_config.py b/python-package/packager/build_config.py index 26392a897..d3733d628 100644 --- a/python-package/packager/build_config.py +++ b/python-package/packager/build_config.py @@ -15,6 +15,8 @@ class BuildConfiguration: # pylint: disable=R0902 use_cuda: bool = False # Whether to enable NCCL use_nccl: bool = False + # Whether to load nccl dynamically + use_dlopen_nccl: bool = False # Whether to enable HDFS use_hdfs: bool = False # Whether to enable Azure Storage diff --git a/python-package/pyproject.toml b/python-package/pyproject.toml index 199e0f06c..3bd642cc7 100644 --- a/python-package/pyproject.toml +++ b/python-package/pyproject.toml @@ -29,7 +29,8 @@ classifiers = [ ] dependencies = [ "numpy", - "scipy" + "scipy", + "nvidia-nccl-cu12 ; platform_system == 'Linux' and platform_machine != 'aarch64'" ] [project.urls] diff --git a/python-package/xgboost/collective.py b/python-package/xgboost/collective.py index 4c67ccbfc..4eb5ea2ab 100644 --- a/python-package/xgboost/collective.py +++ b/python-package/xgboost/collective.py @@ -2,14 +2,15 @@ import ctypes import json import logging +import os import pickle from enum import IntEnum, unique -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import numpy as np from ._typing import _T -from .core import _LIB, _check_call, c_str, from_pystr_to_cstr, py_str +from .core import _LIB, _check_call, build_info, c_str, from_pystr_to_cstr, py_str LOGGER = logging.getLogger("[xgboost.collective]") @@ -250,6 +251,31 @@ class CommunicatorContext: def __init__(self, **args: Any) -> None: self.args = args + key = "dmlc_nccl_path" + if args.get(key, None) is not None: + return + + binfo = build_info() + if not binfo["USE_DLOPEN_NCCL"]: + return + + try: + # PyPI package of NCCL. + from nvidia.nccl import lib + + # There are two versions of nvidia-nccl, one is from PyPI, another one from + # nvidia-pyindex. We support only the first one as the second one is too old + # (2.9.8 as of writing). + if lib.__file__ is not None: + dirname: Optional[str] = os.path.dirname(lib.__file__) + else: + dirname = None + + if dirname: + path = os.path.join(dirname, "libnccl.so.2") + self.args[key] = path + except ImportError: + pass def __enter__(self) -> Dict[str, Any]: init(**self.args) diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 648851b31..bfc94aa04 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -184,6 +184,13 @@ def _py_version() -> str: return f.read().strip() +def _register_log_callback(lib: ctypes.CDLL) -> None: + lib.XGBGetLastError.restype = ctypes.c_char_p + lib.callback = _get_log_callback_func() # type: ignore + if lib.XGBRegisterLogCallback(lib.callback) != 0: + raise XGBoostError(lib.XGBGetLastError()) + + def _load_lib() -> ctypes.CDLL: """Load xgboost Library.""" lib_paths = find_lib_path() @@ -228,10 +235,7 @@ Likely causes: Error message(s): {os_error_list} """ ) - lib.XGBGetLastError.restype = ctypes.c_char_p - lib.callback = _get_log_callback_func() # type: ignore - if lib.XGBRegisterLogCallback(lib.callback) != 0: - raise XGBoostError(lib.XGBGetLastError()) + _register_log_callback(lib) def parse(ver: str) -> Tuple[int, int, int]: """Avoid dependency on packaging (PEP 440).""" diff --git a/src/c_api/c_api.cc b/src/c_api/c_api.cc index 8975bfb2e..22f03640e 100644 --- a/src/c_api/c_api.cc +++ b/src/c_api/c_api.cc @@ -7,8 +7,6 @@ #include // for strtoimax #include // for nan #include // for strcmp -#include // for operator<<, basic_ostream, ios, stringstream -#include // for less #include // for numeric_limits #include // for operator!=, _Rb_tree_const_iterator, _Rb_tre... #include // for shared_ptr, allocator, __shared_ptr_access @@ -22,7 +20,6 @@ #include "../common/charconv.h" // for from_chars, to_chars, NumericLimits, from_ch... #include "../common/hist_util.h" // for HistogramCuts #include "../common/io.h" // for FileExtension, LoadSequentialFile, MemoryBuf... -#include "../common/linalg_op.h" // for ElementWiseTransformHost #include "../common/threading_utils.h" // for OmpGetNumThreads, ParallelFor #include "../data/adapter.h" // for ArrayAdapter, DenseAdapter, RecordBatchesIte... #include "../data/ellpack_page.h" // for EllpackPage @@ -35,14 +32,12 @@ #include "dmlc/parameter.h" // for FieldAccessEntry, FieldEntry, ParamManager #include "dmlc/thread_local.h" // for ThreadLocalStore #include "rabit/c_api.h" // for RabitLinkTag -#include "rabit/rabit.h" // for CheckPoint, LoadCheckPoint #include "xgboost/base.h" // for bst_ulong, bst_float, GradientPair, bst_feat... #include "xgboost/context.h" // for Context #include "xgboost/data.h" // for DMatrix, MetaInfo, DataType, ExtSparsePage #include "xgboost/feature_map.h" // for FeatureMap #include "xgboost/global_config.h" // for GlobalConfiguration, GlobalConfigThreadLocal... #include "xgboost/host_device_vector.h" // for HostDeviceVector -#include "xgboost/intrusive_ptr.h" // for xgboost #include "xgboost/json.h" // for Json, get, Integer, IsA, Boolean, String #include "xgboost/learner.h" // for Learner, PredictionType #include "xgboost/logging.h" // for LOG_FATAL, LogMessageFatal, CHECK, LogCheck_EQ @@ -79,6 +74,7 @@ void XGBBuildInfoDevice(Json *p_info) { info["USE_CUDA"] = Boolean{false}; info["USE_NCCL"] = Boolean{false}; info["USE_RMM"] = Boolean{false}; + info["USE_DLOPEN_NCCL"] = Boolean{false}; } } // namespace xgboost #endif diff --git a/src/c_api/c_api.cu b/src/c_api/c_api.cu index 84a371558..4ace8b7cc 100644 --- a/src/c_api/c_api.cu +++ b/src/c_api/c_api.cu @@ -33,8 +33,16 @@ void XGBBuildInfoDevice(Json *p_info) { info["USE_NCCL"] = Boolean{true}; v = {Json{Integer{NCCL_MAJOR}}, Json{Integer{NCCL_MINOR}}, Json{Integer{NCCL_PATCH}}}; info["NCCL_VERSION"] = v; + +#if defined(XGBOOST_USE_DLOPEN_NCCL) + info["USE_DLOPEN_NCCL"] = Boolean{true}; +#else + info["USE_DLOPEN_NCCL"] = Boolean{false}; +#endif // defined(XGBOOST_USE_DLOPEN_NCCL) + #else info["USE_NCCL"] = Boolean{false}; + info["USE_DLOPEN_NCCL"] = Boolean{false}; #endif #if defined(XGBOOST_USE_RMM) diff --git a/src/collective/coll.cu b/src/collective/coll.cu index bac9fb094..60072b6a5 100644 --- a/src/collective/coll.cu +++ b/src/collective/coll.cu @@ -19,25 +19,6 @@ Coll* Coll::MakeCUDAVar() { return new NCCLColl{}; } NCCLColl::~NCCLColl() = default; namespace { -Result GetNCCLResult(ncclResult_t code) { - if (code == ncclSuccess) { - return Success(); - } - - std::stringstream ss; - ss << "NCCL failure: " << ncclGetErrorString(code) << "."; - if (code == ncclUnhandledCudaError) { - // nccl usually preserves the last error so we can get more details. - auto err = cudaPeekAtLastError(); - ss << " CUDA error: " << thrust::system_error(err, thrust::cuda_category()).what() << "\n"; - } else if (code == ncclSystemError) { - ss << " This might be caused by a network configuration issue. Please consider specifying " - "the network interface for NCCL via environment variables listed in its reference: " - "`https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html`.\n"; - } - return Fail(ss.str()); -} - auto GetNCCLType(ArrayInterfaceHandler::Type type) { auto fatal = [] { LOG(FATAL) << "Invalid type for NCCL operation."; @@ -94,11 +75,12 @@ void RunBitwiseAllreduce(dh::CUDAStreamView stream, common::Span ou common::Span data, Op op) { dh::device_vector buffer(data.size() * pcomm->World()); auto* device_buffer = buffer.data().get(); + auto stub = pcomm->Stub(); // First gather data from all the workers. CHECK(handle); - auto rc = GetNCCLResult( - ncclAllGather(data.data(), device_buffer, data.size(), ncclInt8, handle, pcomm->Stream())); + auto rc = GetNCCLResult(stub, stub->Allgather(data.data(), device_buffer, data.size(), ncclInt8, + handle, pcomm->Stream())); if (!rc.OK()) { return rc; } @@ -149,6 +131,8 @@ ncclRedOp_t GetNCCLRedOp(Op const& op) { } auto nccl = dynamic_cast(&comm); CHECK(nccl); + auto stub = nccl->Stub(); + return Success() << [&] { if (IsBitwiseOp(op)) { return BitwiseAllReduce(nccl, nccl->Handle(), data, op); @@ -156,9 +140,9 @@ ncclRedOp_t GetNCCLRedOp(Op const& op) { return DispatchDType(type, [=](auto t) { using T = decltype(t); auto rdata = common::RestoreType(data); - auto rc = ncclAllReduce(data.data(), data.data(), rdata.size(), GetNCCLType(type), - GetNCCLRedOp(op), nccl->Handle(), nccl->Stream()); - return GetNCCLResult(rc); + auto rc = stub->Allreduce(data.data(), data.data(), rdata.size(), GetNCCLType(type), + GetNCCLRedOp(op), nccl->Handle(), nccl->Stream()); + return GetNCCLResult(stub, rc); }); } } << [&] { return nccl->Block(); }; @@ -171,9 +155,11 @@ ncclRedOp_t GetNCCLRedOp(Op const& op) { } auto nccl = dynamic_cast(&comm); CHECK(nccl); + auto stub = nccl->Stub(); + return Success() << [&] { - return GetNCCLResult(ncclBroadcast(data.data(), data.data(), data.size_bytes(), ncclInt8, root, - nccl->Handle(), nccl->Stream())); + return GetNCCLResult(stub, stub->Broadcast(data.data(), data.data(), data.size_bytes(), + ncclInt8, root, nccl->Handle(), nccl->Stream())); } << [&] { return nccl->Block(); }; } @@ -184,10 +170,12 @@ ncclRedOp_t GetNCCLRedOp(Op const& op) { } auto nccl = dynamic_cast(&comm); CHECK(nccl); + auto stub = nccl->Stub(); + auto send = data.subspan(comm.Rank() * size, size); return Success() << [&] { - return GetNCCLResult( - ncclAllGather(send.data(), data.data(), size, ncclInt8, nccl->Handle(), nccl->Stream())); + return GetNCCLResult(stub, stub->Allgather(send.data(), data.data(), size, ncclInt8, + nccl->Handle(), nccl->Stream())); } << [&] { return nccl->Block(); }; } @@ -199,19 +187,20 @@ namespace cuda_impl { */ Result BroadcastAllgatherV(NCCLComm const* comm, common::Span data, common::Span sizes, common::Span recv) { - return Success() << [] { return GetNCCLResult(ncclGroupStart()); } << [&] { + auto stub = comm->Stub(); + return Success() << [&stub] { return GetNCCLResult(stub, stub->GroupStart()); } << [&] { std::size_t offset = 0; for (std::int32_t r = 0; r < comm->World(); ++r) { auto as_bytes = sizes[r]; - auto rc = ncclBroadcast(data.data(), recv.subspan(offset, as_bytes).data(), as_bytes, - ncclInt8, r, comm->Handle(), dh::DefaultStream()); + auto rc = stub->Broadcast(data.data(), recv.subspan(offset, as_bytes).data(), as_bytes, + ncclInt8, r, comm->Handle(), dh::DefaultStream()); if (rc != ncclSuccess) { - return GetNCCLResult(rc); + return GetNCCLResult(stub, rc); } offset += as_bytes; } return Success(); - } << [] { return GetNCCLResult(ncclGroupEnd()); }; + } << [&] { return GetNCCLResult(stub, stub->GroupEnd()); }; } } // namespace cuda_impl @@ -224,10 +213,11 @@ Result BroadcastAllgatherV(NCCLComm const* comm, common::Span if (!comm.IsDistributed()) { return Success(); } + auto stub = nccl->Stub(); switch (algo) { case AllgatherVAlgo::kRing: { - return Success() << [] { return GetNCCLResult(ncclGroupStart()); } << [&] { + return Success() << [&] { return GetNCCLResult(stub, stub->GroupStart()); } << [&] { // get worker offset detail::AllgatherVOffset(sizes, recv_segments); // copy data @@ -237,8 +227,8 @@ Result BroadcastAllgatherV(NCCLComm const* comm, common::Span cudaMemcpyDeviceToDevice, nccl->Stream())); } return detail::RingAllgatherV(comm, sizes, recv_segments, recv); - } << [] { - return GetNCCLResult(ncclGroupEnd()); + } << [&] { + return GetNCCLResult(stub, stub->GroupEnd()); } << [&] { return nccl->Block(); }; } case AllgatherVAlgo::kBcast: { diff --git a/src/collective/coll.cuh b/src/collective/coll.cuh index 87fb46711..6ededd101 100644 --- a/src/collective/coll.cuh +++ b/src/collective/coll.cuh @@ -8,7 +8,8 @@ #include "../data/array_interface.h" // for ArrayInterfaceHandler #include "coll.h" // for Coll #include "comm.h" // for Comm -#include "xgboost/span.h" // for Span +#include "nccl_stub.h" +#include "xgboost/span.h" // for Span namespace xgboost::collective { class NCCLColl : public Coll { diff --git a/src/collective/comm.cc b/src/collective/comm.cc index 9da9083f8..783278b65 100644 --- a/src/collective/comm.cc +++ b/src/collective/comm.cc @@ -7,15 +7,12 @@ #include // for seconds #include // for exit #include // for shared_ptr -#include // for unique_lock #include // for string #include // for move, forward #include "../common/common.h" // for AssertGPUSupport -#include "../common/json_utils.h" // for OptionalArg #include "allgather.h" // for RingAllgather #include "protocol.h" // for kMagic -#include "tracker.h" // for GetHostAddress #include "xgboost/base.h" // for XGBOOST_STRICT_R_MODE #include "xgboost/collective/socket.h" // for TCPSocket #include "xgboost/json.h" // for Json, Object @@ -62,14 +59,6 @@ Result ConnectTrackerImpl(proto::PeerInfo info, std::chrono::seconds timeout, st this->Rank(), this->World()); } -#if !defined(XGBOOST_USE_NCCL) -Comm* Comm::MakeCUDAVar(Context const*, std::shared_ptr) const { - common::AssertGPUSupport(); - common::AssertNCCLSupport(); - return nullptr; -} -#endif // !defined(XGBOOST_USE_NCCL) - [[nodiscard]] Result ConnectWorkers(Comm const& comm, TCPSocket* listener, std::int32_t lport, proto::PeerInfo ninfo, std::chrono::seconds timeout, std::int32_t retry, @@ -194,12 +183,21 @@ Comm* Comm::MakeCUDAVar(Context const*, std::shared_ptr) const { } RabitComm::RabitComm(std::string const& host, std::int32_t port, std::chrono::seconds timeout, - std::int32_t retry, std::string task_id) - : Comm{std::move(host), port, timeout, retry, std::move(task_id)} { + std::int32_t retry, std::string task_id, StringView nccl_path) + : HostComm{std::move(host), port, timeout, retry, std::move(task_id)}, + nccl_path_{std::move(nccl_path)} { auto rc = this->Bootstrap(timeout_, retry_, task_id_); CHECK(rc.OK()) << rc.Report(); } +#if !defined(XGBOOST_USE_NCCL) +Comm* RabitComm::MakeCUDAVar(Context const*, std::shared_ptr) const { + common::AssertGPUSupport(); + common::AssertNCCLSupport(); + return nullptr; +} +#endif // !defined(XGBOOST_USE_NCCL) + [[nodiscard]] Result RabitComm::Bootstrap(std::chrono::seconds timeout, std::int32_t retry, std::string task_id) { TCPSocket tracker; diff --git a/src/collective/comm.cu b/src/collective/comm.cu index 09edc522d..cc67def0a 100644 --- a/src/collective/comm.cu +++ b/src/collective/comm.cu @@ -13,19 +13,21 @@ #include "../common/cuda_context.cuh" // for CUDAContext #include "../common/device_helpers.cuh" // for DefaultStream #include "../common/type.h" // for EraseType -#include "broadcast.h" // for Broadcast #include "comm.cuh" // for NCCLComm #include "comm.h" // for Comm +#include "nccl_stub.h" // for NcclStub #include "xgboost/collective/result.h" // for Result #include "xgboost/span.h" // for Span namespace xgboost::collective { namespace { -Result GetUniqueId(Comm const& comm, std::shared_ptr coll, ncclUniqueId* pid) { +Result GetUniqueId(Comm const& comm, std::shared_ptr stub, std::shared_ptr coll, + ncclUniqueId* pid) { static const int kRootRank = 0; ncclUniqueId id; if (comm.Rank() == kRootRank) { - dh::safe_nccl(ncclGetUniqueId(&id)); + auto rc = GetNCCLResult(stub, stub->GetUniqueId(&id)); + CHECK(rc.OK()) << rc.Report(); } auto rc = coll->Broadcast( comm, common::Span{reinterpret_cast(&id), sizeof(ncclUniqueId)}, kRootRank); @@ -54,11 +56,12 @@ static std::string PrintUUID(xgboost::common::Span c } } // namespace -Comm* Comm::MakeCUDAVar(Context const* ctx, std::shared_ptr pimpl) const { - return new NCCLComm{ctx, *this, pimpl}; +Comm* RabitComm::MakeCUDAVar(Context const* ctx, std::shared_ptr pimpl) const { + return new NCCLComm{ctx, *this, pimpl, StringView{this->nccl_path_}}; } -NCCLComm::NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr pimpl) +NCCLComm::NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr pimpl, + StringView nccl_path) : Comm{root.TrackerInfo().host, root.TrackerInfo().port, root.Timeout(), root.Retry(), root.TaskID()}, stream_{ctx->CUDACtx()->Stream()} { @@ -70,6 +73,7 @@ NCCLComm::NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr p } dh::safe_cuda(cudaSetDevice(ctx->Ordinal())); + stub_ = std::make_shared(nccl_path); std::vector uuids(root.World() * kUuidLength, 0); auto s_uuid = xgboost::common::Span{uuids.data(), uuids.size()}; @@ -95,19 +99,24 @@ NCCLComm::NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr p << "Multiple processes within communication group running on same CUDA " << "device is not supported. " << PrintUUID(s_this_uuid) << "\n"; - rc = GetUniqueId(root, pimpl, &nccl_unique_id_); + rc = std::move(rc) << [&] { + return GetUniqueId(root, this->stub_, pimpl, &nccl_unique_id_); + } << [&] { + return GetNCCLResult(this->stub_, this->stub_->CommInitRank(&nccl_comm_, root.World(), + nccl_unique_id_, root.Rank())); + }; CHECK(rc.OK()) << rc.Report(); - dh::safe_nccl(ncclCommInitRank(&nccl_comm_, root.World(), nccl_unique_id_, root.Rank())); for (std::int32_t r = 0; r < root.World(); ++r) { this->channels_.emplace_back( - std::make_shared(root, r, nccl_comm_, dh::DefaultStream())); + std::make_shared(root, r, nccl_comm_, stub_, dh::DefaultStream())); } } NCCLComm::~NCCLComm() { if (nccl_comm_) { - dh::safe_nccl(ncclCommDestroy(nccl_comm_)); + auto rc = GetNCCLResult(stub_, stub_->CommDestroy(nccl_comm_)); + CHECK(rc.OK()) << rc.Report(); } } } // namespace xgboost::collective diff --git a/src/collective/comm.cuh b/src/collective/comm.cuh index ea15c50f3..ef537b5a9 100644 --- a/src/collective/comm.cuh +++ b/src/collective/comm.cuh @@ -6,9 +6,13 @@ #ifdef XGBOOST_USE_NCCL #include "nccl.h" #endif // XGBOOST_USE_NCCL + +#include // for move + #include "../common/device_helpers.cuh" #include "coll.h" #include "comm.h" +#include "nccl_stub.h" // for NcclStub #include "xgboost/context.h" namespace xgboost::collective { @@ -21,15 +25,20 @@ inline Result GetCUDAResult(cudaError rc) { return Fail(msg); } +#if defined(XGBOOST_USE_NCCL) class NCCLComm : public Comm { ncclComm_t nccl_comm_{nullptr}; + std::shared_ptr stub_; ncclUniqueId nccl_unique_id_{}; dh::CUDAStreamView stream_; + std::string nccl_path_; public: [[nodiscard]] ncclComm_t Handle() const { return nccl_comm_; } + auto Stub() const { return stub_; } - explicit NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr pimpl); + explicit NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr pimpl, + StringView nccl_path); [[nodiscard]] Result LogTracker(std::string) const override { LOG(FATAL) << "Device comm is used for logging."; return Fail("Undefined."); @@ -43,25 +52,53 @@ class NCCLComm : public Comm { } }; +inline Result GetNCCLResult(std::shared_ptr stub, ncclResult_t code) { + if (code == ncclSuccess) { + return Success(); + } + + std::stringstream ss; + ss << "NCCL failure: " << stub->GetErrorString(code) << "."; + if (code == ncclUnhandledCudaError) { + // nccl usually preserves the last error so we can get more details. + auto err = cudaPeekAtLastError(); + ss << " CUDA error: " << thrust::system_error(err, thrust::cuda_category()).what() << "\n"; + } else if (code == ncclSystemError) { + ss << " This might be caused by a network configuration issue. Please consider specifying " + "the network interface for NCCL via environment variables listed in its reference: " + "`https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html`.\n"; + } + return Fail(ss.str()); +} + class NCCLChannel : public Channel { std::int32_t rank_{-1}; ncclComm_t nccl_comm_{}; + std::shared_ptr stub_; dh::CUDAStreamView stream_; public: explicit NCCLChannel(Comm const& comm, std::int32_t rank, ncclComm_t nccl_comm, - dh::CUDAStreamView stream) - : rank_{rank}, nccl_comm_{nccl_comm}, Channel{comm, nullptr}, stream_{stream} {} + std::shared_ptr stub, dh::CUDAStreamView stream) + : rank_{rank}, + nccl_comm_{nccl_comm}, + stub_{std::move(stub)}, + Channel{comm, nullptr}, + stream_{stream} {} void SendAll(std::int8_t const* ptr, std::size_t n) override { - dh::safe_nccl(ncclSend(ptr, n, ncclInt8, rank_, nccl_comm_, stream_)); + auto rc = GetNCCLResult(stub_, stub_->Send(ptr, n, ncclInt8, rank_, nccl_comm_, stream_)); + CHECK(rc.OK()) << rc.Report(); } void RecvAll(std::int8_t* ptr, std::size_t n) override { - dh::safe_nccl(ncclRecv(ptr, n, ncclInt8, rank_, nccl_comm_, stream_)); + auto rc = GetNCCLResult(stub_, stub_->Recv(ptr, n, ncclInt8, rank_, nccl_comm_, stream_)); + CHECK(rc.OK()) << rc.Report(); } [[nodiscard]] Result Block() override { auto rc = stream_.Sync(false); return GetCUDAResult(rc); } }; + +#endif // defined(XGBOOST_USE_NCCL) } // namespace xgboost::collective diff --git a/src/collective/comm.h b/src/collective/comm.h index 76ab479d7..b2f519e3d 100644 --- a/src/collective/comm.h +++ b/src/collective/comm.h @@ -34,6 +34,8 @@ inline std::int32_t BootstrapPrev(std::int32_t r, std::int32_t world) { return nrank; } +inline StringView DefaultNcclName() { return "libnccl.so.2"; } + class Channel; class Coll; @@ -86,11 +88,21 @@ class Comm : public std::enable_shared_from_this { [[nodiscard]] virtual Result LogTracker(std::string msg) const = 0; [[nodiscard]] virtual Result SignalError(Result const&) { return Success(); } - - virtual Comm* MakeCUDAVar(Context const* ctx, std::shared_ptr pimpl) const; }; -class RabitComm : public Comm { +/** + * @brief Base class for CPU-based communicator. + */ +class HostComm : public Comm { + public: + using Comm::Comm; + [[nodiscard]] virtual Comm* MakeCUDAVar(Context const* ctx, + std::shared_ptr pimpl) const = 0; +}; + +class RabitComm : public HostComm { + std::string nccl_path_ = std::string{DefaultNcclName()}; + [[nodiscard]] Result Bootstrap(std::chrono::seconds timeout, std::int32_t retry, std::string task_id); [[nodiscard]] Result Shutdown(); @@ -100,13 +112,15 @@ class RabitComm : public Comm { RabitComm() = default; // ctor for testing where environment is known. RabitComm(std::string const& host, std::int32_t port, std::chrono::seconds timeout, - std::int32_t retry, std::string task_id); + std::int32_t retry, std::string task_id, StringView nccl_path); ~RabitComm() noexcept(false) override; [[nodiscard]] bool IsFederated() const override { return false; } [[nodiscard]] Result LogTracker(std::string msg) const override; [[nodiscard]] Result SignalError(Result const&) override; + + [[nodiscard]] Comm* MakeCUDAVar(Context const* ctx, std::shared_ptr pimpl) const override; }; /** diff --git a/src/collective/comm_group.cc b/src/collective/comm_group.cc index 3d2e24492..f7bbba754 100644 --- a/src/collective/comm_group.cc +++ b/src/collective/comm_group.cc @@ -37,7 +37,7 @@ namespace xgboost::collective { [[nodiscard]] Comm const& CommGroup::Ctx(Context const* ctx, DeviceOrd device) const { if (device.IsCUDA()) { CHECK(ctx->IsCUDA()); - if (!gpu_comm_) { + if (!gpu_comm_ || gpu_comm_->World() != comm_->World()) { gpu_comm_.reset(comm_->MakeCUDAVar(ctx, backend_)); } return *gpu_comm_; @@ -55,7 +55,6 @@ CommGroup::CommGroup() } std::string type = OptionalArg(config, "dmlc_communicator", std::string{"rabit"}); - std::vector keys; // Try both lower and upper case for compatibility auto get_param = [&](std::string name, auto dft, auto t) { std::string upper; @@ -63,8 +62,6 @@ CommGroup::CommGroup() [](char c) { return std::toupper(c); }); std::transform(name.cbegin(), name.cend(), name.begin(), [](char c) { return std::tolower(c); }); - keys.push_back(upper); - keys.push_back(name); auto const& obj = get(config); auto it = obj.find(upper); @@ -75,19 +72,19 @@ CommGroup::CommGroup() } }; // Common args - auto retry = - OptionalArg(config, "dmlc_retry", static_cast(DefaultRetry())); - auto timeout = OptionalArg(config, "dmlc_timeout_sec", - static_cast(DefaultTimeoutSec())); + auto retry = get_param("dmlc_retry", static_cast(DefaultRetry()), Integer{}); + auto timeout = + get_param("dmlc_timeout_sec", static_cast(DefaultTimeoutSec()), Integer{}); auto task_id = get_param("dmlc_task_id", std::string{}, String{}); if (type == "rabit") { auto host = get_param("dmlc_tracker_uri", std::string{}, String{}); auto port = get_param("dmlc_tracker_port", static_cast(0), Integer{}); + auto nccl = get_param("dmlc_nccl_path", std::string{DefaultNcclName()}, String{}); auto ptr = new CommGroup{std::shared_ptr{new RabitComm{ // NOLINT host, static_cast(port), std::chrono::seconds{timeout}, - static_cast(retry), task_id}}, + static_cast(retry), task_id, nccl}}, std::shared_ptr(new Coll{})}; // NOLINT return ptr; } else if (type == "federated") { diff --git a/src/collective/comm_group.h b/src/collective/comm_group.h index 62f3e565f..2f6f91d73 100644 --- a/src/collective/comm_group.h +++ b/src/collective/comm_group.h @@ -17,14 +17,16 @@ namespace xgboost::collective { * collective implementations. */ class CommGroup { - std::shared_ptr comm_; + std::shared_ptr comm_; mutable std::shared_ptr gpu_comm_; std::shared_ptr backend_; mutable std::shared_ptr gpu_coll_; // lazy initialization CommGroup(std::shared_ptr comm, std::shared_ptr coll) - : comm_{std::move(comm)}, backend_{std::move(coll)} {} + : comm_{std::dynamic_pointer_cast(comm)}, backend_{std::move(coll)} { + CHECK(comm_); + } public: CommGroup(); diff --git a/src/collective/communicator.cc b/src/collective/communicator.cc index 6ac9ff58e..7fabe50b4 100644 --- a/src/collective/communicator.cc +++ b/src/collective/communicator.cc @@ -3,6 +3,7 @@ */ #include "communicator.h" +#include "comm.h" #include "in_memory_communicator.h" #include "noop_communicator.h" #include "rabit_communicator.h" @@ -14,8 +15,12 @@ namespace xgboost::collective { thread_local std::unique_ptr Communicator::communicator_{new NoOpCommunicator()}; thread_local CommunicatorType Communicator::type_{}; +thread_local std::string Communicator::nccl_path_{}; void Communicator::Init(Json const& config) { + auto nccl = OptionalArg(config, "dmlc_nccl_path", std::string{DefaultNcclName()}); + nccl_path_ = nccl; + auto type = GetTypeFromEnv(); auto const arg = GetTypeFromConfig(config); if (arg != CommunicatorType::kUnknown) { diff --git a/src/collective/communicator.cu b/src/collective/communicator.cu index a80eab6d5..a7552d356 100644 --- a/src/collective/communicator.cu +++ b/src/collective/communicator.cu @@ -31,17 +31,17 @@ DeviceCommunicator* Communicator::GetDevice(int device_ordinal) { #ifdef XGBOOST_USE_NCCL switch (type_) { case CommunicatorType::kRabit: - device_communicator_.reset(new NcclDeviceCommunicator(device_ordinal, false)); + device_communicator_.reset(new NcclDeviceCommunicator(device_ordinal, false, nccl_path_)); break; case CommunicatorType::kFederated: case CommunicatorType::kInMemory: device_communicator_.reset(new DeviceCommunicatorAdapter(device_ordinal)); break; case CommunicatorType::kInMemoryNccl: - device_communicator_.reset(new NcclDeviceCommunicator(device_ordinal, true)); + device_communicator_.reset(new NcclDeviceCommunicator(device_ordinal, true, nccl_path_)); break; default: - device_communicator_.reset(new NcclDeviceCommunicator(device_ordinal, false)); + device_communicator_.reset(new NcclDeviceCommunicator(device_ordinal, false, nccl_path_)); } #else device_communicator_.reset(new DeviceCommunicatorAdapter(device_ordinal)); diff --git a/src/collective/communicator.h b/src/collective/communicator.h index feb446355..b6910b80f 100644 --- a/src/collective/communicator.h +++ b/src/collective/communicator.h @@ -234,6 +234,7 @@ class Communicator { static thread_local std::unique_ptr communicator_; static thread_local CommunicatorType type_; + static thread_local std::string nccl_path_; #if defined(XGBOOST_USE_CUDA) static thread_local std::unique_ptr device_communicator_; #endif diff --git a/src/collective/nccl_device_communicator.cu b/src/collective/nccl_device_communicator.cu index 3d4905cb1..25b198bde 100644 --- a/src/collective/nccl_device_communicator.cu +++ b/src/collective/nccl_device_communicator.cu @@ -2,12 +2,14 @@ * Copyright 2023 XGBoost contributors */ #if defined(XGBOOST_USE_NCCL) +#include "comm.cuh" #include "nccl_device_communicator.cuh" namespace xgboost { namespace collective { -NcclDeviceCommunicator::NcclDeviceCommunicator(int device_ordinal, bool needs_sync) +NcclDeviceCommunicator::NcclDeviceCommunicator(int device_ordinal, bool needs_sync, + StringView nccl_path) : device_ordinal_{device_ordinal}, needs_sync_{needs_sync}, world_size_{GetWorldSize()}, @@ -18,6 +20,7 @@ NcclDeviceCommunicator::NcclDeviceCommunicator(int device_ordinal, bool needs_sy if (world_size_ == 1) { return; } + stub_ = std::make_shared(std::move(nccl_path)); std::vector uuids(world_size_ * kUuidLength, 0); auto s_uuid = xgboost::common::Span{uuids.data(), uuids.size()}; @@ -43,7 +46,9 @@ NcclDeviceCommunicator::NcclDeviceCommunicator(int device_ordinal, bool needs_sy nccl_unique_id_ = GetUniqueId(); dh::safe_cuda(cudaSetDevice(device_ordinal_)); - dh::safe_nccl(ncclCommInitRank(&nccl_comm_, world_size_, nccl_unique_id_, rank_)); + auto rc = + GetNCCLResult(stub_, stub_->CommInitRank(&nccl_comm_, world_size_, nccl_unique_id_, rank_)); + CHECK(rc.OK()) << rc.Report(); } NcclDeviceCommunicator::~NcclDeviceCommunicator() { @@ -51,7 +56,8 @@ NcclDeviceCommunicator::~NcclDeviceCommunicator() { return; } if (nccl_comm_) { - dh::safe_nccl(ncclCommDestroy(nccl_comm_)); + auto rc = GetNCCLResult(stub_, stub_->CommDestroy(nccl_comm_)); + CHECK(rc.OK()) << rc.Report(); } if (xgboost::ConsoleLogger::ShouldLog(xgboost::ConsoleLogger::LV::kDebug)) { LOG(CONSOLE) << "======== NCCL Statistics========"; @@ -137,8 +143,10 @@ void NcclDeviceCommunicator::BitwiseAllReduce(void *send_receive_buffer, std::si auto *device_buffer = buffer.data().get(); // First gather data from all the workers. - dh::safe_nccl(ncclAllGather(send_receive_buffer, device_buffer, count, GetNcclDataType(data_type), + auto rc = GetNCCLResult( + stub_, stub_->Allgather(send_receive_buffer, device_buffer, count, GetNcclDataType(data_type), nccl_comm_, dh::DefaultStream())); + CHECK(rc.OK()) << rc.Report(); if (needs_sync_) { dh::DefaultStream().Sync(); } @@ -170,9 +178,10 @@ void NcclDeviceCommunicator::AllReduce(void *send_receive_buffer, std::size_t co if (IsBitwiseOp(op)) { BitwiseAllReduce(send_receive_buffer, count, data_type, op); } else { - dh::safe_nccl(ncclAllReduce(send_receive_buffer, send_receive_buffer, count, - GetNcclDataType(data_type), GetNcclRedOp(op), nccl_comm_, - dh::DefaultStream())); + auto rc = GetNCCLResult(stub_, stub_->Allreduce(send_receive_buffer, send_receive_buffer, count, + GetNcclDataType(data_type), GetNcclRedOp(op), + nccl_comm_, dh::DefaultStream())); + CHECK(rc.OK()) << rc.Report(); } allreduce_bytes_ += count * GetTypeSize(data_type); allreduce_calls_ += 1; @@ -185,8 +194,9 @@ void NcclDeviceCommunicator::AllGather(void const *send_buffer, void *receive_bu } dh::safe_cuda(cudaSetDevice(device_ordinal_)); - dh::safe_nccl(ncclAllGather(send_buffer, receive_buffer, send_size, ncclInt8, nccl_comm_, - dh::DefaultStream())); + auto rc = GetNCCLResult(stub_, stub_->Allgather(send_buffer, receive_buffer, send_size, ncclInt8, + nccl_comm_, dh::DefaultStream())); + CHECK(rc.OK()) << rc.Report(); } void NcclDeviceCommunicator::AllGatherV(void const *send_buffer, size_t length_bytes, @@ -206,14 +216,19 @@ void NcclDeviceCommunicator::AllGatherV(void const *send_buffer, size_t length_b receive_buffer->resize(total_bytes); size_t offset = 0; - dh::safe_nccl(ncclGroupStart()); - for (int32_t i = 0; i < world_size_; ++i) { - size_t as_bytes = segments->at(i); - dh::safe_nccl(ncclBroadcast(send_buffer, receive_buffer->data().get() + offset, as_bytes, - ncclChar, i, nccl_comm_, dh::DefaultStream())); - offset += as_bytes; - } - dh::safe_nccl(ncclGroupEnd()); + auto rc = Success() << [&] { return GetNCCLResult(stub_, stub_->GroupStart()); } << [&] { + for (int32_t i = 0; i < world_size_; ++i) { + size_t as_bytes = segments->at(i); + auto rc = GetNCCLResult( + stub_, stub_->Broadcast(send_buffer, receive_buffer->data().get() + offset, as_bytes, + ncclChar, i, nccl_comm_, dh::DefaultStream())); + if (!rc.OK()) { + return rc; + } + offset += as_bytes; + } + return Success(); + } << [&] { return GetNCCLResult(stub_, stub_->GroupEnd()); }; } void NcclDeviceCommunicator::Synchronize() { diff --git a/src/collective/nccl_device_communicator.cuh b/src/collective/nccl_device_communicator.cuh index 084db2046..a194b4ef2 100644 --- a/src/collective/nccl_device_communicator.cuh +++ b/src/collective/nccl_device_communicator.cuh @@ -4,8 +4,10 @@ #pragma once #include "../common/device_helpers.cuh" +#include "comm.cuh" #include "communicator.h" #include "device_communicator.cuh" +#include "nccl_stub.h" namespace xgboost { namespace collective { @@ -25,7 +27,7 @@ class NcclDeviceCommunicator : public DeviceCommunicator { * needed. The in-memory communicator is used in tests with multiple threads, each thread * representing a rank/worker, so the additional synchronization is needed to avoid deadlocks. */ - explicit NcclDeviceCommunicator(int device_ordinal, bool needs_sync); + explicit NcclDeviceCommunicator(int device_ordinal, bool needs_sync, StringView nccl_path); ~NcclDeviceCommunicator() override; void AllReduce(void *send_receive_buffer, std::size_t count, DataType data_type, Operation op) override; @@ -64,7 +66,8 @@ class NcclDeviceCommunicator : public DeviceCommunicator { static const int kRootRank = 0; ncclUniqueId id; if (rank_ == kRootRank) { - dh::safe_nccl(ncclGetUniqueId(&id)); + auto rc = GetNCCLResult(stub_, stub_->GetUniqueId(&id)); + CHECK(rc.OK()) << rc.Report(); } Broadcast(static_cast(&id), sizeof(ncclUniqueId), static_cast(kRootRank)); return id; @@ -78,6 +81,7 @@ class NcclDeviceCommunicator : public DeviceCommunicator { int const world_size_; int const rank_; ncclComm_t nccl_comm_{}; + std::shared_ptr stub_; ncclUniqueId nccl_unique_id_{}; size_t allreduce_bytes_{0}; // Keep statistics of the number of bytes communicated. size_t allreduce_calls_{0}; // Keep statistics of the number of reduce calls. diff --git a/src/collective/nccl_stub.cc b/src/collective/nccl_stub.cc new file mode 100644 index 000000000..f4705a46e --- /dev/null +++ b/src/collective/nccl_stub.cc @@ -0,0 +1,109 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#if defined(XGBOOST_USE_NCCL) +#include "nccl_stub.h" + +#include // for CUDA_VERSION +#include // for dlclose, dlsym, dlopen +#include + +#include // for int32_t +#include // for stringstream +#include // for string +#include // for move + +#include "xgboost/logging.h" + +namespace xgboost::collective { +NcclStub::NcclStub(StringView path) : path_{std::move(path)} { +#if defined(XGBOOST_USE_DLOPEN_NCCL) + CHECK(!path_.empty()) << "Empty path for NCCL."; + + auto cu_major = (CUDA_VERSION) / 1000; + std::stringstream ss; + ss << R"m( + +If XGBoost is installed from PyPI with pip, the error can fixed by: + +- Run `pip install nvidia-nccl-cu)m" + << cu_major << "` (Or with any CUDA version that's compatible with " << cu_major << ")."; + ss << R"m( + +Otherwise, please refer to: + + https://xgboost.readthedocs.io/en/stable/tutorials/dask.html#troubleshooting + +for more info, or open an issue on GitHub. Starting from XGBoost 2.1.0, the PyPI package +no long bundles NCCL in the binary wheel. + +)m"; + auto help = ss.str(); + std::string msg{"Failed to load NCCL from path: `" + path_ + "`. Error:\n "}; + + auto safe_load = [&](auto t, StringView name) { + std::stringstream errs; + auto ptr = reinterpret_cast(dlsym(handle_, name.c_str())); + if (!ptr) { + errs << "Failed to load NCCL symbol `" << name << "` from " << path_ << ". Error:\n " + << dlerror() << help; + LOG(FATAL) << errs.str(); + } + return ptr; + }; + + handle_ = dlopen(path_.c_str(), RTLD_LAZY); + if (!handle_) { + LOG(FATAL) << msg << dlerror() << help; + } + + allreduce_ = safe_load(allreduce_, "ncclAllReduce"); + broadcast_ = safe_load(broadcast_, "ncclBroadcast"); + allgather_ = safe_load(allgather_, "ncclAllGather"); + comm_init_rank_ = safe_load(comm_init_rank_, "ncclCommInitRank"); + comm_destroy_ = safe_load(comm_destroy_, "ncclCommDestroy"); + get_uniqueid_ = safe_load(get_uniqueid_, "ncclGetUniqueId"); + send_ = safe_load(send_, "ncclSend"); + recv_ = safe_load(recv_, "ncclRecv"); + group_start_ = safe_load(group_start_, "ncclGroupStart"); + group_end_ = safe_load(group_end_, "ncclGroupEnd"); + get_error_string_ = safe_load(get_error_string_, "ncclGetErrorString"); + get_version_ = safe_load(get_version_, "ncclGetVersion"); + + std::int32_t v; + CHECK_EQ(get_version_(&v), ncclSuccess); + auto patch = v % 100; + auto minor = (v / 100) % 100; + auto major = v / 10000; + + LOG(INFO) << "Loaded shared NCCL " << major << "." << minor << "." << patch << ":`" << path_ + << "`" << std::endl; +#else + allreduce_ = ncclAllReduce; + broadcast_ = ncclBroadcast; + allgather_ = ncclAllGather; + comm_init_rank_ = ncclCommInitRank; + comm_destroy_ = ncclCommDestroy; + get_uniqueid_ = ncclGetUniqueId; + send_ = ncclSend; + recv_ = ncclRecv; + group_start_ = ncclGroupStart; + group_end_ = ncclGroupEnd; + get_error_string_ = ncclGetErrorString; + get_version_ = ncclGetVersion; +#endif +}; + +NcclStub::~NcclStub() { // NOLINT +#if defined(XGBOOST_USE_DLOPEN_NCCL) + if (handle_) { + auto rc = dlclose(handle_); + if (rc != 0) { + LOG(WARNING) << "Failed to close NCCL handle:" << dlerror(); + } + } + handle_ = nullptr; +#endif // defined(XGBOOST_USE_DLOPEN_NCCL) +} +} // namespace xgboost::collective +#endif // defined(XGBOOST_USE_NCCL) diff --git a/src/collective/nccl_stub.h b/src/collective/nccl_stub.h new file mode 100644 index 000000000..a003a6f22 --- /dev/null +++ b/src/collective/nccl_stub.h @@ -0,0 +1,94 @@ +/** + * Copyright 2023, XGBoost Contributors + */ +#pragma once +#if defined(XGBOOST_USE_NCCL) +#include +#include + +#include // for string + +#include "xgboost/string_view.h" // for StringView + +namespace xgboost::collective { +class NcclStub { +#if defined(XGBOOST_USE_DLOPEN_NCCL) + void* handle_{nullptr}; +#endif // defined(XGBOOST_USE_DLOPEN_NCCL) + std::string path_; + + decltype(ncclAllReduce)* allreduce_{nullptr}; + decltype(ncclBroadcast)* broadcast_{nullptr}; + decltype(ncclAllGather)* allgather_{nullptr}; + decltype(ncclCommInitRank)* comm_init_rank_{nullptr}; + decltype(ncclCommDestroy)* comm_destroy_{nullptr}; + decltype(ncclGetUniqueId)* get_uniqueid_{nullptr}; + decltype(ncclSend)* send_{nullptr}; + decltype(ncclRecv)* recv_{nullptr}; + decltype(ncclGroupStart)* group_start_{nullptr}; + decltype(ncclGroupEnd)* group_end_{nullptr}; + decltype(ncclGetErrorString)* get_error_string_{nullptr}; + decltype(ncclGetVersion)* get_version_{nullptr}; + + public: + explicit NcclStub(StringView path); + ~NcclStub(); + + [[nodiscard]] ncclResult_t Allreduce(const void* sendbuff, void* recvbuff, size_t count, + ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, + cudaStream_t stream) const { + CHECK(allreduce_); + return this->allreduce_(sendbuff, recvbuff, count, datatype, op, comm, stream); + } + [[nodiscard]] ncclResult_t Broadcast(const void* sendbuff, void* recvbuff, size_t count, + ncclDataType_t datatype, int root, ncclComm_t comm, + cudaStream_t stream) const { + CHECK(broadcast_); + return this->broadcast_(sendbuff, recvbuff, count, datatype, root, comm, stream); + } + [[nodiscard]] ncclResult_t Allgather(const void* sendbuff, void* recvbuff, size_t sendcount, + ncclDataType_t datatype, ncclComm_t comm, + cudaStream_t stream) const { + CHECK(allgather_); + return this->allgather_(sendbuff, recvbuff, sendcount, datatype, comm, stream); + } + [[nodiscard]] ncclResult_t CommInitRank(ncclComm_t* comm, int nranks, ncclUniqueId commId, + int rank) const { + CHECK(comm_init_rank_); + return this->comm_init_rank_(comm, nranks, commId, rank); + } + [[nodiscard]] ncclResult_t CommDestroy(ncclComm_t comm) const { + CHECK(comm_destroy_); + return this->comm_destroy_(comm); + } + + [[nodiscard]] ncclResult_t GetUniqueId(ncclUniqueId* uniqueId) const { + CHECK(get_uniqueid_); + return this->get_uniqueid_(uniqueId); + } + [[nodiscard]] ncclResult_t Send(const void* sendbuff, size_t count, ncclDataType_t datatype, + int peer, ncclComm_t comm, cudaStream_t stream) { + CHECK(send_); + return send_(sendbuff, count, datatype, peer, comm, stream); + } + [[nodiscard]] ncclResult_t Recv(void* recvbuff, size_t count, ncclDataType_t datatype, int peer, + ncclComm_t comm, cudaStream_t stream) const { + CHECK(recv_); + return recv_(recvbuff, count, datatype, peer, comm, stream); + } + [[nodiscard]] ncclResult_t GroupStart() const { + CHECK(group_start_); + return group_start_(); + } + [[nodiscard]] ncclResult_t GroupEnd() const { + CHECK(group_end_); + return group_end_(); + } + + [[nodiscard]] const char* GetErrorString(ncclResult_t result) const { + return get_error_string_(result); + } +}; +} // namespace xgboost::collective + +#endif // defined(XGBOOST_USE_NCCL) diff --git a/src/common/device_helpers.cuh b/src/common/device_helpers.cuh index 066f8a3e6..89ec42f2b 100644 --- a/src/common/device_helpers.cuh +++ b/src/common/device_helpers.cuh @@ -115,30 +115,6 @@ XGBOOST_DEV_INLINE T atomicAdd(T *addr, T v) { // NOLINT } namespace dh { -#ifdef XGBOOST_USE_NCCL -#define safe_nccl(ans) ThrowOnNcclError((ans), __FILE__, __LINE__) - -inline ncclResult_t ThrowOnNcclError(ncclResult_t code, const char *file, int line) { - if (code != ncclSuccess) { - std::stringstream ss; - ss << "NCCL failure: " << ncclGetErrorString(code) << "."; - ss << " " << file << "(" << line << ")\n"; - if (code == ncclUnhandledCudaError) { - // nccl usually preserves the last error so we can get more details. - auto err = cudaPeekAtLastError(); - ss << " CUDA error: " << thrust::system_error(err, thrust::cuda_category()).what() << "\n"; - } else if (code == ncclSystemError) { - ss << " This might be caused by a network configuration issue. Please consider specifying " - "the network interface for NCCL via environment variables listed in its reference: " - "`https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html`.\n"; - } - LOG(FATAL) << ss.str(); - } - - return code; -} -#endif - inline int32_t CudaGetPointerDevice(void const *ptr) { int32_t device = -1; cudaPointerAttributes attr; diff --git a/tests/buildkite/build-cuda-with-rmm.sh b/tests/buildkite/build-cuda-with-rmm.sh index 46bc98028..615608249 100755 --- a/tests/buildkite/build-cuda-with-rmm.sh +++ b/tests/buildkite/build-cuda-with-rmm.sh @@ -21,11 +21,18 @@ command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 docker --build-arg `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION" echo "--- Build libxgboost from the source" -$command_wrapper tests/ci_build/prune_libnccl.sh -$command_wrapper tests/ci_build/build_via_cmake.sh -DCMAKE_PREFIX_PATH="/opt/grpc;/opt/rmm" \ - -DUSE_CUDA=ON -DUSE_NCCL=ON -DUSE_OPENMP=ON -DHIDE_CXX_SYMBOLS=ON -DPLUGIN_FEDERATED=ON \ - -DPLUGIN_RMM=ON -DUSE_NCCL_LIB_PATH=ON -DNCCL_INCLUDE_DIR=/usr/include \ - -DNCCL_LIBRARY=/workspace/libnccl_static.a ${arch_flag} +$command_wrapper tests/ci_build/build_via_cmake.sh \ + -DCMAKE_PREFIX_PATH="/opt/grpc;/opt/rmm" \ + -DUSE_CUDA=ON \ + -DUSE_OPENMP=ON \ + -DHIDE_CXX_SYMBOLS=ON \ + -DPLUGIN_FEDERATED=ON \ + -DPLUGIN_RMM=ON \ + -DUSE_NCCL=ON \ + -DUSE_NCCL_LIB_PATH=ON \ + -DNCCL_INCLUDE_DIR=/usr/include \ + -DUSE_DLOPEN_NCCL=ON \ + ${arch_flag} echo "--- Build binary wheel" $command_wrapper bash -c \ "cd python-package && rm -rf dist/* && pip wheel --no-deps -v . --wheel-dir dist/" diff --git a/tests/buildkite/build-cuda.sh b/tests/buildkite/build-cuda.sh index 1926754b8..7bd3492a2 100755 --- a/tests/buildkite/build-cuda.sh +++ b/tests/buildkite/build-cuda.sh @@ -21,11 +21,17 @@ command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 docker --build-arg `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION" echo "--- Build libxgboost from the source" -$command_wrapper tests/ci_build/prune_libnccl.sh -$command_wrapper tests/ci_build/build_via_cmake.sh -DCMAKE_PREFIX_PATH="/opt/grpc" \ - -DUSE_CUDA=ON -DUSE_NCCL=ON -DUSE_OPENMP=ON -DHIDE_CXX_SYMBOLS=ON -DPLUGIN_FEDERATED=ON \ - -DUSE_NCCL_LIB_PATH=ON -DNCCL_INCLUDE_DIR=/usr/include \ - -DNCCL_LIBRARY=/workspace/libnccl_static.a ${arch_flag} +$command_wrapper tests/ci_build/build_via_cmake.sh \ + -DCMAKE_PREFIX_PATH="/opt/grpc" \ + -DUSE_CUDA=ON \ + -DUSE_OPENMP=ON \ + -DHIDE_CXX_SYMBOLS=ON \ + -DPLUGIN_FEDERATED=ON \ + -DUSE_NCCL=ON \ + -DUSE_NCCL_LIB_PATH=ON \ + -DNCCL_INCLUDE_DIR=/usr/include \ + -DUSE_DLOPEN_NCCL=ON \ + ${arch_flag} echo "--- Build binary wheel" $command_wrapper bash -c \ "cd python-package && rm -rf dist/* && pip wheel --no-deps -v . --wheel-dir dist/" diff --git a/tests/buildkite/test-cpp-gpu.sh b/tests/buildkite/test-cpp-gpu.sh index 58d250308..36f54cd3d 100755 --- a/tests/buildkite/test-cpp-gpu.sh +++ b/tests/buildkite/test-cpp-gpu.sh @@ -10,6 +10,7 @@ chmod +x build/testxgboost tests/ci_build/ci_build.sh gpu nvidia-docker \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ + --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ build/testxgboost echo "--- Run Google Tests with CUDA, using a GPU, RMM enabled" diff --git a/tests/buildkite/test-cpp-mgpu.sh b/tests/buildkite/test-cpp-mgpu.sh index 935a301a6..2aac47407 100755 --- a/tests/buildkite/test-cpp-mgpu.sh +++ b/tests/buildkite/test-cpp-mgpu.sh @@ -13,4 +13,5 @@ chmod +x build/testxgboost tests/ci_build/ci_build.sh gpu nvidia-docker \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ + --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ build/testxgboost --gtest_filter=*MGPU* diff --git a/tests/buildkite/test-python-gpu.sh b/tests/buildkite/test-python-gpu.sh index a575878d3..c2376c021 100755 --- a/tests/buildkite/test-python-gpu.sh +++ b/tests/buildkite/test-python-gpu.sh @@ -24,7 +24,8 @@ export CI_DOCKER_EXTRA_PARAMS_INIT='--shm-size=4g' command_wrapper="tests/ci_build/ci_build.sh gpu nvidia-docker --build-arg "` `"CUDA_VERSION_ARG=$CUDA_VERSION --build-arg "` - `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION" + `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION --build-arg "` + `"NCCL_VERSION_ARG=$NCCL_VERSION" # Run specified test suite case "$suite" in diff --git a/tests/ci_build/Dockerfile.gpu b/tests/ci_build/Dockerfile.gpu index 0822767c5..0a5adb6ea 100644 --- a/tests/ci_build/Dockerfile.gpu +++ b/tests/ci_build/Dockerfile.gpu @@ -2,6 +2,7 @@ ARG CUDA_VERSION_ARG FROM nvidia/cuda:$CUDA_VERSION_ARG-runtime-ubuntu22.04 ARG CUDA_VERSION_ARG ARG RAPIDS_VERSION_ARG +ARG NCCL_VERSION_ARG # Environment ENV DEBIAN_FRONTEND noninteractive @@ -23,7 +24,9 @@ RUN \ conda install -c conda-forge mamba && \ mamba create -n gpu_test -c rapidsai-nightly -c rapidsai -c nvidia -c conda-forge -c defaults \ python=3.10 cudf=$RAPIDS_VERSION_ARG* rmm=$RAPIDS_VERSION_ARG* cudatoolkit=$CUDA_VERSION_ARG \ - dask dask-cuda=$RAPIDS_VERSION_ARG* dask-cudf=$RAPIDS_VERSION_ARG* cupy \ + nccl>=$(cut -d "-" -f 1 << $NCCL_VERSION_ARG) \ + dask \ + dask-cuda=$RAPIDS_VERSION_ARG* dask-cudf=$RAPIDS_VERSION_ARG* cupy \ numpy pytest pytest-timeout scipy scikit-learn pandas matplotlib wheel python-kubernetes urllib3 graphviz hypothesis \ pyspark>=3.4.0 cloudpickle cuda-python && \ mamba clean --all && \ diff --git a/tests/ci_build/Dockerfile.gpu_build_centos7 b/tests/ci_build/Dockerfile.gpu_build_centos7 index 98a0a7033..16445de2a 100644 --- a/tests/ci_build/Dockerfile.gpu_build_centos7 +++ b/tests/ci_build/Dockerfile.gpu_build_centos7 @@ -27,7 +27,7 @@ RUN \ wget -nv -nc https://developer.download.nvidia.com/compute/machine-learning/repos/rhel7/x86_64/nvidia-machine-learning-repo-rhel7-1.0.0-1.x86_64.rpm && \ rpm -i nvidia-machine-learning-repo-rhel7-1.0.0-1.x86_64.rpm && \ yum -y update && \ - yum install -y libnccl-${NCCL_VERSION}+cuda${CUDA_SHORT} libnccl-devel-${NCCL_VERSION}+cuda${CUDA_SHORT} libnccl-static-${NCCL_VERSION}+cuda${CUDA_SHORT} && \ + yum install -y libnccl-${NCCL_VERSION}+cuda${CUDA_SHORT} libnccl-devel-${NCCL_VERSION}+cuda${CUDA_SHORT} && \ rm -f nvidia-machine-learning-repo-rhel7-1.0.0-1.x86_64.rpm; ENV PATH=/opt/mambaforge/bin:/usr/local/ninja:$PATH diff --git a/tests/ci_build/prune_libnccl.sh b/tests/ci_build/prune_libnccl.sh deleted file mode 100755 index c5a0d8123..000000000 --- a/tests/ci_build/prune_libnccl.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -e - -rm -rf tmp_nccl - -mkdir tmp_nccl -pushd tmp_nccl - -set -x - -cat << EOF > test.cu -int main(void) { return 0; } -EOF - -cat << EOF > CMakeLists.txt -cmake_minimum_required(VERSION 3.18 FATAL_ERROR) -project(gencode_extractor CXX C) -cmake_policy(SET CMP0104 NEW) -set(CMAKE_CUDA_HOST_COMPILER \${CMAKE_CXX_COMPILER}) -enable_language(CUDA) -include(../cmake/Utils.cmake) -compute_cmake_cuda_archs("") -add_library(test OBJECT test.cu) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -EOF - -cmake . -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -gen_code=$(grep -o -- '--generate-code=\S*' compile_commands.json | paste -sd ' ') - -nvprune ${gen_code} /usr/lib64/libnccl_static.a -o ../libnccl_static.a - -popd -rm -rf tmp_nccl - -set +x diff --git a/tests/ci_build/rename_whl.py b/tests/ci_build/rename_whl.py index 766c88a2f..2da7db8de 100644 --- a/tests/ci_build/rename_whl.py +++ b/tests/ci_build/rename_whl.py @@ -1,22 +1,10 @@ import os import sys -from contextlib import contextmanager - - -@contextmanager -def cd(path): - path = os.path.normpath(path) - cwd = os.getcwd() - os.chdir(path) - print("cd " + path) - try: - yield path - finally: - os.chdir(cwd) +from test_utils import DirectoryExcursion if len(sys.argv) != 4: - print('Usage: {} [wheel to rename] [commit id] [platform tag]'.format(sys.argv[0])) + print("Usage: {} [wheel to rename] [commit id] [platform tag]".format(sys.argv[0])) sys.exit(1) @@ -26,20 +14,26 @@ platform_tag = sys.argv[3] dirname, basename = os.path.dirname(whl_path), os.path.basename(whl_path) -with cd(dirname): - tokens = basename.split('-') +with DirectoryExcursion(dirname): + tokens = basename.split("-") assert len(tokens) == 5 - version = tokens[1].split('+')[0] - keywords = {'pkg_name': tokens[0], - 'version': version, - 'commit_id': commit_id, - 'platform_tag': platform_tag} - new_name = '{pkg_name}-{version}+{commit_id}-py3-none-{platform_tag}.whl'.format(**keywords) - print('Renaming {} to {}...'.format(basename, new_name)) + version = tokens[1].split("+")[0] + keywords = { + "pkg_name": tokens[0], + "version": version, + "commit_id": commit_id, + "platform_tag": platform_tag, + } + new_name = "{pkg_name}-{version}+{commit_id}-py3-none-{platform_tag}.whl".format( + **keywords + ) + print("Renaming {} to {}...".format(basename, new_name)) if os.path.isfile(new_name): os.remove(new_name) os.rename(basename, new_name) filesize = os.path.getsize(new_name) / 1024 / 1024 # MB + print(f"Wheel size: {filesize}") + msg = f"Limit of wheel size set by PyPI is exceeded. {new_name}: {filesize}" assert filesize <= 300, msg diff --git a/tests/cpp/collective/test_allgather.cu b/tests/cpp/collective/test_allgather.cu index 48f7c2615..236108198 100644 --- a/tests/cpp/collective/test_allgather.cu +++ b/tests/cpp/collective/test_allgather.cu @@ -90,10 +90,10 @@ class Worker : public NCCLWorkerForTest { } }; -class AllgatherTestGPU : public SocketTest {}; +class MGPUAllgatherTest : public SocketTest {}; } // namespace -TEST_F(AllgatherTestGPU, MGPUTestVRing) { +TEST_F(MGPUAllgatherTest, MGPUTestVRing) { auto n_workers = common::AllVisibleGPUs(); TestDistributed(n_workers, [=](std::string host, std::int32_t port, std::chrono::seconds timeout, std::int32_t r) { @@ -104,7 +104,7 @@ TEST_F(AllgatherTestGPU, MGPUTestVRing) { }); } -TEST_F(AllgatherTestGPU, MGPUTestVBcast) { +TEST_F(MGPUAllgatherTest, MGPUTestVBcast) { auto n_workers = common::AllVisibleGPUs(); TestDistributed(n_workers, [=](std::string host, std::int32_t port, std::chrono::seconds timeout, std::int32_t r) { diff --git a/tests/cpp/collective/test_allreduce.cu b/tests/cpp/collective/test_allreduce.cu index af9a4e58f..04ec9f773 100644 --- a/tests/cpp/collective/test_allreduce.cu +++ b/tests/cpp/collective/test_allreduce.cu @@ -5,17 +5,15 @@ #include #include // for host_vector -#include "../../../src/collective/coll.h" // for Coll #include "../../../src/common/common.h" #include "../../../src/common/device_helpers.cuh" // for ToSpan, device_vector #include "../../../src/common/type.h" // for EraseType -#include "../helpers.h" // for MakeCUDACtx #include "test_worker.cuh" // for NCCLWorkerForTest #include "test_worker.h" // for WorkerForTest, TestDistributed namespace xgboost::collective { namespace { -class AllreduceTestGPU : public SocketTest {}; +class MGPUAllreduceTest : public SocketTest {}; class Worker : public NCCLWorkerForTest { public: @@ -47,7 +45,7 @@ class Worker : public NCCLWorkerForTest { }; } // namespace -TEST_F(AllreduceTestGPU, BitOr) { +TEST_F(MGPUAllreduceTest, BitOr) { auto n_workers = common::AllVisibleGPUs(); TestDistributed(n_workers, [=](std::string host, std::int32_t port, std::chrono::seconds timeout, std::int32_t r) { @@ -57,7 +55,7 @@ TEST_F(AllreduceTestGPU, BitOr) { }); } -TEST_F(AllreduceTestGPU, Sum) { +TEST_F(MGPUAllreduceTest, Sum) { auto n_workers = common::AllVisibleGPUs(); TestDistributed(n_workers, [=](std::string host, std::int32_t port, std::chrono::seconds timeout, std::int32_t r) { diff --git a/tests/cpp/collective/test_nccl_device_communicator.cu b/tests/cpp/collective/test_nccl_device_communicator.cu index a09696c19..3d7b1efc8 100644 --- a/tests/cpp/collective/test_nccl_device_communicator.cu +++ b/tests/cpp/collective/test_nccl_device_communicator.cu @@ -8,6 +8,7 @@ #include #include // for string +#include "../../../src/collective/comm.cuh" #include "../../../src/collective/communicator-inl.cuh" #include "../../../src/collective/nccl_device_communicator.cuh" #include "../helpers.h" @@ -16,17 +17,15 @@ namespace xgboost { namespace collective { TEST(NcclDeviceCommunicatorSimpleTest, ThrowOnInvalidDeviceOrdinal) { - auto construct = []() { NcclDeviceCommunicator comm{-1, false}; }; + auto construct = []() { NcclDeviceCommunicator comm{-1, false, DefaultNcclName()}; }; EXPECT_THROW(construct(), dmlc::Error); } TEST(NcclDeviceCommunicatorSimpleTest, SystemError) { - try { - dh::safe_nccl(ncclSystemError); - } catch (dmlc::Error const& e) { - auto str = std::string{e.what()}; - ASSERT_TRUE(str.find("environment variables") != std::string::npos); - } + auto stub = std::make_shared(DefaultNcclName()); + auto rc = GetNCCLResult(stub, ncclSystemError); + auto msg = rc.Report(); + ASSERT_TRUE(msg.find("environment variables") != std::string::npos); } namespace { diff --git a/tests/cpp/collective/test_worker.h b/tests/cpp/collective/test_worker.h index 490cdf13c..acee0f297 100644 --- a/tests/cpp/collective/test_worker.h +++ b/tests/cpp/collective/test_worker.h @@ -33,7 +33,7 @@ class WorkerForTest { tracker_port_{port}, world_size_{world}, task_id_{"t:" + std::to_string(rank)}, - comm_{tracker_host_, tracker_port_, timeout, retry_, task_id_} { + comm_{tracker_host_, tracker_port_, timeout, retry_, task_id_, DefaultNcclName()} { CHECK_EQ(world_size_, comm_.World()); } virtual ~WorkerForTest() = default; diff --git a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py index 893582ee1..883dbbaf2 100644 --- a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py +++ b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py @@ -12,6 +12,7 @@ from hypothesis._settings import duration import xgboost as xgb from xgboost import testing as tm +from xgboost.collective import CommunicatorContext from xgboost.testing.params import hist_parameter_strategy pytestmark = [ @@ -572,6 +573,65 @@ def test_with_asyncio(local_cuda_client: Client) -> None: assert isinstance(output["history"], dict) +def test_invalid_nccl(local_cuda_client: Client) -> None: + client = local_cuda_client + workers = tm.get_client_workers(client) + args = client.sync( + dxgb._get_rabit_args, len(workers), dxgb._get_dask_config(), client + ) + + def run(wid: int) -> None: + ctx = CommunicatorContext(dmlc_nccl_path="foo", **args) + X, y, w = tm.make_regression(n_samples=10, n_features=10, use_cupy=True) + + with ctx: + with pytest.raises(ValueError, match=r"pip install"): + xgb.QuantileDMatrix(X, y, weight=w) + + futures = client.map(run, range(len(workers)), workers=workers) + client.gather(futures) + + +@pytest.mark.parametrize("tree_method", ["hist", "approx"]) +def test_nccl_load(local_cuda_client: Client, tree_method: str) -> None: + X, y, w = tm.make_regression(128, 16, use_cupy=True) + + def make_model() -> None: + xgb.XGBRegressor( + device="cuda", + tree_method=tree_method, + objective="reg:quantileerror", + verbosity=2, + quantile_alpha=[0.2, 0.8], + ).fit(X, y, sample_weight=w) + + # no nccl load when using single-node. + with tm.captured_output() as (out, err): + make_model() + assert out.getvalue().find("NCCL") == -1 + assert err.getvalue().find("NCCL") == -1 + + client = local_cuda_client + workers = tm.get_client_workers(client) + args = client.sync( + dxgb._get_rabit_args, len(workers), dxgb._get_dask_config(), client + ) + + # nccl is loaded + def run(wid: int) -> None: + # FIXME(jiamingy): https://github.com/dmlc/xgboost/issues/9147 + from xgboost.core import _LIB, _register_log_callback + _register_log_callback(_LIB) + + with CommunicatorContext(**args): + with tm.captured_output() as (out, err): + make_model() + assert out.getvalue().find("Loaded shared NCCL") != -1, out.getvalue() + + futures = client.map(run, range(len(workers)), workers=workers) + client.gather(futures) + + async def run_from_dask_array_asyncio(scheduler_address: str) -> dxgb.TrainReturnT: async with Client(scheduler_address, asynchronous=True) as client: import cupy as cp From 1877cb8e832af362fb810d947434f2fb70b3e7ad Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 22 Nov 2023 21:17:48 +0800 Subject: [PATCH 015/109] Change default metric for gamma regression to deviance. (#9757) * Change default metric for gamma regression to deviance. - Cleanup the gamma implementation. - Use deviance instead since the objective is derived from deviance. --- src/objective/regression_loss.h | 22 +++++++-- src/objective/regression_obj.cu | 85 ++------------------------------- 2 files changed, 21 insertions(+), 86 deletions(-) diff --git a/src/objective/regression_loss.h b/src/objective/regression_loss.h index 1ef7106cf..d2710d35a 100644 --- a/src/objective/regression_loss.h +++ b/src/objective/regression_loss.h @@ -13,9 +13,7 @@ #include "xgboost/logging.h" #include "xgboost/task.h" // ObjInfo -namespace xgboost { -namespace obj { -// common regressions +namespace xgboost::obj { // linear regression struct LinearSquareLoss { XGBOOST_DEVICE static bst_float PredTransform(bst_float x) { return x; } @@ -106,7 +104,21 @@ struct LogisticRaw : public LogisticRegression { static ObjInfo Info() { return ObjInfo::kRegression; } }; -} // namespace obj -} // namespace xgboost +// gamma deviance loss. +class GammaDeviance { + public: + XGBOOST_DEVICE static float PredTransform(float x) { return std::exp(x); } + XGBOOST_DEVICE static float ProbToMargin(float x) { return std::log(x); } + XGBOOST_DEVICE static float FirstOrderGradient(float p, float y) { + return 1.0f - y / p; + } + XGBOOST_DEVICE static float SecondOrderGradient(float p, float y) { return y / p; } + static ObjInfo Info() { return ObjInfo::kRegression; } + static const char* Name() { return "reg:gamma"; } + static const char* DefaultEvalMetric() { return "gamma-deviance"; } + XGBOOST_DEVICE static bool CheckLabel(float x) { return x > 0.0f; } + static const char* LabelErrorMsg() { return "label must be positive for gamma regression."; } +}; +} // namespace xgboost::obj #endif // XGBOOST_OBJECTIVE_REGRESSION_LOSS_H_ diff --git a/src/objective/regression_obj.cu b/src/objective/regression_obj.cu index 7f498c5f1..f74d01acc 100644 --- a/src/objective/regression_obj.cu +++ b/src/objective/regression_obj.cu @@ -221,6 +221,10 @@ XGBOOST_REGISTER_OBJECTIVE(LogisticRaw, LogisticRaw::Name()) "before logistic transformation.") .set_body([]() { return new RegLossObj(); }); +XGBOOST_REGISTER_OBJECTIVE(GammaRegression, GammaDeviance::Name()) + .describe("Gamma regression using the gamma deviance loss with log link.") + .set_body([]() { return new RegLossObj(); }); + // Deprecated functions XGBOOST_REGISTER_OBJECTIVE(LinearRegression, "reg:linear") .describe("Regression with squared error.") @@ -501,87 +505,6 @@ XGBOOST_REGISTER_OBJECTIVE(CoxRegression, "survival:cox") .describe("Cox regression for censored survival data (negative labels are considered censored).") .set_body([]() { return new CoxRegression(); }); -// gamma regression -class GammaRegression : public FitIntercept { - public: - void Configure(Args const&) override {} - [[nodiscard]] ObjInfo Task() const override { return ObjInfo::kRegression; } - - void GetGradient(const HostDeviceVector& preds, const MetaInfo& info, std::int32_t, - linalg::Matrix* out_gpair) override { - CHECK_NE(info.labels.Size(), 0U) << "label set cannot be empty"; - CHECK_EQ(preds.Size(), info.labels.Size()) << "labels are not correctly provided"; - const size_t ndata = preds.Size(); - auto device = ctx_->Device(); - out_gpair->SetDevice(ctx_->Device()); - out_gpair->Reshape(info.num_row_, this->Targets(info)); - label_correct_.Resize(1); - label_correct_.Fill(1); - - const bool is_null_weight = info.weights_.Size() == 0; - if (!is_null_weight) { - CHECK_EQ(info.weights_.Size(), ndata) - << "Number of weights should be equal to number of data points."; - } - common::Transform<>::Init( - [=] XGBOOST_DEVICE(size_t _idx, - common::Span _label_correct, - common::Span _out_gpair, - common::Span _preds, - common::Span _labels, - common::Span _weights) { - bst_float p = _preds[_idx]; - bst_float w = is_null_weight ? 1.0f : _weights[_idx]; - bst_float y = _labels[_idx]; - if (y <= 0.0f) { - _label_correct[0] = 0; - } - _out_gpair[_idx] = GradientPair((1 - y / expf(p)) * w, y / expf(p) * w); - }, - common::Range{0, static_cast(ndata)}, this->ctx_->Threads(), device).Eval( - &label_correct_, out_gpair->Data(), &preds, info.labels.Data(), &info.weights_); - - // copy "label correct" flags back to host - std::vector& label_correct_h = label_correct_.HostVector(); - for (auto const flag : label_correct_h) { - if (flag == 0) { - LOG(FATAL) << "GammaRegression: label must be positive."; - } - } - } - void PredTransform(HostDeviceVector *io_preds) const override { - common::Transform<>::Init( - [] XGBOOST_DEVICE(size_t _idx, common::Span _preds) { - _preds[_idx] = expf(_preds[_idx]); - }, - common::Range{0, static_cast(io_preds->Size())}, this->ctx_->Threads(), - io_preds->Device()) - .Eval(io_preds); - } - void EvalTransform(HostDeviceVector *io_preds) override { - PredTransform(io_preds); - } - [[nodiscard]] float ProbToMargin(bst_float base_score) const override { - return std::log(base_score); - } - [[nodiscard]] const char* DefaultEvalMetric() const override { - return "gamma-nloglik"; - } - void SaveConfig(Json* p_out) const override { - auto& out = *p_out; - out["name"] = String("reg:gamma"); - } - void LoadConfig(Json const&) override {} - - private: - HostDeviceVector label_correct_; -}; - -// register the objective functions -XGBOOST_REGISTER_OBJECTIVE(GammaRegression, "reg:gamma") -.describe("Gamma regression for severity data.") -.set_body([]() { return new GammaRegression(); }); - // declare parameter struct TweedieRegressionParam : public XGBoostParameter { From e9260de3f30708af5992009468432fbfe788fc42 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 23 Nov 2023 00:12:39 +0800 Subject: [PATCH 016/109] [breaking] Remove dense libsvm parser plugin. (#9799) --- .github/workflows/main.yml | 2 +- CMakeLists.txt | 4 +- plugin/CMakeLists.txt | 4 -- plugin/README.md | 2 - plugin/dense_parser/dense_libsvm.cc | 87 ------------------------- python-package/packager/build_config.py | 12 ++-- tests/buildkite/build-cpu.sh | 2 +- 7 files changed, 9 insertions(+), 104 deletions(-) delete mode 100644 plugin/dense_parser/dense_libsvm.cc diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 67e77ad6e..8f1252806 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: run: | mkdir build cd build - cmake .. -DGOOGLE_TEST=ON -DUSE_OPENMP=ON -DUSE_DMLC_GTEST=ON -DPLUGIN_DENSE_PARSER=ON -GNinja -DBUILD_DEPRECATED_CLI=ON + cmake .. -DGOOGLE_TEST=ON -DUSE_OPENMP=ON -DUSE_DMLC_GTEST=ON -GNinja -DBUILD_DEPRECATED_CLI=ON ninja -v - name: Run gtest binary run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index bf8f0cf62..a9c6f7410 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,6 @@ set(ENABLED_SANITIZERS "address" "leak" CACHE STRING "Semicolon separated list of sanitizer names. E.g 'address;leak'. Supported sanitizers are address, leak, undefined and thread.") ## Plugins -option(PLUGIN_DENSE_PARSER "Build dense parser plugin" OFF) option(PLUGIN_RMM "Build with RAPIDS Memory Manager (RMM)" OFF) option(PLUGIN_FEDERATED "Build with Federated Learning" OFF) ## TODO: 1. Add check if DPC++ compiler is used for building @@ -185,6 +184,9 @@ endif() if(USE_HDFS) message(SEND_ERROR "The option `USE_HDFS` has been removed from XGBoost") endif() +if(PLUGIN_DENSE_PARSER) + message(SEND_ERROR "The option `PLUGIN_DENSE_PARSER` has been removed from XGBoost.") +endif() #-- Sanitizer if(USE_SANITIZER) diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 6089ae486..58b31053f 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -1,7 +1,3 @@ -if(PLUGIN_DENSE_PARSER) - target_sources(objxgboost PRIVATE ${xgboost_SOURCE_DIR}/plugin/dense_parser/dense_libsvm.cc) -endif() - if(PLUGIN_UPDATER_ONEAPI) add_library(oneapi_plugin OBJECT ${xgboost_SOURCE_DIR}/plugin/updater_oneapi/regression_obj_oneapi.cc diff --git a/plugin/README.md b/plugin/README.md index 6e115c465..008f4ad49 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -36,5 +36,3 @@ The register macros available to plugin writers are: And from dmlc-core: - DMLC_REGISTER_PARAMETER - Register a set of parameter for a specific usecase - - DMLC_REGISTER_DATA_PARSER - Register a data parser where the data can be - represented by a URL. This is used by DMatrix. diff --git a/plugin/dense_parser/dense_libsvm.cc b/plugin/dense_parser/dense_libsvm.cc deleted file mode 100644 index 0dd2d0419..000000000 --- a/plugin/dense_parser/dense_libsvm.cc +++ /dev/null @@ -1,87 +0,0 @@ -/*! - * Copyright 2015 by Contributors - * \file dense_libsvm.cc - * \brief Plugin to load in libsvm, but fill all the missing entries with zeros. - * This plugin is mainly used for benchmark purposes and do not need to be included. - */ -#include -#include -#include - -namespace dmlc { -namespace data { - -template -class DensifyParser : public dmlc::Parser { - public: - DensifyParser(dmlc::Parser* parser, uint32_t num_col) - : parser_(parser), num_col_(num_col) { - } - - void BeforeFirst() override { - parser_->BeforeFirst(); - } - - bool Next() override { - if (!parser_->Next()) return false; - const RowBlock& batch = parser_->Value(); - LOG(INFO) << batch.size; - dense_index_.resize(num_col_ * batch.size); - dense_value_.resize(num_col_ * batch.size); - std::fill(dense_value_.begin(), dense_value_.end(), 0.0); - offset_.resize(batch.size + 1); - offset_[0] = 0; - - for (size_t i = 0; i < batch.size; ++i) { - offset_[i + 1] = (i + 1) * num_col_; - Row row = batch[i]; - for (uint32_t j = 0; j < num_col_; ++j) { - dense_index_[i * num_col_ + j] = j; - } - for (unsigned k = 0; k < row.length; ++k) { - uint32_t index = row.get_index(k); - CHECK_LT(index, num_col_) - << "Featuere index larger than num_col"; - dense_value_[i * num_col_ + index] = row.get_value(k); - } - } - out_ = batch; - out_.index = dmlc::BeginPtr(dense_index_); - out_.value = dmlc::BeginPtr(dense_value_); - out_.offset = dmlc::BeginPtr(offset_); - return true; - } - - const dmlc::RowBlock& Value() const override { - return out_; - } - - size_t BytesRead() const override { - return parser_->BytesRead(); - } - - private: - RowBlock out_; - std::unique_ptr > parser_; - uint32_t num_col_; - std::vector offset_; - std::vector dense_index_; - std::vector dense_value_; -}; - -template -Parser * -CreateDenseLibSVMParser(const std::string& path, - const std::map& args, - unsigned part_index, - unsigned num_parts) { - CHECK_NE(args.count("num_col"), 0) << "expect num_col in dense_libsvm"; - return new DensifyParser( - Parser::Create(path.c_str(), part_index, num_parts, "libsvm"), - uint32_t(atoi(args.at("num_col").c_str()))); -} -} // namespace data - -DMLC_REGISTER_DATA_PARSER(uint32_t, real_t, dense_libsvm, - data::CreateDenseLibSVMParser); -} // namespace dmlc diff --git a/python-package/packager/build_config.py b/python-package/packager/build_config.py index d3733d628..933bfdce2 100644 --- a/python-package/packager/build_config.py +++ b/python-package/packager/build_config.py @@ -17,14 +17,10 @@ class BuildConfiguration: # pylint: disable=R0902 use_nccl: bool = False # Whether to load nccl dynamically use_dlopen_nccl: bool = False - # Whether to enable HDFS - use_hdfs: bool = False - # Whether to enable Azure Storage - use_azure: bool = False - # Whether to enable AWS S3 - use_s3: bool = False - # Whether to enable the dense parser plugin - plugin_dense_parser: bool = False + # Whether to enable federated learning + plugin_federated: bool = False + # Whether to enable rmm support + plugin_rmm: bool = False # Special option: See explanation below use_system_libxgboost: bool = False diff --git a/tests/buildkite/build-cpu.sh b/tests/buildkite/build-cpu.sh index 88da7d395..73e88d8aa 100755 --- a/tests/buildkite/build-cpu.sh +++ b/tests/buildkite/build-cpu.sh @@ -15,7 +15,7 @@ $command_wrapper rm -fv dmlc-core/include/dmlc/build_config_default.h # include/dmlc/build_config_default.h. echo "--- Build libxgboost from the source" $command_wrapper tests/ci_build/build_via_cmake.sh -DCMAKE_PREFIX_PATH=/opt/grpc \ - -DPLUGIN_DENSE_PARSER=ON -DPLUGIN_FEDERATED=ON + -DPLUGIN_FEDERATED=ON echo "--- Run Google Test" $command_wrapper bash -c "cd build && ctest --extra-verbose" echo "--- Stash XGBoost CLI executable" From 8fe1a2213c063b633272d217f3b020671f462953 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sat, 25 Nov 2023 09:10:56 +0800 Subject: [PATCH 017/109] Cleanup code for distributed training. (#9805) * Cleanup code for distributed training. - Merge `GetNcclResult` into nccl stub. - Split up utilities from the main dask module. - Let Channel return `Result` to accommodate nccl channel. - Remove old `use_label_encoder` parameter. --- python-package/xgboost/dask/__init__.py | 73 +++++++++-------- python-package/xgboost/dask/utils.py | 24 ++++++ python-package/xgboost/sklearn.py | 1 - python-package/xgboost/spark/core.py | 1 - src/c_api/c_api.cu | 5 +- src/collective/allgather.cc | 51 ++++++------ src/collective/allreduce.cc | 8 +- src/collective/broadcast.cc | 9 ++- src/collective/coll.cu | 29 ++++--- src/collective/comm.cu | 14 ++-- src/collective/comm.cuh | 29 +------ src/collective/comm.h | 14 ++-- src/collective/nccl_device_communicator.cu | 29 +++---- src/collective/nccl_device_communicator.cuh | 2 +- src/collective/nccl_stub.cc | 26 +++++- src/collective/nccl_stub.h | 79 ++++++++----------- src/common/device_helpers.cuh | 4 - tests/cpp/collective/test_comm.cc | 13 +-- .../test_nccl_device_communicator.cu | 2 +- 19 files changed, 221 insertions(+), 192 deletions(-) create mode 100644 python-package/xgboost/dask/utils.py diff --git a/python-package/xgboost/dask/__init__.py b/python-package/xgboost/dask/__init__.py index a58c0f225..068b1e6ea 100644 --- a/python-package/xgboost/dask/__init__.py +++ b/python-package/xgboost/dask/__init__.py @@ -94,6 +94,8 @@ from xgboost.sklearn import ( from xgboost.tracker import RabitTracker, get_host_ip from xgboost.training import train as worker_train +from .utils import get_n_threads + if TYPE_CHECKING: import dask import distributed @@ -908,6 +910,34 @@ async def _check_workers_are_alive( raise RuntimeError(f"Missing required workers: {missing_workers}") +def _get_dmatrices( + train_ref: dict, + train_id: int, + *refs: dict, + evals_id: Sequence[int], + evals_name: Sequence[str], + n_threads: int, +) -> Tuple[DMatrix, List[Tuple[DMatrix, str]]]: + Xy = _dmatrix_from_list_of_parts(**train_ref, nthread=n_threads) + evals: List[Tuple[DMatrix, str]] = [] + for i, ref in enumerate(refs): + if evals_id[i] == train_id: + evals.append((Xy, evals_name[i])) + continue + if ref.get("ref", None) is not None: + if ref["ref"] != train_id: + raise ValueError( + "The training DMatrix should be used as a reference to evaluation" + " `QuantileDMatrix`." + ) + del ref["ref"] + eval_Xy = _dmatrix_from_list_of_parts(**ref, nthread=n_threads, ref=Xy) + else: + eval_Xy = _dmatrix_from_list_of_parts(**ref, nthread=n_threads) + evals.append((eval_Xy, evals_name[i])) + return Xy, evals + + async def _train_async( client: "distributed.Client", global_config: Dict[str, Any], @@ -940,41 +970,20 @@ async def _train_async( ) -> Optional[TrainReturnT]: worker = distributed.get_worker() local_param = parameters.copy() - n_threads = 0 - # dask worker nthreads, "state" is available in 2022.6.1 - dwnt = worker.state.nthreads if hasattr(worker, "state") else worker.nthreads - for p in ["nthread", "n_jobs"]: - if ( - local_param.get(p, None) is not None - and local_param.get(p, dwnt) != dwnt - ): - LOGGER.info("Overriding `nthreads` defined in dask worker.") - n_threads = local_param[p] - break - if n_threads == 0 or n_threads is None: - n_threads = dwnt + n_threads = get_n_threads(local_param, worker) local_param.update({"nthread": n_threads, "n_jobs": n_threads}) + local_history: TrainingCallback.EvalsLog = {} + with CommunicatorContext(**rabit_args), config.config_context(**global_config): - Xy = _dmatrix_from_list_of_parts(**train_ref, nthread=n_threads) - evals: List[Tuple[DMatrix, str]] = [] - for i, ref in enumerate(refs): - if evals_id[i] == train_id: - evals.append((Xy, evals_name[i])) - continue - if ref.get("ref", None) is not None: - if ref["ref"] != train_id: - raise ValueError( - "The training DMatrix should be used as a reference" - " to evaluation `QuantileDMatrix`." - ) - del ref["ref"] - eval_Xy = _dmatrix_from_list_of_parts( - **ref, nthread=n_threads, ref=Xy - ) - else: - eval_Xy = _dmatrix_from_list_of_parts(**ref, nthread=n_threads) - evals.append((eval_Xy, evals_name[i])) + Xy, evals = _get_dmatrices( + train_ref, + train_id, + *refs, + evals_id=evals_id, + evals_name=evals_name, + n_threads=n_threads, + ) booster = worker_train( params=local_param, diff --git a/python-package/xgboost/dask/utils.py b/python-package/xgboost/dask/utils.py new file mode 100644 index 000000000..98e6029b5 --- /dev/null +++ b/python-package/xgboost/dask/utils.py @@ -0,0 +1,24 @@ +"""Utilities for the XGBoost Dask interface.""" +import logging +from typing import TYPE_CHECKING, Any, Dict + +LOGGER = logging.getLogger("[xgboost.dask]") + + +if TYPE_CHECKING: + import distributed + + +def get_n_threads(local_param: Dict[str, Any], worker: "distributed.Worker") -> int: + """Get the number of threads from a worker and the user-supplied parameters.""" + # dask worker nthreads, "state" is available in 2022.6.1 + dwnt = worker.state.nthreads if hasattr(worker, "state") else worker.nthreads + n_threads = None + for p in ["nthread", "n_jobs"]: + if local_param.get(p, None) is not None and local_param.get(p, dwnt) != dwnt: + LOGGER.info("Overriding `nthreads` defined in dask worker.") + n_threads = local_param[p] + break + if n_threads == 0 or n_threads is None: + n_threads = dwnt + return n_threads diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index 3906973a8..ea309bd94 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -808,7 +808,6 @@ class XGBModel(XGBModelBase): "kwargs", "missing", "n_estimators", - "use_label_encoder", "enable_categorical", "early_stopping_rounds", "callbacks", diff --git a/python-package/xgboost/spark/core.py b/python-package/xgboost/spark/core.py index aa8c5b998..7ac01ff07 100644 --- a/python-package/xgboost/spark/core.py +++ b/python-package/xgboost/spark/core.py @@ -138,7 +138,6 @@ _inverse_pyspark_param_alias_map = {v: k for k, v in _pyspark_param_alias_map.it _unsupported_xgb_params = [ "gpu_id", # we have "device" pyspark param instead. "enable_categorical", # Use feature_types param to specify categorical feature instead - "use_label_encoder", "n_jobs", # Do not allow user to set it, will use `spark.task.cpus` value instead. "nthread", # Ditto ] diff --git a/src/c_api/c_api.cu b/src/c_api/c_api.cu index 4ace8b7cc..47868f466 100644 --- a/src/c_api/c_api.cu +++ b/src/c_api/c_api.cu @@ -1,5 +1,5 @@ /** - * Copyright 2019-2023 by XGBoost Contributors + * Copyright 2019-2023, XGBoost Contributors */ #include // for transform @@ -15,6 +15,9 @@ #include "xgboost/data.h" #include "xgboost/json.h" #include "xgboost/learner.h" +#if defined(XGBOOST_USE_NCCL) +#include +#endif namespace xgboost { void XGBBuildInfoDevice(Json *p_info) { diff --git a/src/collective/allgather.cc b/src/collective/allgather.cc index fa369a9da..148cb6cd2 100644 --- a/src/collective/allgather.cc +++ b/src/collective/allgather.cc @@ -26,18 +26,19 @@ Result RingAllgather(Comm const& comm, common::Span data, std::size } for (std::int32_t r = 0; r < world; ++r) { - auto send_rank = (rank + world - r + worker_off) % world; - auto send_off = send_rank * segment_size; - send_off = std::min(send_off, data.size_bytes()); - auto send_seg = data.subspan(send_off, std::min(segment_size, data.size_bytes() - send_off)); - next_ch->SendAll(send_seg.data(), send_seg.size_bytes()); - - auto recv_rank = (rank + world - r - 1 + worker_off) % world; - auto recv_off = recv_rank * segment_size; - recv_off = std::min(recv_off, data.size_bytes()); - auto recv_seg = data.subspan(recv_off, std::min(segment_size, data.size_bytes() - recv_off)); - prev_ch->RecvAll(recv_seg.data(), recv_seg.size_bytes()); - auto rc = prev_ch->Block(); + auto rc = Success() << [&] { + auto send_rank = (rank + world - r + worker_off) % world; + auto send_off = send_rank * segment_size; + send_off = std::min(send_off, data.size_bytes()); + auto send_seg = data.subspan(send_off, std::min(segment_size, data.size_bytes() - send_off)); + return next_ch->SendAll(send_seg.data(), send_seg.size_bytes()); + } << [&] { + auto recv_rank = (rank + world - r - 1 + worker_off) % world; + auto recv_off = recv_rank * segment_size; + recv_off = std::min(recv_off, data.size_bytes()); + auto recv_seg = data.subspan(recv_off, std::min(segment_size, data.size_bytes() - recv_off)); + return prev_ch->RecvAll(recv_seg.data(), recv_seg.size_bytes()); + } << [&] { return prev_ch->Block(); }; if (!rc.OK()) { return rc; } @@ -78,19 +79,19 @@ namespace detail { auto next_ch = comm.Chan(next); for (std::int32_t r = 0; r < world; ++r) { - auto send_rank = (rank + world - r) % world; - auto send_off = offset[send_rank]; - auto send_size = sizes[send_rank]; - auto send_seg = erased_result.subspan(send_off, send_size); - next_ch->SendAll(send_seg); - - auto recv_rank = (rank + world - r - 1) % world; - auto recv_off = offset[recv_rank]; - auto recv_size = sizes[recv_rank]; - auto recv_seg = erased_result.subspan(recv_off, recv_size); - prev_ch->RecvAll(recv_seg.data(), recv_seg.size_bytes()); - - auto rc = prev_ch->Block(); + auto rc = Success() << [&] { + auto send_rank = (rank + world - r) % world; + auto send_off = offset[send_rank]; + auto send_size = sizes[send_rank]; + auto send_seg = erased_result.subspan(send_off, send_size); + return next_ch->SendAll(send_seg); + } << [&] { + auto recv_rank = (rank + world - r - 1) % world; + auto recv_off = offset[recv_rank]; + auto recv_size = sizes[recv_rank]; + auto recv_seg = erased_result.subspan(recv_off, recv_size); + return prev_ch->RecvAll(recv_seg.data(), recv_seg.size_bytes()); + } << [&] { return prev_ch->Block(); }; if (!rc.OK()) { return rc; } diff --git a/src/collective/allreduce.cc b/src/collective/allreduce.cc index f95a9a9f1..93b76355f 100644 --- a/src/collective/allreduce.cc +++ b/src/collective/allreduce.cc @@ -37,7 +37,10 @@ Result RingScatterReduceTyped(Comm const& comm, common::Span data, auto seg_nbytes = std::min(data.size_bytes() - send_off, n_bytes_in_seg); auto send_seg = data.subspan(send_off, seg_nbytes); - next_ch->SendAll(send_seg); + auto rc = next_ch->SendAll(send_seg); + if (!rc.OK()) { + return rc; + } // receive from ring prev auto recv_off = ((rank + world - r - 1) % world) * n_bytes_in_seg; @@ -47,8 +50,7 @@ Result RingScatterReduceTyped(Comm const& comm, common::Span data, auto recv_seg = data.subspan(recv_off, seg_nbytes); auto seg = s_buf.subspan(0, recv_seg.size()); - prev_ch->RecvAll(seg); - auto rc = comm.Block(); + rc = std::move(rc) << [&] { return prev_ch->RecvAll(seg); } << [&] { return comm.Block(); }; if (!rc.OK()) { return rc; } diff --git a/src/collective/broadcast.cc b/src/collective/broadcast.cc index 660bb9130..e1ef60f86 100644 --- a/src/collective/broadcast.cc +++ b/src/collective/broadcast.cc @@ -62,8 +62,8 @@ Result Broadcast(Comm const& comm, common::Span data, std::int32_t if (shifted_rank != 0) { // not root auto parent = ShiftRight(ShiftedParentRank(shifted_rank, depth), world, root); - comm.Chan(parent)->RecvAll(data); - auto rc = comm.Chan(parent)->Block(); + auto rc = Success() << [&] { return comm.Chan(parent)->RecvAll(data); } + << [&] { return comm.Chan(parent)->Block(); }; if (!rc.OK()) { return Fail("broadcast failed.", std::move(rc)); } @@ -75,7 +75,10 @@ Result Broadcast(Comm const& comm, common::Span data, std::int32_t auto sft_peer = shifted_rank + (1 << i); auto peer = ShiftRight(sft_peer, world, root); CHECK_NE(peer, root); - comm.Chan(peer)->SendAll(data); + auto rc = comm.Chan(peer)->SendAll(data); + if (!rc.OK()) { + return rc; + } } } diff --git a/src/collective/coll.cu b/src/collective/coll.cu index 60072b6a5..d1b66a8ce 100644 --- a/src/collective/coll.cu +++ b/src/collective/coll.cu @@ -79,8 +79,8 @@ void RunBitwiseAllreduce(dh::CUDAStreamView stream, common::Span ou // First gather data from all the workers. CHECK(handle); - auto rc = GetNCCLResult(stub, stub->Allgather(data.data(), device_buffer, data.size(), ncclInt8, - handle, pcomm->Stream())); + auto rc = + stub->Allgather(data.data(), device_buffer, data.size(), ncclInt8, handle, pcomm->Stream()); if (!rc.OK()) { return rc; } @@ -140,9 +140,8 @@ ncclRedOp_t GetNCCLRedOp(Op const& op) { return DispatchDType(type, [=](auto t) { using T = decltype(t); auto rdata = common::RestoreType(data); - auto rc = stub->Allreduce(data.data(), data.data(), rdata.size(), GetNCCLType(type), - GetNCCLRedOp(op), nccl->Handle(), nccl->Stream()); - return GetNCCLResult(stub, rc); + return stub->Allreduce(data.data(), data.data(), rdata.size(), GetNCCLType(type), + GetNCCLRedOp(op), nccl->Handle(), nccl->Stream()); }); } } << [&] { return nccl->Block(); }; @@ -158,8 +157,8 @@ ncclRedOp_t GetNCCLRedOp(Op const& op) { auto stub = nccl->Stub(); return Success() << [&] { - return GetNCCLResult(stub, stub->Broadcast(data.data(), data.data(), data.size_bytes(), - ncclInt8, root, nccl->Handle(), nccl->Stream())); + return stub->Broadcast(data.data(), data.data(), data.size_bytes(), ncclInt8, root, + nccl->Handle(), nccl->Stream()); } << [&] { return nccl->Block(); }; } @@ -174,8 +173,8 @@ ncclRedOp_t GetNCCLRedOp(Op const& op) { auto send = data.subspan(comm.Rank() * size, size); return Success() << [&] { - return GetNCCLResult(stub, stub->Allgather(send.data(), data.data(), size, ncclInt8, - nccl->Handle(), nccl->Stream())); + return stub->Allgather(send.data(), data.data(), size, ncclInt8, nccl->Handle(), + nccl->Stream()); } << [&] { return nccl->Block(); }; } @@ -188,19 +187,19 @@ namespace cuda_impl { Result BroadcastAllgatherV(NCCLComm const* comm, common::Span data, common::Span sizes, common::Span recv) { auto stub = comm->Stub(); - return Success() << [&stub] { return GetNCCLResult(stub, stub->GroupStart()); } << [&] { + return Success() << [&stub] { return stub->GroupStart(); } << [&] { std::size_t offset = 0; for (std::int32_t r = 0; r < comm->World(); ++r) { auto as_bytes = sizes[r]; auto rc = stub->Broadcast(data.data(), recv.subspan(offset, as_bytes).data(), as_bytes, ncclInt8, r, comm->Handle(), dh::DefaultStream()); - if (rc != ncclSuccess) { - return GetNCCLResult(stub, rc); + if (!rc.OK()) { + return rc; } offset += as_bytes; } return Success(); - } << [&] { return GetNCCLResult(stub, stub->GroupEnd()); }; + } << [&] { return stub->GroupEnd(); }; } } // namespace cuda_impl @@ -217,7 +216,7 @@ Result BroadcastAllgatherV(NCCLComm const* comm, common::Span switch (algo) { case AllgatherVAlgo::kRing: { - return Success() << [&] { return GetNCCLResult(stub, stub->GroupStart()); } << [&] { + return Success() << [&] { return stub->GroupStart(); } << [&] { // get worker offset detail::AllgatherVOffset(sizes, recv_segments); // copy data @@ -228,7 +227,7 @@ Result BroadcastAllgatherV(NCCLComm const* comm, common::Span } return detail::RingAllgatherV(comm, sizes, recv_segments, recv); } << [&] { - return GetNCCLResult(stub, stub->GroupEnd()); + return stub->GroupEnd(); } << [&] { return nccl->Block(); }; } case AllgatherVAlgo::kBcast: { diff --git a/src/collective/comm.cu b/src/collective/comm.cu index cc67def0a..56681253c 100644 --- a/src/collective/comm.cu +++ b/src/collective/comm.cu @@ -26,7 +26,7 @@ Result GetUniqueId(Comm const& comm, std::shared_ptr stub, std::shared static const int kRootRank = 0; ncclUniqueId id; if (comm.Rank() == kRootRank) { - auto rc = GetNCCLResult(stub, stub->GetUniqueId(&id)); + auto rc = stub->GetUniqueId(&id); CHECK(rc.OK()) << rc.Report(); } auto rc = coll->Broadcast( @@ -99,12 +99,10 @@ NCCLComm::NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr p << "Multiple processes within communication group running on same CUDA " << "device is not supported. " << PrintUUID(s_this_uuid) << "\n"; - rc = std::move(rc) << [&] { - return GetUniqueId(root, this->stub_, pimpl, &nccl_unique_id_); - } << [&] { - return GetNCCLResult(this->stub_, this->stub_->CommInitRank(&nccl_comm_, root.World(), - nccl_unique_id_, root.Rank())); - }; + rc = std::move(rc) << [&] { return GetUniqueId(root, this->stub_, pimpl, &nccl_unique_id_); } << + [&] { + return this->stub_->CommInitRank(&nccl_comm_, root.World(), nccl_unique_id_, root.Rank()); + }; CHECK(rc.OK()) << rc.Report(); for (std::int32_t r = 0; r < root.World(); ++r) { @@ -115,7 +113,7 @@ NCCLComm::NCCLComm(Context const* ctx, Comm const& root, std::shared_ptr p NCCLComm::~NCCLComm() { if (nccl_comm_) { - auto rc = GetNCCLResult(stub_, stub_->CommDestroy(nccl_comm_)); + auto rc = stub_->CommDestroy(nccl_comm_); CHECK(rc.OK()) << rc.Report(); } } diff --git a/src/collective/comm.cuh b/src/collective/comm.cuh index ef537b5a9..a818d95f8 100644 --- a/src/collective/comm.cuh +++ b/src/collective/comm.cuh @@ -52,25 +52,6 @@ class NCCLComm : public Comm { } }; -inline Result GetNCCLResult(std::shared_ptr stub, ncclResult_t code) { - if (code == ncclSuccess) { - return Success(); - } - - std::stringstream ss; - ss << "NCCL failure: " << stub->GetErrorString(code) << "."; - if (code == ncclUnhandledCudaError) { - // nccl usually preserves the last error so we can get more details. - auto err = cudaPeekAtLastError(); - ss << " CUDA error: " << thrust::system_error(err, thrust::cuda_category()).what() << "\n"; - } else if (code == ncclSystemError) { - ss << " This might be caused by a network configuration issue. Please consider specifying " - "the network interface for NCCL via environment variables listed in its reference: " - "`https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html`.\n"; - } - return Fail(ss.str()); -} - class NCCLChannel : public Channel { std::int32_t rank_{-1}; ncclComm_t nccl_comm_{}; @@ -86,13 +67,11 @@ class NCCLChannel : public Channel { Channel{comm, nullptr}, stream_{stream} {} - void SendAll(std::int8_t const* ptr, std::size_t n) override { - auto rc = GetNCCLResult(stub_, stub_->Send(ptr, n, ncclInt8, rank_, nccl_comm_, stream_)); - CHECK(rc.OK()) << rc.Report(); + [[nodiscard]] Result SendAll(std::int8_t const* ptr, std::size_t n) override { + return stub_->Send(ptr, n, ncclInt8, rank_, nccl_comm_, stream_); } - void RecvAll(std::int8_t* ptr, std::size_t n) override { - auto rc = GetNCCLResult(stub_, stub_->Recv(ptr, n, ncclInt8, rank_, nccl_comm_, stream_)); - CHECK(rc.OK()) << rc.Report(); + [[nodiscard]] Result RecvAll(std::int8_t* ptr, std::size_t n) override { + return stub_->Recv(ptr, n, ncclInt8, rank_, nccl_comm_, stream_); } [[nodiscard]] Result Block() override { auto rc = stream_.Sync(false); diff --git a/src/collective/comm.h b/src/collective/comm.h index b2f519e3d..82aa2c45e 100644 --- a/src/collective/comm.h +++ b/src/collective/comm.h @@ -135,21 +135,25 @@ class Channel { explicit Channel(Comm const& comm, std::shared_ptr sock) : sock_{std::move(sock)}, comm_{comm} {} - virtual void SendAll(std::int8_t const* ptr, std::size_t n) { + [[nodiscard]] virtual Result SendAll(std::int8_t const* ptr, std::size_t n) { Loop::Op op{Loop::Op::kWrite, comm_.Rank(), const_cast(ptr), n, sock_.get(), 0}; CHECK(sock_.get()); comm_.Submit(std::move(op)); + return Success(); } - void SendAll(common::Span data) { - this->SendAll(data.data(), data.size_bytes()); + [[nodiscard]] Result SendAll(common::Span data) { + return this->SendAll(data.data(), data.size_bytes()); } - virtual void RecvAll(std::int8_t* ptr, std::size_t n) { + [[nodiscard]] virtual Result RecvAll(std::int8_t* ptr, std::size_t n) { Loop::Op op{Loop::Op::kRead, comm_.Rank(), ptr, n, sock_.get(), 0}; CHECK(sock_.get()); comm_.Submit(std::move(op)); + return Success(); + } + [[nodiscard]] Result RecvAll(common::Span data) { + return this->RecvAll(data.data(), data.size_bytes()); } - void RecvAll(common::Span data) { this->RecvAll(data.data(), data.size_bytes()); } [[nodiscard]] auto Socket() const { return sock_; } [[nodiscard]] virtual Result Block() { return comm_.Block(); } diff --git a/src/collective/nccl_device_communicator.cu b/src/collective/nccl_device_communicator.cu index 25b198bde..31c2d394d 100644 --- a/src/collective/nccl_device_communicator.cu +++ b/src/collective/nccl_device_communicator.cu @@ -46,8 +46,7 @@ NcclDeviceCommunicator::NcclDeviceCommunicator(int device_ordinal, bool needs_sy nccl_unique_id_ = GetUniqueId(); dh::safe_cuda(cudaSetDevice(device_ordinal_)); - auto rc = - GetNCCLResult(stub_, stub_->CommInitRank(&nccl_comm_, world_size_, nccl_unique_id_, rank_)); + auto rc = stub_->CommInitRank(&nccl_comm_, world_size_, nccl_unique_id_, rank_); CHECK(rc.OK()) << rc.Report(); } @@ -56,7 +55,7 @@ NcclDeviceCommunicator::~NcclDeviceCommunicator() { return; } if (nccl_comm_) { - auto rc = GetNCCLResult(stub_, stub_->CommDestroy(nccl_comm_)); + auto rc = stub_->CommDestroy(nccl_comm_); CHECK(rc.OK()) << rc.Report(); } if (xgboost::ConsoleLogger::ShouldLog(xgboost::ConsoleLogger::LV::kDebug)) { @@ -143,9 +142,8 @@ void NcclDeviceCommunicator::BitwiseAllReduce(void *send_receive_buffer, std::si auto *device_buffer = buffer.data().get(); // First gather data from all the workers. - auto rc = GetNCCLResult( - stub_, stub_->Allgather(send_receive_buffer, device_buffer, count, GetNcclDataType(data_type), - nccl_comm_, dh::DefaultStream())); + auto rc = stub_->Allgather(send_receive_buffer, device_buffer, count, GetNcclDataType(data_type), + nccl_comm_, dh::DefaultStream()); CHECK(rc.OK()) << rc.Report(); if (needs_sync_) { dh::DefaultStream().Sync(); @@ -178,9 +176,9 @@ void NcclDeviceCommunicator::AllReduce(void *send_receive_buffer, std::size_t co if (IsBitwiseOp(op)) { BitwiseAllReduce(send_receive_buffer, count, data_type, op); } else { - auto rc = GetNCCLResult(stub_, stub_->Allreduce(send_receive_buffer, send_receive_buffer, count, - GetNcclDataType(data_type), GetNcclRedOp(op), - nccl_comm_, dh::DefaultStream())); + auto rc = stub_->Allreduce(send_receive_buffer, send_receive_buffer, count, + GetNcclDataType(data_type), GetNcclRedOp(op), nccl_comm_, + dh::DefaultStream()); CHECK(rc.OK()) << rc.Report(); } allreduce_bytes_ += count * GetTypeSize(data_type); @@ -194,8 +192,8 @@ void NcclDeviceCommunicator::AllGather(void const *send_buffer, void *receive_bu } dh::safe_cuda(cudaSetDevice(device_ordinal_)); - auto rc = GetNCCLResult(stub_, stub_->Allgather(send_buffer, receive_buffer, send_size, ncclInt8, - nccl_comm_, dh::DefaultStream())); + auto rc = stub_->Allgather(send_buffer, receive_buffer, send_size, ncclInt8, nccl_comm_, + dh::DefaultStream()); CHECK(rc.OK()) << rc.Report(); } @@ -216,19 +214,18 @@ void NcclDeviceCommunicator::AllGatherV(void const *send_buffer, size_t length_b receive_buffer->resize(total_bytes); size_t offset = 0; - auto rc = Success() << [&] { return GetNCCLResult(stub_, stub_->GroupStart()); } << [&] { + auto rc = Success() << [&] { return stub_->GroupStart(); } << [&] { for (int32_t i = 0; i < world_size_; ++i) { size_t as_bytes = segments->at(i); - auto rc = GetNCCLResult( - stub_, stub_->Broadcast(send_buffer, receive_buffer->data().get() + offset, as_bytes, - ncclChar, i, nccl_comm_, dh::DefaultStream())); + auto rc = stub_->Broadcast(send_buffer, receive_buffer->data().get() + offset, as_bytes, + ncclChar, i, nccl_comm_, dh::DefaultStream()); if (!rc.OK()) { return rc; } offset += as_bytes; } return Success(); - } << [&] { return GetNCCLResult(stub_, stub_->GroupEnd()); }; + } << [&] { return stub_->GroupEnd(); }; } void NcclDeviceCommunicator::Synchronize() { diff --git a/src/collective/nccl_device_communicator.cuh b/src/collective/nccl_device_communicator.cuh index a194b4ef2..ef431b571 100644 --- a/src/collective/nccl_device_communicator.cuh +++ b/src/collective/nccl_device_communicator.cuh @@ -66,7 +66,7 @@ class NcclDeviceCommunicator : public DeviceCommunicator { static const int kRootRank = 0; ncclUniqueId id; if (rank_ == kRootRank) { - auto rc = GetNCCLResult(stub_, stub_->GetUniqueId(&id)); + auto rc = stub_->GetUniqueId(&id); CHECK(rc.OK()) << rc.Report(); } Broadcast(static_cast(&id), sizeof(ncclUniqueId), static_cast(kRootRank)); diff --git a/src/collective/nccl_stub.cc b/src/collective/nccl_stub.cc index f4705a46e..fea3f2755 100644 --- a/src/collective/nccl_stub.cc +++ b/src/collective/nccl_stub.cc @@ -4,9 +4,12 @@ #if defined(XGBOOST_USE_NCCL) #include "nccl_stub.h" -#include // for CUDA_VERSION -#include // for dlclose, dlsym, dlopen +#include // for CUDA_VERSION +#include // for cudaPeekAtLastError +#include // for dlclose, dlsym, dlopen #include +#include // for cuda_category +#include // for system_error #include // for int32_t #include // for stringstream @@ -16,6 +19,25 @@ #include "xgboost/logging.h" namespace xgboost::collective { +Result NcclStub::GetNcclResult(ncclResult_t code) const { + if (code == ncclSuccess) { + return Success(); + } + + std::stringstream ss; + ss << "NCCL failure: " << this->GetErrorString(code) << "."; + if (code == ncclUnhandledCudaError) { + // nccl usually preserves the last error so we can get more details. + auto err = cudaPeekAtLastError(); + ss << " CUDA error: " << thrust::system_error(err, thrust::cuda_category()).what() << "\n"; + } else if (code == ncclSystemError) { + ss << " This might be caused by a network configuration issue. Please consider specifying " + "the network interface for NCCL via environment variables listed in its reference: " + "`https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html`.\n"; + } + return Fail(ss.str()); +} + NcclStub::NcclStub(StringView path) : path_{std::move(path)} { #if defined(XGBOOST_USE_DLOPEN_NCCL) CHECK(!path_.empty()) << "Empty path for NCCL."; diff --git a/src/collective/nccl_stub.h b/src/collective/nccl_stub.h index a003a6f22..5281b736d 100644 --- a/src/collective/nccl_stub.h +++ b/src/collective/nccl_stub.h @@ -8,9 +8,13 @@ #include // for string -#include "xgboost/string_view.h" // for StringView +#include "xgboost/collective/result.h" // for Result +#include "xgboost/string_view.h" // for StringView namespace xgboost::collective { +/** + * @brief A stub for NCCL to facilitate dynamic loading. + */ class NcclStub { #if defined(XGBOOST_USE_DLOPEN_NCCL) void* handle_{nullptr}; @@ -30,61 +34,48 @@ class NcclStub { decltype(ncclGetErrorString)* get_error_string_{nullptr}; decltype(ncclGetVersion)* get_version_{nullptr}; + public: + Result GetNcclResult(ncclResult_t code) const; + public: explicit NcclStub(StringView path); ~NcclStub(); - [[nodiscard]] ncclResult_t Allreduce(const void* sendbuff, void* recvbuff, size_t count, - ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, - cudaStream_t stream) const { - CHECK(allreduce_); - return this->allreduce_(sendbuff, recvbuff, count, datatype, op, comm, stream); + [[nodiscard]] Result Allreduce(const void* sendbuff, void* recvbuff, size_t count, + ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, + cudaStream_t stream) const { + return this->GetNcclResult(allreduce_(sendbuff, recvbuff, count, datatype, op, comm, stream)); } - [[nodiscard]] ncclResult_t Broadcast(const void* sendbuff, void* recvbuff, size_t count, - ncclDataType_t datatype, int root, ncclComm_t comm, - cudaStream_t stream) const { - CHECK(broadcast_); - return this->broadcast_(sendbuff, recvbuff, count, datatype, root, comm, stream); + [[nodiscard]] Result Broadcast(const void* sendbuff, void* recvbuff, size_t count, + ncclDataType_t datatype, int root, ncclComm_t comm, + cudaStream_t stream) const { + return this->GetNcclResult(broadcast_(sendbuff, recvbuff, count, datatype, root, comm, stream)); } - [[nodiscard]] ncclResult_t Allgather(const void* sendbuff, void* recvbuff, size_t sendcount, - ncclDataType_t datatype, ncclComm_t comm, - cudaStream_t stream) const { - CHECK(allgather_); - return this->allgather_(sendbuff, recvbuff, sendcount, datatype, comm, stream); + [[nodiscard]] Result Allgather(const void* sendbuff, void* recvbuff, size_t sendcount, + ncclDataType_t datatype, ncclComm_t comm, + cudaStream_t stream) const { + return this->GetNcclResult(allgather_(sendbuff, recvbuff, sendcount, datatype, comm, stream)); } - [[nodiscard]] ncclResult_t CommInitRank(ncclComm_t* comm, int nranks, ncclUniqueId commId, - int rank) const { - CHECK(comm_init_rank_); - return this->comm_init_rank_(comm, nranks, commId, rank); + [[nodiscard]] Result CommInitRank(ncclComm_t* comm, int nranks, ncclUniqueId commId, + int rank) const { + return this->GetNcclResult(this->comm_init_rank_(comm, nranks, commId, rank)); } - [[nodiscard]] ncclResult_t CommDestroy(ncclComm_t comm) const { - CHECK(comm_destroy_); - return this->comm_destroy_(comm); + [[nodiscard]] Result CommDestroy(ncclComm_t comm) const { + return this->GetNcclResult(comm_destroy_(comm)); } - - [[nodiscard]] ncclResult_t GetUniqueId(ncclUniqueId* uniqueId) const { - CHECK(get_uniqueid_); - return this->get_uniqueid_(uniqueId); + [[nodiscard]] Result GetUniqueId(ncclUniqueId* uniqueId) const { + return this->GetNcclResult(get_uniqueid_(uniqueId)); } - [[nodiscard]] ncclResult_t Send(const void* sendbuff, size_t count, ncclDataType_t datatype, - int peer, ncclComm_t comm, cudaStream_t stream) { - CHECK(send_); - return send_(sendbuff, count, datatype, peer, comm, stream); + [[nodiscard]] Result Send(const void* sendbuff, size_t count, ncclDataType_t datatype, int peer, + ncclComm_t comm, cudaStream_t stream) { + return this->GetNcclResult(send_(sendbuff, count, datatype, peer, comm, stream)); } - [[nodiscard]] ncclResult_t Recv(void* recvbuff, size_t count, ncclDataType_t datatype, int peer, - ncclComm_t comm, cudaStream_t stream) const { - CHECK(recv_); - return recv_(recvbuff, count, datatype, peer, comm, stream); + [[nodiscard]] Result Recv(void* recvbuff, size_t count, ncclDataType_t datatype, int peer, + ncclComm_t comm, cudaStream_t stream) const { + return this->GetNcclResult(recv_(recvbuff, count, datatype, peer, comm, stream)); } - [[nodiscard]] ncclResult_t GroupStart() const { - CHECK(group_start_); - return group_start_(); - } - [[nodiscard]] ncclResult_t GroupEnd() const { - CHECK(group_end_); - return group_end_(); - } - + [[nodiscard]] Result GroupStart() const { return this->GetNcclResult(group_start_()); } + [[nodiscard]] Result GroupEnd() const { return this->GetNcclResult(group_end_()); } [[nodiscard]] const char* GetErrorString(ncclResult_t result) const { return get_error_string_(result); } diff --git a/src/common/device_helpers.cuh b/src/common/device_helpers.cuh index 89ec42f2b..ffe61800e 100644 --- a/src/common/device_helpers.cuh +++ b/src/common/device_helpers.cuh @@ -36,10 +36,6 @@ #include "xgboost/logging.h" #include "xgboost/span.h" -#ifdef XGBOOST_USE_NCCL -#include "nccl.h" -#endif // XGBOOST_USE_NCCL - #if defined(XGBOOST_USE_RMM) && XGBOOST_USE_RMM == 1 #include "rmm/mr/device/per_device_resource.hpp" #include "rmm/mr/device/thrust_allocator_adaptor.hpp" diff --git a/tests/cpp/collective/test_comm.cc b/tests/cpp/collective/test_comm.cc index 52fec7b5d..8e69b2f8e 100644 --- a/tests/cpp/collective/test_comm.cc +++ b/tests/cpp/collective/test_comm.cc @@ -25,15 +25,18 @@ TEST_F(CommTest, Channel) { WorkerForTest worker{host, port, timeout, n_workers, i}; if (i % 2 == 0) { auto p_chan = worker.Comm().Chan(i + 1); - p_chan->SendAll( - EraseType(common::Span{&i, static_cast(1)})); - auto rc = p_chan->Block(); + auto rc = Success() << [&] { + return p_chan->SendAll( + EraseType(common::Span{&i, static_cast(1)})); + } << [&] { return p_chan->Block(); }; ASSERT_TRUE(rc.OK()) << rc.Report(); } else { auto p_chan = worker.Comm().Chan(i - 1); std::int32_t r{-1}; - p_chan->RecvAll(EraseType(common::Span{&r, static_cast(1)})); - auto rc = p_chan->Block(); + auto rc = Success() << [&] { + return p_chan->RecvAll( + EraseType(common::Span{&r, static_cast(1)})); + } << [&] { return p_chan->Block(); }; ASSERT_TRUE(rc.OK()) << rc.Report(); ASSERT_EQ(r, i - 1); } diff --git a/tests/cpp/collective/test_nccl_device_communicator.cu b/tests/cpp/collective/test_nccl_device_communicator.cu index 3d7b1efc8..47e86220d 100644 --- a/tests/cpp/collective/test_nccl_device_communicator.cu +++ b/tests/cpp/collective/test_nccl_device_communicator.cu @@ -23,7 +23,7 @@ TEST(NcclDeviceCommunicatorSimpleTest, ThrowOnInvalidDeviceOrdinal) { TEST(NcclDeviceCommunicatorSimpleTest, SystemError) { auto stub = std::make_shared(DefaultNcclName()); - auto rc = GetNCCLResult(stub, ncclSystemError); + auto rc = stub->GetNcclResult(ncclSystemError); auto msg = rc.Report(); ASSERT_TRUE(msg.find("environment variables") != std::string::npos); } From 3f4e22015a6796a0807650d7b2f97b108f8760df Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sat, 25 Nov 2023 11:25:47 +0800 Subject: [PATCH 018/109] Mark NCCL python test optional. (#9804) Skip the tests if XGBoost is not compiled with dlopen. --- .../test_gpu_with_dask/test_gpu_with_dask.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py index 883dbbaf2..f25ac9fb0 100644 --- a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py +++ b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py @@ -573,6 +573,10 @@ def test_with_asyncio(local_cuda_client: Client) -> None: assert isinstance(output["history"], dict) +@pytest.mark.skipif( + condition=not xgb.build_info()["USE_DLOPEN_NCCL"], + reason="Not compiled with dlopen.", +) def test_invalid_nccl(local_cuda_client: Client) -> None: client = local_cuda_client workers = tm.get_client_workers(client) @@ -592,6 +596,10 @@ def test_invalid_nccl(local_cuda_client: Client) -> None: client.gather(futures) +@pytest.mark.skipif( + condition=not xgb.build_info()["USE_DLOPEN_NCCL"], + reason="Not compiled with dlopen.", +) @pytest.mark.parametrize("tree_method", ["hist", "approx"]) def test_nccl_load(local_cuda_client: Client, tree_method: str) -> None: X, y, w = tm.make_regression(128, 16, use_cupy=True) From e9f149481e64d7d97cf37e95433ea6f6674d4dbf Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Mon, 27 Nov 2023 17:19:01 +0800 Subject: [PATCH 019/109] [sklearn] Fix loading model attributes. (#9808) --- python-package/xgboost/dask/__init__.py | 7 +-- python-package/xgboost/sklearn.py | 54 +++++++++---------- tests/python/test_with_sklearn.py | 22 ++++---- .../test_with_dask/test_with_dask.py | 7 +++ 4 files changed, 48 insertions(+), 42 deletions(-) diff --git a/python-package/xgboost/dask/__init__.py b/python-package/xgboost/dask/__init__.py index 068b1e6ea..046a2c982 100644 --- a/python-package/xgboost/dask/__init__.py +++ b/python-package/xgboost/dask/__init__.py @@ -79,7 +79,6 @@ from xgboost.data import _is_cudf_ser, _is_cupy_array from xgboost.sklearn import ( XGBClassifier, XGBClassifierBase, - XGBClassifierMixIn, XGBModel, XGBRanker, XGBRankerMixIn, @@ -1863,7 +1862,7 @@ class DaskXGBRegressor(DaskScikitLearnBase, XGBRegressorBase): "Implementation of the scikit-learn API for XGBoost classification.", ["estimators", "model"], ) -class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierMixIn, XGBClassifierBase): +class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): # pylint: disable=missing-class-docstring async def _fit_async( self, @@ -2045,10 +2044,6 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierMixIn, XGBClassifierBa preds = da.map_blocks(_argmax, pred_probs, drop_axis=1) return preds - def load_model(self, fname: ModelIn) -> None: - super().load_model(fname) - self._load_model_attributes(self.get_booster()) - @xgboost_model_doc( """Implementation of the Scikit-Learn API for XGBoost Ranking. diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index ea309bd94..748b26cf6 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -43,19 +43,6 @@ from .data import _is_cudf_df, _is_cudf_ser, _is_cupy_array, _is_pandas_df from .training import train -class XGBClassifierMixIn: # pylint: disable=too-few-public-methods - """MixIn for classification.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - def _load_model_attributes(self, booster: Booster) -> None: - config = json.loads(booster.save_config()) - self.n_classes_ = int(config["learner"]["learner_model_param"]["num_class"]) - # binary classification is treated as regression in XGBoost. - self.n_classes_ = 2 if self.n_classes_ < 2 else self.n_classes_ - - class XGBRankerMixIn: # pylint: disable=too-few-public-methods """MixIn for ranking, defines the _estimator_type usually defined in scikit-learn base classes. @@ -850,21 +837,38 @@ class XGBModel(XGBModelBase): self.get_booster().load_model(fname) meta_str = self.get_booster().attr("scikit_learn") - if meta_str is None: - return + if meta_str is not None: + meta = json.loads(meta_str) + t = meta.get("_estimator_type", None) + if t is not None and t != self._get_type(): + raise TypeError( + "Loading an estimator with different type. Expecting: " + f"{self._get_type()}, got: {t}" + ) - meta = json.loads(meta_str) - t = meta.get("_estimator_type", None) - if t is not None and t != self._get_type(): - raise TypeError( - "Loading an estimator with different type. Expecting: " - f"{self._get_type()}, got: {t}" - ) self.feature_types = self.get_booster().feature_types self.get_booster().set_attr(scikit_learn=None) + config = json.loads(self.get_booster().save_config()) + self._load_model_attributes(config) load_model.__doc__ = f"""{Booster.load_model.__doc__}""" + def _load_model_attributes(self, config: dict) -> None: + """Load model attributes without hyper-parameters.""" + from sklearn.base import is_classifier + + booster = self.get_booster() + + self.objective = config["learner"]["objective"]["name"] + self.booster = config["learner"]["gradient_booster"]["name"] + self.base_score = config["learner"]["learner_model_param"]["base_score"] + self.feature_types = booster.feature_types + + if is_classifier(self): + self.n_classes_ = int(config["learner"]["learner_model_param"]["num_class"]) + # binary classification is treated as regression in XGBoost. + self.n_classes_ = 2 if self.n_classes_ < 2 else self.n_classes_ + # pylint: disable=too-many-branches def _configure_fit( self, @@ -1414,7 +1418,7 @@ def _cls_predict_proba(n_classes: int, prediction: PredtT, vstack: Callable) -> Number of boosting rounds. """, ) -class XGBClassifier(XGBModel, XGBClassifierMixIn, XGBClassifierBase): +class XGBClassifier(XGBModel, XGBClassifierBase): # pylint: disable=missing-docstring,invalid-name,too-many-instance-attributes @_deprecate_positional_args def __init__( @@ -1642,10 +1646,6 @@ class XGBClassifier(XGBModel, XGBClassifierMixIn, XGBClassifierBase): def classes_(self) -> np.ndarray: return np.arange(self.n_classes_) - def load_model(self, fname: ModelIn) -> None: - super().load_model(fname) - self._load_model_attributes(self.get_booster()) - @xgboost_model_doc( "scikit-learn API for XGBoost random forest classification.", diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 16f7ab9d1..1e49ed053 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -944,6 +944,7 @@ def save_load_model(model_path): predt_0 = clf.predict(X) clf.save_model(model_path) clf.load_model(model_path) + assert clf.booster == "gblinear" predt_1 = clf.predict(X) np.testing.assert_allclose(predt_0, predt_1) assert clf.best_iteration == best_iteration @@ -959,25 +960,26 @@ def save_load_model(model_path): def test_save_load_model(): with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, 'digits.model') + model_path = os.path.join(tempdir, "digits.model") save_load_model(model_path) with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, 'digits.model.json') + model_path = os.path.join(tempdir, "digits.model.json") save_load_model(model_path) from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, 'digits.model.ubj') + model_path = os.path.join(tempdir, "digits.model.ubj") digits = load_digits(n_class=2) - y = digits['target'] - X = digits['data'] - booster = xgb.train({'tree_method': 'hist', - 'objective': 'binary:logistic'}, - dtrain=xgb.DMatrix(X, y), - num_boost_round=4) + y = digits["target"] + X = digits["data"] + booster = xgb.train( + {"tree_method": "hist", "objective": "binary:logistic"}, + dtrain=xgb.DMatrix(X, y), + num_boost_round=4, + ) predt_0 = booster.predict(xgb.DMatrix(X)) booster.save_model(model_path) cls = xgb.XGBClassifier() @@ -1011,6 +1013,8 @@ def test_save_load_model(): clf = xgb.XGBClassifier() clf.load_model(model_path) assert clf.classes_.size == 10 + assert clf.objective == "multi:softprob" + np.testing.assert_equal(clf.classes_, np.arange(10)) assert clf.n_classes_ == 10 diff --git a/tests/test_distributed/test_with_dask/test_with_dask.py b/tests/test_distributed/test_with_dask/test_with_dask.py index 3510dff7b..d380f0dee 100644 --- a/tests/test_distributed/test_with_dask/test_with_dask.py +++ b/tests/test_distributed/test_with_dask/test_with_dask.py @@ -1931,6 +1931,7 @@ class TestWithDask: cls.client = client cls.fit(X, y) predt_0 = cls.predict(X) + proba_0 = cls.predict_proba(X) with tempfile.TemporaryDirectory() as tmpdir: path = os.path.join(tmpdir, "model.pkl") @@ -1940,7 +1941,9 @@ class TestWithDask: with open(path, "rb") as fd: cls = pickle.load(fd) predt_1 = cls.predict(X) + proba_1 = cls.predict_proba(X) np.testing.assert_allclose(predt_0.compute(), predt_1.compute()) + np.testing.assert_allclose(proba_0.compute(), proba_1.compute()) path = os.path.join(tmpdir, "cls.json") cls.save_model(path) @@ -1949,16 +1952,20 @@ class TestWithDask: cls.load_model(path) assert cls.n_classes_ == 10 predt_2 = cls.predict(X) + proba_2 = cls.predict_proba(X) np.testing.assert_allclose(predt_0.compute(), predt_2.compute()) + np.testing.assert_allclose(proba_0.compute(), proba_2.compute()) # Use single node to load cls = xgb.XGBClassifier() cls.load_model(path) assert cls.n_classes_ == 10 predt_3 = cls.predict(X_) + proba_3 = cls.predict_proba(X_) np.testing.assert_allclose(predt_0.compute(), predt_3) + np.testing.assert_allclose(proba_0.compute(), proba_3) def test_dask_unsupported_features(client: "Client") -> None: From 34a261669614e63c8095df1a01842e5920330b30 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Mon, 27 Nov 2023 20:09:25 +0800 Subject: [PATCH 020/109] [jvm-packages] Update dependencies. (#9809) - scalatest: 3.2.17 - maven-checkstyle-plugin: 3.3.1 - maven-surefire-plugin: 3.2.2 - maven-project-info-reports-plugin: 3.5.0 - maven-javadoc-plugin: 3.6.2 --- jvm-packages/pom.xml | 8 ++++---- jvm-packages/xgboost4j-gpu/pom.xml | 2 +- jvm-packages/xgboost4j/pom.xml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index 5469773c5..39d4bc444 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -46,7 +46,7 @@ 23.08.0 23.08.1 cuda11 - 3.2.16 + 3.2.17 2.11.0 @@ -381,7 +381,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.3.0 + 3.3.1 checkstyle.xml true @@ -434,7 +434,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.2.2 false false @@ -457,7 +457,7 @@ maven-project-info-reports-plugin - 3.4.5 + 3.5.0 net.alchim31.maven diff --git a/jvm-packages/xgboost4j-gpu/pom.xml b/jvm-packages/xgboost4j-gpu/pom.xml index c08988ac8..2fab78126 100644 --- a/jvm-packages/xgboost4j-gpu/pom.xml +++ b/jvm-packages/xgboost4j-gpu/pom.xml @@ -72,7 +72,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.2 protected true diff --git a/jvm-packages/xgboost4j/pom.xml b/jvm-packages/xgboost4j/pom.xml index 46ee9158f..e05bbcf48 100644 --- a/jvm-packages/xgboost4j/pom.xml +++ b/jvm-packages/xgboost4j/pom.xml @@ -60,7 +60,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.2 protected true From bfa1252fca4587151d5ac7cf79875cc130bba057 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 28 Nov 2023 22:42:41 +0100 Subject: [PATCH 021/109] [R][doc] Update docs about fitting from CSR (#9818) --- R-package/R/xgb.DMatrix.R | 2 +- R-package/man/xgb.DMatrix.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index b01e98637..8e19e87b0 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -5,7 +5,7 @@ #' \code{\link{xgb.DMatrix.save}}). #' #' @param data a \code{matrix} object (either numeric or integer), a \code{dgCMatrix} object, -#' a \code{dgRMatrix} object (only when making predictions from a fitted model), +#' a \code{dgRMatrix} object, #' a \code{dsparseVector} object (only when making predictions from a fitted model, will be #' interpreted as a row vector), or a character string representing a filename. #' @param info a named list of additional information to store in the \code{xgb.DMatrix} object. diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 59ef0b3be..38a65c638 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -15,7 +15,7 @@ xgb.DMatrix( } \arguments{ \item{data}{a \code{matrix} object (either numeric or integer), a \code{dgCMatrix} object, -a \code{dgRMatrix} object (only when making predictions from a fitted model), +a \code{dgRMatrix} object, a \code{dsparseVector} object (only when making predictions from a fitted model, will be interpreted as a row vector), or a character string representing a filename.} From 59684b2db648563211465a7a60c6899362ba9956 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 29 Nov 2023 13:13:40 +0800 Subject: [PATCH 022/109] [doc] Draft for language binding consistency. [skip ci] (#9755) --- doc/contrib/consistency.rst | 62 +++++++++++++++++++++++++++++++++++++ doc/contrib/index.rst | 1 + 2 files changed, 63 insertions(+) create mode 100644 doc/contrib/consistency.rst diff --git a/doc/contrib/consistency.rst b/doc/contrib/consistency.rst new file mode 100644 index 000000000..a268820eb --- /dev/null +++ b/doc/contrib/consistency.rst @@ -0,0 +1,62 @@ +################################# +Consistency for Language Bindings +################################# + +XGBoost has many different language bindings developed over the years, some are in the main repository while others live independently. Many features and interfaces are inconsistent with each others, this document aims to provide some guidelines and actionable items for language binding designers. + +******************* +Model Serialization +******************* + +XGBoost C API exposes a couple functions for serializing a model for persistence storage. These saved files are backward compatible, meaning one can load an older XGBoost model with a newer XGBoost version. If there's change in the model format, we have deprecation notice inside the C++ implementation and public issue for tracking the status. See :doc:`/tutorials/saving_model` for details. + +As a result, these are considered to be stable and should work across language bindings. For instance, a model trained in R should be fully functioning in C or Python. Please don't pad anything to the output file or buffer. + +If there are extra fields that must be saved: + +- First review whether the attribute can be retrieved from known properties of the model. For instance, there's a :py:attr:`~xgboost.XGBClassifier.classes_` attribute in the scikit-learn interface :py:class:`~xgboost.XGBClassifier`, which can be obtained through `numpy.arange(n_classes)` and doesn't need to be saved into the model. Preserving version compatibility is not a trivial task and we are still spending a significant amount of time to maintain it. Please don't make complication if it's not necessary. + +- Then please consider whether it's universal. For instance, we have added `feature_types` to the model serialization for categorical features (which is a new feature after 1.6), the attribute is useful or will be useful in the future regardless of the language binding. + +- If the field is small, we can save it as model attribute (which is a key-value structure). These attributes are ignored by all other language bindings and mostly an ad-hoc storage. + +- Lastly, we should use the UBJSON as the default output format when given a chance (not to be burdened by the old binary format). + +********************* +Training Continuation +********************* + +There are cases where we want to train a model based on the previous model, for boosting trees, it's either adding new trees or modifying the existing trees. This can be normal model update, error recovery, or other special cases we don't know of yet. When it happens, the training iteration should start from 0, not from the last boosted rounds of the model. 0 is a special iteration number, we perform some extra checks like whether the label is valid during that iteration. These checks can be expensive but necessary for eliminating silent errors. Keeping the iteration starts from zero allows us to perform these checks only once for each input data. + +********* +Inference +********* + +The inference function is quite inconsistent among language bindings at the time of writing due to historical reasons, but this makes more important for us to have consistency in mind in the future development. + +- Firstly, it's the output shape. There's a relatively new parameter called ``strict_shape`` in XGBoost and is rarely used. We want to make it as the default behavior but couldn't due to compatibility concerns. See :doc:`/prediction` for details. In short, if specified, XGBoost C++ implementation can output prediction with the correct shape, instead of letting the language binding to handle it. +- Policy around early stopping is at the moment inconsistent between various interfaces. Some considers the ``best_iteration`` attribute while others don't. We should formalize that all interfaces in the future should use the ``best_iteration`` during inference unless user has explicitly specified the ``iteration_range`` parameter. + +**************** +Parameter naming +**************** + +There are many parameter naming conventions out there, Some XGBoost interfaces try to align with the larger communities. For example, the R package might support parameters naming like ``max.depth=3``, while the Spark package might support ``MaxDepth=3``. These are fine, it's better for the users to keep their pipeline consistent. However, while supporting naming variants, the normal, XGBoost way of naming should also be supported, meaning ``max_depth=3`` should be a valid parameter no-matter what language one is using. If someone were to write duplicated parameter ``max.depth=3, max_depth=3``, a clear error should be preferred instead of prioritizing one over the other. + +****************** +Default Parameters +****************** + +Like many other machine learning libraries, all parameters from XGBoost can either be inferred from the data or have default values. Bindings should not make copies of these default values and let the XGBoost core decide. When the parameter key is not passed into the C++ core, XGBoost will pick the default accordingly. These defaults are not necessarily optimal, but they are there for consistency. If there's a new choice of default parameter, we can change it inside the core and it will be automatically propagated to all bindings. Given the same set of parameters and data, various bindings should strive to produce the same model. One exception is the `num_boost_rounds`, which exists only in high-level bindings and has various alias like ``n_estimators``. Its default value is close to arbitrary at the moment, we haven't been able to get a good default yet. + +******* +Logging +******* + +XGBoost has a default logger builtin that can be a wrapper over binding-specific logging facility. For instance, the Python binding registers a callback to use Python :py:mod:`warnings` and :py:func:`print` function to output logging. We want to keep logging native to the larger communities instead of using the ``std::cerr`` from C++. + +*********************************** +Minimum Amount of Data Manipulation +*********************************** + +XGBoost is mostly a machine learning library providing boosting algorithm implementation. Some other implementations might perform some sort of data manipulation implicitly like deciding the coding of the data, and transforming the data according to some heuristic before training. We prefer to keep these operations based on necessities instead of convenience to keep the scope of the project well-defined. Whenever possible, we should leave these features to 3-party libraries and consider how a user can compose their pipeline. For instance, XGBoost itself should not perform ordinal encoding for categorical data, users will pick an encoder that fits their use cases (like out-of-core implementation, distributed implementation, known mapping, etc). If some transformations are decided to be part of the algorithm, we can have it inside the core instead of the language binding. Examples would be target-encoding or sketching the response variables. If we were to support them, we could have it inside the core implementation as part of the ML algorithm. This aligns with the same principles of default parameters, various bindings should provide similar (if not the same) results given the same set of parameters and data. \ No newline at end of file diff --git a/doc/contrib/index.rst b/doc/contrib/index.rst index 6a36cb108..feac865fb 100644 --- a/doc/contrib/index.rst +++ b/doc/contrib/index.rst @@ -23,6 +23,7 @@ Here are guidelines for contributing to various aspect of the XGBoost project: Community Guideline donate coding_guide + consistency python_packaging unit_tests Docs and Examples From c0ef2f8dce0528bb4ee4b9e4235ce6d2c4b973f4 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 29 Nov 2023 06:14:17 +0100 Subject: [PATCH 023/109] [R] Fix potential memory leaks in case of R allocation failures (#9817) --- R-package/src/xgboost_R.cc | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 2938d4b6e..982350e95 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -82,11 +82,11 @@ XGB_DLL SEXP XGBGetGlobalConfig_R() { } XGB_DLL SEXP XGDMatrixCreateFromFile_R(SEXP fname, SEXP silent) { - SEXP ret; + SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); DMatrixHandle handle; CHECK_CALL(XGDMatrixCreateFromFile(CHAR(asChar(fname)), asInteger(silent), &handle)); - ret = PROTECT(R_MakeExternalPtr(handle, R_NilValue, R_NilValue)); + R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); UNPROTECT(1); @@ -158,7 +158,7 @@ void CreateFromSparse(SEXP indptr, SEXP indices, SEXP data, std::string *indptr_ XGB_DLL SEXP XGDMatrixCreateFromCSC_R(SEXP indptr, SEXP indices, SEXP data, SEXP num_row, SEXP missing, SEXP n_threads) { - SEXP ret; + SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); std::int32_t threads = asInteger(n_threads); @@ -180,8 +180,7 @@ XGB_DLL SEXP XGDMatrixCreateFromCSC_R(SEXP indptr, SEXP indices, SEXP data, SEXP CHECK_CALL(XGDMatrixCreateFromCSC(sindptr.c_str(), sindices.c_str(), sdata.c_str(), nrow, config.c_str(), &handle)); - ret = PROTECT(R_MakeExternalPtr(handle, R_NilValue, R_NilValue)); - + R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); UNPROTECT(1); @@ -190,7 +189,7 @@ XGB_DLL SEXP XGDMatrixCreateFromCSC_R(SEXP indptr, SEXP indices, SEXP data, SEXP XGB_DLL SEXP XGDMatrixCreateFromCSR_R(SEXP indptr, SEXP indices, SEXP data, SEXP num_col, SEXP missing, SEXP n_threads) { - SEXP ret; + SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); std::int32_t threads = asInteger(n_threads); @@ -211,8 +210,7 @@ XGB_DLL SEXP XGDMatrixCreateFromCSR_R(SEXP indptr, SEXP indices, SEXP data, SEXP Json::Dump(jconfig, &config); CHECK_CALL(XGDMatrixCreateFromCSR(sindptr.c_str(), sindices.c_str(), sdata.c_str(), ncol, config.c_str(), &handle)); - ret = PROTECT(R_MakeExternalPtr(handle, R_NilValue, R_NilValue)); - + R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); UNPROTECT(1); @@ -220,7 +218,7 @@ XGB_DLL SEXP XGDMatrixCreateFromCSR_R(SEXP indptr, SEXP indices, SEXP data, SEXP } XGB_DLL SEXP XGDMatrixSliceDMatrix_R(SEXP handle, SEXP idxset) { - SEXP ret; + SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); int len = length(idxset); std::vector idxvec(len); @@ -232,7 +230,7 @@ XGB_DLL SEXP XGDMatrixSliceDMatrix_R(SEXP handle, SEXP idxset) { BeginPtr(idxvec), len, &res, 0)); - ret = PROTECT(R_MakeExternalPtr(res, R_NilValue, R_NilValue)); + R_SetExternalPtrAddr(ret, res); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); UNPROTECT(1); @@ -351,7 +349,7 @@ void _BoosterFinalizer(SEXP ext) { } XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { - SEXP ret; + SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); int len = length(dmats); std::vector dvec; @@ -360,7 +358,7 @@ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { } BoosterHandle handle; CHECK_CALL(XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle)); - ret = PROTECT(R_MakeExternalPtr(handle, R_NilValue, R_NilValue)); + R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _BoosterFinalizer, TRUE); R_API_END(); UNPROTECT(1); From e2e089ce12793744295bbc598592f2d5b8cd3014 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 29 Nov 2023 15:51:07 +0800 Subject: [PATCH 024/109] [jvm-packages] Bump rapids version. (#9820) --- jvm-packages/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index 39d4bc444..a06653e73 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -43,8 +43,8 @@ 5 OFF OFF - 23.08.0 - 23.08.1 + 23.10.0 + 23.10.0 cuda11 3.2.17 2.11.0 From da3d55db5b8a1e79908f27dd083e4b207854e40d Mon Sep 17 00:00:00 2001 From: "Yuan (Terry) Tang" Date: Wed, 29 Nov 2023 14:27:05 -0500 Subject: [PATCH 025/109] Update affiliation (#9822) --- CONTRIBUTORS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 29d21e6a8..f96a7dc0d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,8 +10,8 @@ The Project Management Committee(PMC) consists group of active committers that m - Tianqi is a Ph.D. student working on large-scale machine learning. He is the creator of the project. * [Michael Benesty](https://github.com/pommedeterresautee) - Michael is a lawyer and data scientist in France. He is the creator of XGBoost interactive analysis module in R. -* [Yuan Tang](https://github.com/terrytangyuan), Akuity - - Yuan is a founding engineer at Akuity. He contributed mostly in R and Python packages. +* [Yuan Tang](https://github.com/terrytangyuan), Red Hat + - Yuan is a principal software engineer at Red Hat. He contributed mostly in R and Python packages. * [Nan Zhu](https://github.com/CodingCat), Uber - Nan is a software engineer in Uber. He contributed mostly in JVM packages. * [Jiaming Yuan](https://github.com/trivialfis) From 37da66f865f4f771a0198b478799db39ed55cd88 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 29 Nov 2023 21:35:05 +0100 Subject: [PATCH 026/109] [R] Use array interface for dense DMatrix creation (#9816) --------- Co-authored-by: Jiaming Yuan --- R-package/src/xgboost_R.cc | 105 +++++++++++++++--------- R-package/tests/testthat/test_dmatrix.R | 32 ++++++++ 2 files changed, 100 insertions(+), 37 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 982350e95..8742a2271 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,67 @@ #include "./xgboost_R.h" // Must follow other includes. +namespace { +[[nodiscard]] std::string MakeArrayInterfaceFromRMat(SEXP R_mat) { + SEXP mat_dims = Rf_getAttrib(R_mat, R_DimSymbol); + const int *ptr_mat_dims = INTEGER(mat_dims); + + // Lambda for type dispatch. + auto make_matrix = [=](auto const *ptr) { + using namespace xgboost; // NOLINT + using T = std::remove_pointer_t; + + auto m = linalg::MatrixView{ + common::Span{ptr, + static_cast(ptr_mat_dims[0]) * static_cast(ptr_mat_dims[1])}, + {ptr_mat_dims[0], ptr_mat_dims[1]}, // Shape + DeviceOrd::CPU(), + linalg::Order::kF // R uses column-major + }; + CHECK(m.FContiguous()); + return linalg::ArrayInterfaceStr(m); + }; + + const SEXPTYPE arr_type = TYPEOF(R_mat); + switch (arr_type) { + case REALSXP: + return make_matrix(REAL(R_mat)); + case INTSXP: + return make_matrix(INTEGER(R_mat)); + case LGLSXP: + return make_matrix(LOGICAL(R_mat)); + default: + LOG(FATAL) << "Array or matrix has unsupported type."; + } + + LOG(FATAL) << "Not reachable"; + return ""; +} + +[[nodiscard]] std::string MakeJsonConfigForArray(SEXP missing, SEXP n_threads, SEXPTYPE arr_type) { + using namespace ::xgboost; // NOLINT + Json jconfig{Object{}}; + + const SEXPTYPE missing_type = TYPEOF(missing); + if (Rf_isNull(missing) || (missing_type == REALSXP && ISNAN(Rf_asReal(missing))) || + (missing_type == LGLSXP && Rf_asLogical(missing) == R_NaInt) || + (missing_type == INTSXP && Rf_asInteger(missing) == R_NaInt)) { + // missing is not specified + if (arr_type == REALSXP) { + jconfig["missing"] = std::numeric_limits::quiet_NaN(); + } else { + jconfig["missing"] = R_NaInt; + } + } else { + // missing specified + jconfig["missing"] = Rf_asReal(missing); + } + + jconfig["nthread"] = Rf_asInteger(n_threads); + return Json::Dump(jconfig); +} +} // namespace + /*! * \brief macro to annotate begin of api */ @@ -94,47 +156,16 @@ XGB_DLL SEXP XGDMatrixCreateFromFile_R(SEXP fname, SEXP silent) { } XGB_DLL SEXP XGDMatrixCreateFromMat_R(SEXP mat, SEXP missing, SEXP n_threads) { - SEXP ret; + SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); - SEXP dim = getAttrib(mat, R_DimSymbol); - size_t nrow = static_cast(INTEGER(dim)[0]); - size_t ncol = static_cast(INTEGER(dim)[1]); - const bool is_int = TYPEOF(mat) == INTSXP; - double *din; - int *iin; - if (is_int) { - iin = INTEGER(mat); - } else { - din = REAL(mat); - } - std::vector data(nrow * ncol); - xgboost::Context ctx; - ctx.nthread = asInteger(n_threads); - std::int32_t threads = ctx.Threads(); - if (is_int) { - xgboost::common::ParallelFor(nrow, threads, [&](xgboost::omp_ulong i) { - for (size_t j = 0; j < ncol; ++j) { - auto v = iin[i + nrow * j]; - if (v == NA_INTEGER) { - data[i * ncol + j] = std::numeric_limits::quiet_NaN(); - } else { - data[i * ncol + j] = static_cast(v); - } - } - }); - } else { - xgboost::common::ParallelFor(nrow, threads, [&](xgboost::omp_ulong i) { - for (size_t j = 0; j < ncol; ++j) { - data[i * ncol + j] = din[i + nrow * j]; - } - }); - } + auto array_str = MakeArrayInterfaceFromRMat(mat); + auto config_str = MakeJsonConfigForArray(missing, n_threads, TYPEOF(mat)); DMatrixHandle handle; - CHECK_CALL(XGDMatrixCreateFromMat_omp(BeginPtr(data), nrow, ncol, - asReal(missing), &handle, threads)); - ret = PROTECT(R_MakeExternalPtr(handle, R_NilValue, R_NilValue)); + CHECK_CALL(XGDMatrixCreateFromDense(array_str.c_str(), config_str.c_str(), &handle)); + + R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); UNPROTECT(1); diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 461b7d158..a0cf90088 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -265,3 +265,35 @@ test_that("xgb.DMatrix: print", { }) expect_equal(txt, "xgb.DMatrix dim: 6513 x 126 info: NA colnames: no") }) + +test_that("xgb.DMatrix: Inf as missing", { + x_inf <- matrix(as.numeric(1:10), nrow = 5) + x_inf[2, 1] <- Inf + + x_nan <- x_inf + x_nan[2, 1] <- NA_real_ + + m_inf <- xgb.DMatrix(x_inf, nthread = n_threads, missing = Inf) + xgb.DMatrix.save(m_inf, "inf.dmatrix") + + m_nan <- xgb.DMatrix(x_nan, nthread = n_threads, missing = NA_real_) + xgb.DMatrix.save(m_nan, "nan.dmatrix") + + infconn <- file("inf.dmatrix", "rb") + nanconn <- file("nan.dmatrix", "rb") + + expect_equal(file.size("inf.dmatrix"), file.size("nan.dmatrix")) + + bytes <- file.size("inf.dmatrix") + infdmatrix <- readBin(infconn, "raw", n = bytes) + nandmatrix <- readBin(nanconn, "raw", n = bytes) + + expect_equal(length(infdmatrix), length(nandmatrix)) + expect_equal(infdmatrix, nandmatrix) + + close(infconn) + close(nanconn) + + file.remove("inf.dmatrix") + file.remove("nan.dmatrix") +}) From 95af5c074be363bddc439fa3d1c1460a29cc818e Mon Sep 17 00:00:00 2001 From: david-cortes Date: Thu, 30 Nov 2023 17:06:59 +0100 Subject: [PATCH 027/109] more usage of array interface, fix potential memory leaks of std::string (#9824) --- R-package/src/xgboost_R.cc | 61 +++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 8742a2271..a82913819 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -59,6 +59,32 @@ namespace { return ""; } +[[nodiscard]] std::string MakeArrayInterfaceFromRVector(SEXP R_vec) { + const size_t vec_len = Rf_xlength(R_vec); + + // Lambda for type dispatch. + auto make_vec = [=](auto const *ptr) { + using namespace xgboost; // NOLINT + auto v = linalg::MakeVec(ptr, vec_len); + return linalg::ArrayInterfaceStr(v); + }; + + const SEXPTYPE arr_type = TYPEOF(R_vec); + switch (arr_type) { + case REALSXP: + return make_vec(REAL(R_vec)); + case INTSXP: + return make_vec(INTEGER(R_vec)); + case LGLSXP: + return make_vec(LOGICAL(R_vec)); + default: + LOG(FATAL) << "Array or matrix has unsupported type."; + } + + LOG(FATAL) << "Not reachable"; + return ""; +} + [[nodiscard]] std::string MakeJsonConfigForArray(SEXP missing, SEXP n_threads, SEXPTYPE arr_type) { using namespace ::xgboost; // NOLINT Json jconfig{Object{}}; @@ -159,12 +185,15 @@ XGB_DLL SEXP XGDMatrixCreateFromMat_R(SEXP mat, SEXP missing, SEXP n_threads) { SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); - auto array_str = MakeArrayInterfaceFromRMat(mat); - auto config_str = MakeJsonConfigForArray(missing, n_threads, TYPEOF(mat)); - DMatrixHandle handle; - CHECK_CALL(XGDMatrixCreateFromDense(array_str.c_str(), config_str.c_str(), &handle)); + int res_code; + { + auto array_str = MakeArrayInterfaceFromRMat(mat); + auto config_str = MakeJsonConfigForArray(missing, n_threads, TYPEOF(mat)); + res_code = XGDMatrixCreateFromDense(array_str.c_str(), config_str.c_str(), &handle); + } + CHECK_CALL(res_code); R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); @@ -279,23 +308,15 @@ XGB_DLL SEXP XGDMatrixSaveBinary_R(SEXP handle, SEXP fname, SEXP silent) { XGB_DLL SEXP XGDMatrixSetInfo_R(SEXP handle, SEXP field, SEXP array) { R_API_BEGIN(); - int len = length(array); - const char *name = CHAR(asChar(field)); - auto ctx = DMatrixCtx(R_ExternalPtrAddr(handle)); - if (!strcmp("group", name)) { - std::vector vec(len); - xgboost::common::ParallelFor(len, ctx->Threads(), [&](xgboost::omp_ulong i) { - vec[i] = static_cast(INTEGER(array)[i]); - }); - CHECK_CALL( - XGDMatrixSetUIntInfo(R_ExternalPtrAddr(handle), CHAR(asChar(field)), BeginPtr(vec), len)); - } else { - std::vector vec(len); - xgboost::common::ParallelFor(len, ctx->Threads(), - [&](xgboost::omp_ulong i) { vec[i] = REAL(array)[i]; }); - CHECK_CALL( - XGDMatrixSetFloatInfo(R_ExternalPtrAddr(handle), CHAR(asChar(field)), BeginPtr(vec), len)); + SEXP field_ = PROTECT(Rf_asChar(field)); + int res_code; + { + const std::string array_str = MakeArrayInterfaceFromRVector(array); + res_code = XGDMatrixSetInfoFromInterface( + R_ExternalPtrAddr(handle), CHAR(field_), array_str.c_str()); } + CHECK_CALL(res_code); + UNPROTECT(1); R_API_END(); return R_NilValue; } From 2d8c67d6dcebad31fa6e81e0fa9e42704ddd1d6f Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sat, 2 Dec 2023 07:34:56 +0800 Subject: [PATCH 028/109] [jvm-packages] Bump dependencies. (#9827) - #9811 - #9814 - #9826 - #9830 - #9833 - #9832 - #9831 - #9834 --- jvm-packages/pom.xml | 4 ++-- jvm-packages/xgboost4j-gpu/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index a06653e73..de778a995 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -33,7 +33,7 @@ UTF-8 1.8 1.8 - 1.17.1 + 1.18.0 4.13.2 3.4.1 3.4.1 @@ -481,7 +481,7 @@ commons-logging commons-logging - 1.2 + 1.3.0 org.scalatest diff --git a/jvm-packages/xgboost4j-gpu/pom.xml b/jvm-packages/xgboost4j-gpu/pom.xml index 2fab78126..13f9797cd 100644 --- a/jvm-packages/xgboost4j-gpu/pom.xml +++ b/jvm-packages/xgboost4j-gpu/pom.xml @@ -63,7 +63,7 @@ org.apache.commons commons-lang3 - 3.13.0 + 3.14.0 From e78b46046edbd22aca59302a0e593099499e2109 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sat, 2 Dec 2023 11:03:17 +0800 Subject: [PATCH 029/109] [CI] Update R version on Linux. (#9835) --- tests/buildkite/build-gpu-rpkg.sh | 4 +++- tests/buildkite/conftest.sh | 1 + tests/ci_build/Dockerfile.gpu_build_r_centos7 | 21 +++++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/buildkite/build-gpu-rpkg.sh b/tests/buildkite/build-gpu-rpkg.sh index 5e0de9f8c..585dc79ae 100755 --- a/tests/buildkite/build-gpu-rpkg.sh +++ b/tests/buildkite/build-gpu-rpkg.sh @@ -7,7 +7,9 @@ source tests/buildkite/conftest.sh echo "--- Build XGBoost R package with CUDA" tests/ci_build/ci_build.sh gpu_build_r_centos7 docker \ - --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} tests/ci_build/build_r_pkg_with_cuda.sh \ + --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ + --build-arg R_VERSION_ARG=${R_VERSION} \ + tests/ci_build/build_r_pkg_with_cuda.sh \ ${BUILDKITE_COMMIT} if [[ ($is_pull_request == 0) && ($is_release_branch == 1) ]] diff --git a/tests/buildkite/conftest.sh b/tests/buildkite/conftest.sh index 3d820d727..881f98672 100755 --- a/tests/buildkite/conftest.sh +++ b/tests/buildkite/conftest.sh @@ -27,6 +27,7 @@ NCCL_VERSION=2.16.5-1 RAPIDS_VERSION=23.10 SPARK_VERSION=3.4.0 JDK_VERSION=8 +R_VERSION=4.3.2 if [[ -z ${BUILDKITE:-} ]] then diff --git a/tests/ci_build/Dockerfile.gpu_build_r_centos7 b/tests/ci_build/Dockerfile.gpu_build_r_centos7 index b73cf5adb..7c95f09b5 100644 --- a/tests/ci_build/Dockerfile.gpu_build_r_centos7 +++ b/tests/ci_build/Dockerfile.gpu_build_r_centos7 @@ -1,6 +1,7 @@ ARG CUDA_VERSION_ARG FROM nvcr.io/nvidia/cuda:$CUDA_VERSION_ARG-devel-centos7 ARG CUDA_VERSION_ARG +ARG R_VERSION_ARG # Install all basic requirements RUN \ @@ -11,26 +12,28 @@ RUN \ yum -y update && \ yum install -y tar unzip wget xz git which ninja-build readline-devel libX11-devel libXt-devel \ xorg-x11-server-devel openssl-devel zlib-devel bzip2-devel xz-devel \ - pcre-devel libcurl-devel texlive-* \ + pcre2-devel libcurl-devel texlive-* \ devtoolset-9-gcc devtoolset-9-binutils devtoolset-9-gcc-c++ \ devtoolset-9-gcc-gfortran devtoolset-9-libquadmath-devel \ devtoolset-9-runtime devtoolset-9-libstdc++-devel -ENV PATH=/opt/mambaforge/bin:/usr/local/ninja:/opt/software/packages/bin:/opt/R/3.3.0/bin:$PATH -ENV LD_LIBRARY_PATH=/opt/software/packages/lib:/opt/R/3.3.0/lib64:$LD_LIBRARY_PATH +ENV PATH=/opt/mambaforge/bin:/usr/local/ninja:/opt/software/packages/bin:/opt/R/$R_VERSION_ARG/bin:$PATH +ENV LD_LIBRARY_PATH=/opt/software/packages/lib:/opt/R/$R_VERSION_ARG/lib64:$LD_LIBRARY_PATH ENV CC=/opt/rh/devtoolset-9/root/usr/bin/gcc ENV CXX=/opt/rh/devtoolset-9/root/usr/bin/c++ ENV CPP=/opt/rh/devtoolset-9/root/usr/bin/cpp ENV F77=/opt/rh/devtoolset-9/root/usr/bin/gfortran +ENV FC=/opt/rh/devtoolset-9/root/usr/bin/gfortran -# R 3.3.0 RUN \ - wget -nv -nc https://cran.r-project.org/src/base/R-3/R-3.3.0.tar.gz && \ - tar xf R-3.3.0.tar.gz && \ - cd R-3.3.0 && \ - ./configure --prefix=/opt/R/3.3.0 --enable-R-shlib && \ + wget -nv -nc https://cran.r-project.org/src/base/R-4/R-$R_VERSION_ARG.tar.gz && \ + tar xf R-$R_VERSION_ARG.tar.gz && \ + cd R-$R_VERSION_ARG && \ + ./configure --prefix=/opt/R/$R_VERSION_ARG --enable-R-shlib --with-pcrel && \ make -j$(nproc) && \ - make install && \ + make install + +run \ # Python wget -nv -O conda.sh https://github.com/conda-forge/miniforge/releases/download/22.11.1-2/Mambaforge-22.11.1-2-Linux-x86_64.sh && \ bash conda.sh -b -p /opt/mambaforge && \ From 7196c9d95e706566424ae0b7ea0046fe9c5a3849 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 2 Dec 2023 06:43:50 +0100 Subject: [PATCH 030/109] [R] Fix memory safety issues (#9823) --- R-package/src/xgboost_R.cc | 355 +++++++++++++++++++++++-------------- 1 file changed, 226 insertions(+), 129 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index a82913819..b267d7da6 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -23,6 +23,31 @@ #include "./xgboost_R.h" // Must follow other includes. namespace { + +struct ErrorWithUnwind : public std::exception {}; + +void ThrowExceptionFromRError(void *unused, Rboolean jump) { + if (jump) { + throw ErrorWithUnwind(); + } +} + +struct PtrToConstChar { + const char *ptr; +}; + +SEXP WrappedMkChar(void *void_ptr) { + return Rf_mkChar(static_cast(void_ptr)->ptr); +} + +SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { + PtrToConstChar ptr_struct{c_str}; + return R_UnwindProtect( + WrappedMkChar, static_cast(&ptr_struct), + ThrowExceptionFromRError, nullptr, + continuation_token); +} + [[nodiscard]] std::string MakeArrayInterfaceFromRMat(SEXP R_mat) { SEXP mat_dims = Rf_getAttrib(R_mat, R_DimSymbol); const int *ptr_mat_dims = INTEGER(mat_dims); @@ -208,8 +233,8 @@ void CreateFromSparse(SEXP indptr, SEXP indices, SEXP data, std::string *indptr_ const int *p_indices = INTEGER(indices); const double *p_data = REAL(data); - auto nindptr = static_cast(length(indptr)); - auto ndata = static_cast(length(data)); + auto nindptr = static_cast(Rf_xlength(indptr)); + auto ndata = static_cast(Rf_xlength(data)); CHECK_EQ(ndata, p_indptr[nindptr - 1]); xgboost::detail::MakeSparseFromPtr(p_indptr, p_indices, p_data, nindptr, indptr_str, indices_str, data_str); @@ -221,24 +246,27 @@ XGB_DLL SEXP XGDMatrixCreateFromCSC_R(SEXP indptr, SEXP indices, SEXP data, SEXP SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); std::int32_t threads = asInteger(n_threads); - - using xgboost::Integer; - using xgboost::Json; - using xgboost::Object; - - std::string sindptr, sindices, sdata; - CreateFromSparse(indptr, indices, data, &sindptr, &sindices, &sdata); - auto nrow = static_cast(INTEGER(num_row)[0]); - DMatrixHandle handle; - Json jconfig{Object{}}; - // Construct configuration - jconfig["nthread"] = Integer{threads}; - jconfig["missing"] = xgboost::Number{asReal(missing)}; - std::string config; - Json::Dump(jconfig, &config); - CHECK_CALL(XGDMatrixCreateFromCSC(sindptr.c_str(), sindices.c_str(), sdata.c_str(), nrow, - config.c_str(), &handle)); + + int res_code; + { + using xgboost::Integer; + using xgboost::Json; + using xgboost::Object; + std::string sindptr, sindices, sdata; + CreateFromSparse(indptr, indices, data, &sindptr, &sindices, &sdata); + auto nrow = static_cast(INTEGER(num_row)[0]); + + Json jconfig{Object{}}; + // Construct configuration + jconfig["nthread"] = Integer{threads}; + jconfig["missing"] = xgboost::Number{asReal(missing)}; + std::string config; + Json::Dump(jconfig, &config); + res_code = XGDMatrixCreateFromCSC(sindptr.c_str(), sindices.c_str(), sdata.c_str(), nrow, + config.c_str(), &handle); + } + CHECK_CALL(res_code); R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); @@ -252,24 +280,27 @@ XGB_DLL SEXP XGDMatrixCreateFromCSR_R(SEXP indptr, SEXP indices, SEXP data, SEXP SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); std::int32_t threads = asInteger(n_threads); - - using xgboost::Integer; - using xgboost::Json; - using xgboost::Object; - - std::string sindptr, sindices, sdata; - CreateFromSparse(indptr, indices, data, &sindptr, &sindices, &sdata); - auto ncol = static_cast(INTEGER(num_col)[0]); - DMatrixHandle handle; - Json jconfig{Object{}}; - // Construct configuration - jconfig["nthread"] = Integer{threads}; - jconfig["missing"] = xgboost::Number{asReal(missing)}; - std::string config; - Json::Dump(jconfig, &config); - CHECK_CALL(XGDMatrixCreateFromCSR(sindptr.c_str(), sindices.c_str(), sdata.c_str(), ncol, - config.c_str(), &handle)); + + int res_code; + { + using xgboost::Integer; + using xgboost::Json; + using xgboost::Object; + + std::string sindptr, sindices, sdata; + CreateFromSparse(indptr, indices, data, &sindptr, &sindices, &sdata); + auto ncol = static_cast(INTEGER(num_col)[0]); + + Json jconfig{Object{}}; + // Construct configuration + jconfig["nthread"] = Integer{threads}; + jconfig["missing"] = xgboost::Number{asReal(missing)}; + std::string config; + Json::Dump(jconfig, &config); + res_code = XGDMatrixCreateFromCSR(sindptr.c_str(), sindices.c_str(), sdata.c_str(), ncol, + config.c_str(), &handle); + } R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); @@ -280,16 +311,22 @@ XGB_DLL SEXP XGDMatrixCreateFromCSR_R(SEXP indptr, SEXP indices, SEXP data, SEXP XGB_DLL SEXP XGDMatrixSliceDMatrix_R(SEXP handle, SEXP idxset) { SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); - int len = length(idxset); - std::vector idxvec(len); - for (int i = 0; i < len; ++i) { - idxvec[i] = INTEGER(idxset)[i] - 1; - } + R_xlen_t len = Rf_xlength(idxset); + const int *idxset_ = INTEGER(idxset); DMatrixHandle res; - CHECK_CALL(XGDMatrixSliceDMatrixEx(R_ExternalPtrAddr(handle), - BeginPtr(idxvec), len, - &res, - 0)); + + int res_code; + { + std::vector idxvec(len); + for (R_xlen_t i = 0; i < len; ++i) { + idxvec[i] = idxset_[i] - 1; + } + res_code = XGDMatrixSliceDMatrixEx(R_ExternalPtrAddr(handle), + BeginPtr(idxvec), len, + &res, + 0); + } + CHECK_CALL(res_code); R_SetExternalPtrAddr(ret, res); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); @@ -325,18 +362,29 @@ XGB_DLL SEXP XGDMatrixSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP array) { R_API_BEGIN(); size_t len{0}; if (!isNull(array)) { - len = length(array); + len = Rf_xlength(array); } - const char *name = CHAR(asChar(field)); - std::vector str_info; + SEXP str_info_holder = PROTECT(Rf_allocVector(VECSXP, len)); for (size_t i = 0; i < len; ++i) { - str_info.emplace_back(CHAR(asChar(VECTOR_ELT(array, i)))); + SET_VECTOR_ELT(str_info_holder, i, Rf_asChar(VECTOR_ELT(array, i))); } - std::vector vec(len); - std::transform(str_info.cbegin(), str_info.cend(), vec.begin(), - [](std::string const &str) { return str.c_str(); }); - CHECK_CALL(XGDMatrixSetStrFeatureInfo(R_ExternalPtrAddr(handle), name, vec.data(), len)); + + SEXP field_ = PROTECT(Rf_asChar(field)); + const char *name = CHAR(field_); + int res_code; + { + std::vector str_info; + for (size_t i = 0; i < len; ++i) { + str_info.emplace_back(CHAR(VECTOR_ELT(str_info_holder, i))); + } + std::vector vec(len); + std::transform(str_info.cbegin(), str_info.cend(), vec.begin(), + [](std::string const &str) { return str.c_str(); }); + res_code = XGDMatrixSetStrFeatureInfo(R_ExternalPtrAddr(handle), name, vec.data(), len); + } + CHECK_CALL(res_code); + UNPROTECT(2); R_API_END(); return R_NilValue; } @@ -369,8 +417,9 @@ XGB_DLL SEXP XGDMatrixGetInfo_R(SEXP handle, SEXP field) { const float *res; CHECK_CALL(XGDMatrixGetFloatInfo(R_ExternalPtrAddr(handle), CHAR(asChar(field)), &olen, &res)); ret = PROTECT(allocVector(REALSXP, olen)); + double *ret_ = REAL(ret); for (size_t i = 0; i < olen; ++i) { - REAL(ret)[i] = res[i]; + ret_[i] = res[i]; } R_API_END(); UNPROTECT(1); @@ -403,13 +452,18 @@ void _BoosterFinalizer(SEXP ext) { XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); R_API_BEGIN(); - int len = length(dmats); - std::vector dvec; - for (int i = 0; i < len; ++i) { - dvec.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); - } + R_xlen_t len = Rf_xlength(dmats); BoosterHandle handle; - CHECK_CALL(XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle)); + + int res_code; + { + std::vector dvec; + for (R_xlen_t i = 0; i < len; ++i) { + dvec.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); + } + res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); + } + CHECK_CALL(res_code); R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _BoosterFinalizer, TRUE); R_API_END(); @@ -419,13 +473,18 @@ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { XGB_DLL SEXP XGBoosterCreateInEmptyObj_R(SEXP dmats, SEXP R_handle) { R_API_BEGIN(); - int len = length(dmats); - std::vector dvec; - for (int i = 0; i < len; ++i) { - dvec.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); - } + R_xlen_t len = Rf_xlength(dmats); BoosterHandle handle; - CHECK_CALL(XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle)); + + int res_code; + { + std::vector dvec; + for (R_xlen_t i = 0; i < len; ++i) { + dvec.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); + } + res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); + } + CHECK_CALL(res_code); R_SetExternalPtrAddr(R_handle, handle); R_RegisterCFinalizerEx(R_handle, _BoosterFinalizer, TRUE); R_API_END(); @@ -434,9 +493,12 @@ XGB_DLL SEXP XGBoosterCreateInEmptyObj_R(SEXP dmats, SEXP R_handle) { XGB_DLL SEXP XGBoosterSetParam_R(SEXP handle, SEXP name, SEXP val) { R_API_BEGIN(); + SEXP name_ = PROTECT(Rf_asChar(name)); + SEXP val_ = PROTECT(Rf_asChar(val)); CHECK_CALL(XGBoosterSetParam(R_ExternalPtrAddr(handle), - CHAR(asChar(name)), - CHAR(asChar(val)))); + CHAR(name_), + CHAR(val_))); + UNPROTECT(2); R_API_END(); return R_NilValue; } @@ -452,7 +514,7 @@ XGB_DLL SEXP XGBoosterUpdateOneIter_R(SEXP handle, SEXP iter, SEXP dtrain) { XGB_DLL SEXP XGBoosterTrainOneIter_R(SEXP handle, SEXP dtrain, SEXP iter, SEXP grad, SEXP hess) { R_API_BEGIN(); - CHECK_EQ(length(grad), length(hess)) << "gradient and hess must have same length."; + CHECK_EQ(Rf_xlength(grad), Rf_xlength(hess)) << "gradient and hess must have same length."; SEXP gdim = getAttrib(grad, R_DimSymbol); auto n_samples = static_cast(INTEGER(gdim)[0]); auto n_targets = static_cast(INTEGER(gdim)[1]); @@ -463,11 +525,15 @@ XGB_DLL SEXP XGBoosterTrainOneIter_R(SEXP handle, SEXP dtrain, SEXP iter, SEXP g double const *d_grad = REAL(grad); double const *d_hess = REAL(hess); - auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); - auto [s_grad, s_hess] = xgboost::detail::MakeGradientInterface( - ctx, d_grad, d_hess, xgboost::linalg::kF, n_samples, n_targets); - CHECK_CALL(XGBoosterTrainOneIter(R_ExternalPtrAddr(handle), R_ExternalPtrAddr(dtrain), - asInteger(iter), s_grad.c_str(), s_hess.c_str())); + int res_code; + { + auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); + auto [s_grad, s_hess] = xgboost::detail::MakeGradientInterface( + ctx, d_grad, d_hess, xgboost::linalg::kF, n_samples, n_targets); + res_code = XGBoosterTrainOneIter(R_ExternalPtrAddr(handle), R_ExternalPtrAddr(dtrain), + asInteger(iter), s_grad.c_str(), s_hess.c_str()); + } + CHECK_CALL(res_code); R_API_END(); return R_NilValue; @@ -476,24 +542,34 @@ XGB_DLL SEXP XGBoosterTrainOneIter_R(SEXP handle, SEXP dtrain, SEXP iter, SEXP g XGB_DLL SEXP XGBoosterEvalOneIter_R(SEXP handle, SEXP iter, SEXP dmats, SEXP evnames) { const char *ret; R_API_BEGIN(); - CHECK_EQ(length(dmats), length(evnames)) + CHECK_EQ(Rf_xlength(dmats), Rf_xlength(evnames)) << "dmats and evnams must have same length"; - int len = length(dmats); - std::vector vec_dmats; - std::vector vec_names; - std::vector vec_sptr; - for (int i = 0; i < len; ++i) { - vec_dmats.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); - vec_names.emplace_back(CHAR(asChar(VECTOR_ELT(evnames, i)))); + R_xlen_t len = Rf_xlength(dmats); + SEXP evnames_lst = PROTECT(Rf_allocVector(VECSXP, len)); + for (R_xlen_t i = 0; i < len; i++) { + SET_VECTOR_ELT(evnames_lst, i, Rf_asChar(VECTOR_ELT(evnames, i))); } - for (int i = 0; i < len; ++i) { - vec_sptr.push_back(vec_names[i].c_str()); + + int res_code; + { + std::vector vec_dmats; + std::vector vec_names; + std::vector vec_sptr; + for (R_xlen_t i = 0; i < len; ++i) { + vec_dmats.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); + vec_names.emplace_back(CHAR(VECTOR_ELT(evnames_lst, i))); + } + for (R_xlen_t i = 0; i < len; ++i) { + vec_sptr.push_back(vec_names[i].c_str()); + } + res_code = XGBoosterEvalOneIter(R_ExternalPtrAddr(handle), + asInteger(iter), + BeginPtr(vec_dmats), + BeginPtr(vec_sptr), + len, &ret); } - CHECK_CALL(XGBoosterEvalOneIter(R_ExternalPtrAddr(handle), - asInteger(iter), - BeginPtr(vec_dmats), - BeginPtr(vec_sptr), - len, &ret)); + CHECK_CALL(res_code); + UNPROTECT(1); R_API_END(); return mkString(ret); } @@ -501,10 +577,11 @@ XGB_DLL SEXP XGBoosterEvalOneIter_R(SEXP handle, SEXP iter, SEXP dmats, SEXP evn XGB_DLL SEXP XGBoosterPredictFromDMatrix_R(SEXP handle, SEXP dmat, SEXP json_config) { SEXP r_out_shape; SEXP r_out_result; - SEXP r_out; + SEXP r_out = PROTECT(allocVector(VECSXP, 2)); + SEXP json_config_ = PROTECT(Rf_asChar(json_config)); R_API_BEGIN(); - char const *c_json_config = CHAR(asChar(json_config)); + char const *c_json_config = CHAR(json_config_); bst_ulong out_dim; bst_ulong const *out_shape; @@ -515,23 +592,23 @@ XGB_DLL SEXP XGBoosterPredictFromDMatrix_R(SEXP handle, SEXP dmat, SEXP json_con r_out_shape = PROTECT(allocVector(INTSXP, out_dim)); size_t len = 1; + int *r_out_shape_ = INTEGER(r_out_shape); for (size_t i = 0; i < out_dim; ++i) { - INTEGER(r_out_shape)[i] = out_shape[i]; + r_out_shape_[i] = out_shape[i]; len *= out_shape[i]; } r_out_result = PROTECT(allocVector(REALSXP, len)); auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); + double *r_out_result_ = REAL(r_out_result); xgboost::common::ParallelFor(len, ctx->Threads(), [&](xgboost::omp_ulong i) { - REAL(r_out_result)[i] = out_result[i]; + r_out_result_[i] = out_result[i]; }); - r_out = PROTECT(allocVector(VECSXP, 2)); - SET_VECTOR_ELT(r_out, 0, r_out_shape); SET_VECTOR_ELT(r_out, 1, r_out_result); R_API_END(); - UNPROTECT(3); + UNPROTECT(4); return r_out; } @@ -554,7 +631,7 @@ XGB_DLL SEXP XGBoosterLoadModelFromRaw_R(SEXP handle, SEXP raw) { R_API_BEGIN(); CHECK_CALL(XGBoosterLoadModelFromBuffer(R_ExternalPtrAddr(handle), RAW(raw), - length(raw))); + Rf_xlength(raw))); R_API_END(); return R_NilValue; } @@ -612,45 +689,54 @@ XGB_DLL SEXP XGBoosterUnserializeFromBuffer_R(SEXP handle, SEXP raw) { R_API_BEGIN(); CHECK_CALL(XGBoosterUnserializeFromBuffer(R_ExternalPtrAddr(handle), RAW(raw), - length(raw))); + Rf_xlength(raw))); R_API_END(); return R_NilValue; } XGB_DLL SEXP XGBoosterDumpModel_R(SEXP handle, SEXP fmap, SEXP with_stats, SEXP dump_format) { SEXP out; + SEXP continuation_token = PROTECT(R_MakeUnwindCont()); + SEXP dump_format_ = PROTECT(Rf_asChar(dump_format)); + SEXP fmap_ = PROTECT(Rf_asChar(fmap)); R_API_BEGIN(); bst_ulong olen; const char **res; - const char *fmt = CHAR(asChar(dump_format)); + const char *fmt = CHAR(dump_format_); CHECK_CALL(XGBoosterDumpModelEx(R_ExternalPtrAddr(handle), - CHAR(asChar(fmap)), + CHAR(fmap_), asInteger(with_stats), fmt, &olen, &res)); out = PROTECT(allocVector(STRSXP, olen)); - if (!strcmp("json", fmt)) { - std::stringstream stream; - stream << "[\n"; - for (size_t i = 0; i < olen; ++i) { - stream << res[i]; - if (i < olen - 1) { - stream << ",\n"; - } else { - stream << "\n"; + try { + if (!strcmp("json", fmt)) { + std::stringstream stream; + stream << "[\n"; + for (size_t i = 0; i < olen; ++i) { + stream << res[i]; + if (i < olen - 1) { + stream << ",\n"; + } else { + stream << "\n"; + } + } + stream << "]"; + const std::string temp_str = stream.str(); + SET_STRING_ELT(out, 0, SafeMkChar(temp_str.c_str(), continuation_token)); + } else { + for (size_t i = 0; i < olen; ++i) { + std::stringstream stream; + stream << "booster[" << i <<"]\n" << res[i]; + const std::string temp_str = stream.str(); + SET_STRING_ELT(out, i, SafeMkChar(temp_str.c_str(), continuation_token)); } } - stream << "]"; - SET_STRING_ELT(out, 0, mkChar(stream.str().c_str())); - } else { - for (size_t i = 0; i < olen; ++i) { - std::stringstream stream; - stream << "booster[" << i <<"]\n" << res[i]; - SET_STRING_ELT(out, i, mkChar(stream.str().c_str())); - } + } catch (ErrorWithUnwind &e) { + R_ContinueUnwind(continuation_token); } R_API_END(); - UNPROTECT(1); + UNPROTECT(4); return out; } @@ -676,9 +762,19 @@ XGB_DLL SEXP XGBoosterGetAttr_R(SEXP handle, SEXP name) { XGB_DLL SEXP XGBoosterSetAttr_R(SEXP handle, SEXP name, SEXP val) { R_API_BEGIN(); - const char *v = isNull(val) ? nullptr : CHAR(asChar(val)); + const char *v = nullptr; + SEXP name_ = PROTECT(Rf_asChar(name)); + SEXP val_; + int n_protected = 1; + if (!Rf_isNull(val)) { + val_ = PROTECT(Rf_asChar(val)); + n_protected++; + v = CHAR(val_); + } + CHECK_CALL(XGBoosterSetAttr(R_ExternalPtrAddr(handle), - CHAR(asChar(name)), v)); + CHAR(name_), v)); + UNPROTECT(n_protected); R_API_END(); return R_NilValue; } @@ -707,7 +803,7 @@ XGB_DLL SEXP XGBoosterFeatureScore_R(SEXP handle, SEXP json_config) { SEXP out_features_sexp; SEXP out_scores_sexp; SEXP out_shape_sexp; - SEXP r_out; + SEXP r_out = PROTECT(allocVector(VECSXP, 3)); R_API_BEGIN(); char const *c_json_config = CHAR(asChar(json_config)); @@ -723,23 +819,24 @@ XGB_DLL SEXP XGBoosterFeatureScore_R(SEXP handle, SEXP json_config) { &out_dim, &out_shape, &out_scores)); out_shape_sexp = PROTECT(allocVector(INTSXP, out_dim)); size_t len = 1; + int *out_shape_sexp_ = INTEGER(out_shape_sexp); for (size_t i = 0; i < out_dim; ++i) { - INTEGER(out_shape_sexp)[i] = out_shape[i]; + out_shape_sexp_[i] = out_shape[i]; len *= out_shape[i]; } - out_scores_sexp = PROTECT(allocVector(REALSXP, len)); - auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); - xgboost::common::ParallelFor(len, ctx->Threads(), [&](xgboost::omp_ulong i) { - REAL(out_scores_sexp)[i] = out_scores[i]; - }); - out_features_sexp = PROTECT(allocVector(STRSXP, out_n_features)); for (size_t i = 0; i < out_n_features; ++i) { SET_STRING_ELT(out_features_sexp, i, mkChar(out_features[i])); } - r_out = PROTECT(allocVector(VECSXP, 3)); + out_scores_sexp = PROTECT(allocVector(REALSXP, len)); + auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); + double *out_scores_sexp_ = REAL(out_scores_sexp); + xgboost::common::ParallelFor(len, ctx->Threads(), [&](xgboost::omp_ulong i) { + out_scores_sexp_[i] = out_scores[i]; + }); + SET_VECTOR_ELT(r_out, 0, out_features_sexp); SET_VECTOR_ELT(r_out, 1, out_shape_sexp); SET_VECTOR_ELT(r_out, 2, out_scores_sexp); From 381f1d3dc993cea02b17718c48cd46e68006684c Mon Sep 17 00:00:00 2001 From: Dmitry Razdoburdin Date: Mon, 4 Dec 2023 09:15:57 +0100 Subject: [PATCH 031/109] Add support inference on SYCL devices (#9800) --------- Co-authored-by: Dmitry Razdoburdin <> Co-authored-by: Nikolay Petrov Co-authored-by: Alexandra --- .github/workflows/main.yml | 39 ++ .github/workflows/python_tests.yml | 41 ++ CMakeLists.txt | 18 +- include/xgboost/context.h | 13 +- include/xgboost/predictor.h | 4 +- plugin/CMakeLists.txt | 28 +- plugin/sycl/README.md | 40 ++ plugin/sycl/data.h | 256 ++++++++++ plugin/sycl/device_manager.cc | 124 +++++ plugin/sycl/device_manager.h | 47 ++ plugin/sycl/predictor/predictor.cc | 342 ++++++++++++++ plugin/updater_oneapi/README.md | 42 -- plugin/updater_oneapi/predictor_oneapi.cc | 447 ------------------ .../updater_oneapi/regression_loss_oneapi.h | 145 ------ .../updater_oneapi/regression_obj_oneapi.cc | 182 ------- src/CMakeLists.txt | 4 + src/common/common.h | 8 +- src/gbm/gbtree.cc | 44 +- src/gbm/gbtree.h | 6 +- tests/ci_build/conda_env/linux_sycl_test.yml | 20 + tests/ci_build/lint_cpp.py | 2 +- tests/ci_build/lint_python.py | 1 + tests/cpp/CMakeLists.txt | 6 +- tests/cpp/plugin/test_predictor_oneapi.cc | 168 ------- .../cpp/plugin/test_regression_obj_oneapi.cc | 176 ------- tests/cpp/plugin/test_sycl_predictor.cc | 101 ++++ tests/cpp/predictor/test_cpu_predictor.cc | 92 +--- tests/cpp/predictor/test_gpu_predictor.cu | 6 +- tests/cpp/predictor/test_predictor.cc | 90 +++- tests/cpp/predictor/test_predictor.h | 6 +- tests/python-sycl/test_sycl_prediction.py | 165 +++++++ 31 files changed, 1369 insertions(+), 1294 deletions(-) create mode 100755 plugin/sycl/README.md create mode 100644 plugin/sycl/data.h create mode 100644 plugin/sycl/device_manager.cc create mode 100644 plugin/sycl/device_manager.h create mode 100755 plugin/sycl/predictor/predictor.cc delete mode 100755 plugin/updater_oneapi/README.md delete mode 100755 plugin/updater_oneapi/predictor_oneapi.cc delete mode 100755 plugin/updater_oneapi/regression_loss_oneapi.h delete mode 100755 plugin/updater_oneapi/regression_obj_oneapi.cc create mode 100644 tests/ci_build/conda_env/linux_sycl_test.yml delete mode 100755 tests/cpp/plugin/test_predictor_oneapi.cc delete mode 100755 tests/cpp/plugin/test_regression_obj_oneapi.cc create mode 100755 tests/cpp/plugin/test_sycl_predictor.cc create mode 100644 tests/python-sycl/test_sycl_prediction.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f1252806..20e91a5d9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,45 @@ jobs: cd build ctest --extra-verbose + gtest-cpu-sycl: + name: Test Google C++ unittest (CPU SYCL) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.8"] + steps: + - uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0 + with: + submodules: 'true' + - uses: mamba-org/provision-with-micromamba@f347426e5745fe3dfc13ec5baf20496990d0281f # v14 + with: + cache-downloads: true + cache-env: true + environment-name: linux_sycl_test + environment-file: tests/ci_build/conda_env/linux_sycl_test.yml + + - name: Display Conda env + run: | + conda info + conda list + - name: Build and install XGBoost + shell: bash -l {0} + run: | + mkdir build + cd build + cmake .. -DGOOGLE_TEST=ON -DUSE_DMLC_GTEST=ON -DPLUGIN_SYCL=ON -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX + make -j$(nproc) + - name: Run gtest binary for SYCL + run: | + cd build + ./testxgboost --gtest_filter=Sycl* + - name: Run gtest binary for non SYCL + run: | + cd build + ./testxgboost --gtest_filter=-Sycl* + c-api-demo: name: Test installing XGBoost lib + building the C API demo runs-on: ${{ matrix.os }} diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index e9704c75d..0fca76673 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -256,6 +256,47 @@ jobs: run: | pytest -s -v -rxXs --durations=0 ./tests/test_distributed/test_with_spark + python-sycl-tests-on-ubuntu: + name: Test XGBoost Python package with SYCL on ${{ matrix.config.os }} + runs-on: ${{ matrix.config.os }} + timeout-minutes: 90 + strategy: + matrix: + config: + - {os: ubuntu-latest, python-version: "3.8"} + + steps: + - uses: actions/checkout@v2 + with: + submodules: 'true' + + - uses: mamba-org/provision-with-micromamba@f347426e5745fe3dfc13ec5baf20496990d0281f # v14 + with: + cache-downloads: true + cache-env: true + environment-name: linux_sycl_test + environment-file: tests/ci_build/conda_env/linux_sycl_test.yml + + - name: Display Conda env + run: | + conda info + conda list + - name: Build XGBoost on Ubuntu + run: | + mkdir build + cd build + cmake .. -DPLUGIN_SYCL=ON -DCMAKE_PREFIX_PATH=$CONDA_PREFIX + make -j$(nproc) + - name: Install Python package + run: | + cd python-package + python --version + pip install -v . + - name: Test Python package + run: | + pytest -s -v -rxXs --durations=0 ./tests/python-sycl/ + + python-system-installation-on-ubuntu: name: Test XGBoost Python package System Installation on ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/CMakeLists.txt b/CMakeLists.txt index a9c6f7410..dbfa1cdc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,11 @@ cmake_minimum_required(VERSION 3.18 FATAL_ERROR) + +if(PLUGIN_SYCL) + set(CMAKE_CXX_COMPILER "g++") + set(CMAKE_C_COMPILER "gcc") + string(REPLACE " -isystem ${CONDA_PREFIX}/include" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +endif() + project(xgboost LANGUAGES CXX C VERSION 2.1.0) include(cmake/Utils.cmake) list(APPEND CMAKE_MODULE_PATH "${xgboost_SOURCE_DIR}/cmake/modules") @@ -102,7 +109,7 @@ address, leak, undefined and thread.") option(PLUGIN_RMM "Build with RAPIDS Memory Manager (RMM)" OFF) option(PLUGIN_FEDERATED "Build with Federated Learning" OFF) ## TODO: 1. Add check if DPC++ compiler is used for building -option(PLUGIN_UPDATER_ONEAPI "DPC++ updater" OFF) +option(PLUGIN_SYCL "SYCL plugin" OFF) option(ADD_PKGCONFIG "Add xgboost.pc into system." ON) #-- Checks for building XGBoost @@ -313,6 +320,15 @@ if(PLUGIN_RMM) get_target_property(rmm_link_libs rmm::rmm INTERFACE_LINK_LIBRARIES) endif() +if(PLUGIN_SYCL) + set(CMAKE_CXX_LINK_EXECUTABLE + "icpx -qopenmp -o ") + set(CMAKE_CXX_CREATE_SHARED_LIBRARY + "icpx -qopenmp \ + , \ + -o ") +endif() + #-- library if(BUILD_STATIC_LIB) add_library(xgboost STATIC) diff --git a/include/xgboost/context.h b/include/xgboost/context.h index 6745bcb60..f32a07a03 100644 --- a/include/xgboost/context.h +++ b/include/xgboost/context.h @@ -250,9 +250,15 @@ struct Context : public XGBoostParameter { default: // Do not use the device name as this is likely an internal error, the name // wouldn't be valid. - LOG(FATAL) << "Unknown device type:" - << static_cast>(this->Device().device); - break; + if (this->Device().IsSycl()) { + LOG(WARNING) << "The requested feature doesn't have SYCL specific implementation yet. " + << "CPU implementation is used"; + return cpu_fn(); + } else { + LOG(FATAL) << "Unknown device type:" + << static_cast>(this->Device().device); + break; + } } return std::invoke_result_t(); } @@ -262,7 +268,6 @@ struct Context : public XGBoostParameter { */ template decltype(auto) DispatchDevice(CPUFn&& cpu_fn, CUDAFn&& cuda_fn, SYCLFn&& sycl_fn) const { - static_assert(std::is_same_v, std::invoke_result_t>); static_assert(std::is_same_v, std::invoke_result_t>); if (this->Device().IsSycl()) { return sycl_fn(); diff --git a/include/xgboost/predictor.h b/include/xgboost/predictor.h index 25571213d..6a38d6496 100644 --- a/include/xgboost/predictor.h +++ b/include/xgboost/predictor.h @@ -92,8 +92,8 @@ class Predictor { * \param out_predt Prediction vector to be initialized. * \param model Tree model used for prediction. */ - void InitOutPredictions(const MetaInfo& info, HostDeviceVector* out_predt, - const gbm::GBTreeModel& model) const; + virtual void InitOutPredictions(const MetaInfo& info, HostDeviceVector* out_predt, + const gbm::GBTreeModel& model) const; /** * \brief Generate batch predictions for a given feature matrix. May use diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 58b31053f..0fecb4fb2 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -1,27 +1,29 @@ -if(PLUGIN_UPDATER_ONEAPI) - add_library(oneapi_plugin OBJECT - ${xgboost_SOURCE_DIR}/plugin/updater_oneapi/regression_obj_oneapi.cc - ${xgboost_SOURCE_DIR}/plugin/updater_oneapi/predictor_oneapi.cc) - target_include_directories(oneapi_plugin +if(PLUGIN_SYCL) + set(CMAKE_CXX_COMPILER "icpx") + add_library(plugin_sycl OBJECT + ${xgboost_SOURCE_DIR}/plugin/sycl/device_manager.cc + ${xgboost_SOURCE_DIR}/plugin/sycl/predictor/predictor.cc) + target_include_directories(plugin_sycl PRIVATE ${xgboost_SOURCE_DIR}/include ${xgboost_SOURCE_DIR}/dmlc-core/include ${xgboost_SOURCE_DIR}/rabit/include) - target_compile_definitions(oneapi_plugin PUBLIC -DXGBOOST_USE_ONEAPI=1) - target_link_libraries(oneapi_plugin PUBLIC -fsycl) - set_target_properties(oneapi_plugin PROPERTIES + target_compile_definitions(plugin_sycl PUBLIC -DXGBOOST_USE_SYCL=1) + target_link_libraries(plugin_sycl PUBLIC -fsycl) + set_target_properties(plugin_sycl PROPERTIES COMPILE_FLAGS -fsycl CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON POSITION_INDEPENDENT_CODE ON) if(USE_OPENMP) find_package(OpenMP REQUIRED) - target_link_libraries(oneapi_plugin PUBLIC OpenMP::OpenMP_CXX) + set_target_properties(plugin_sycl PROPERTIES + COMPILE_FLAGS "-fsycl -qopenmp") endif() - # Get compilation and link flags of oneapi_plugin and propagate to objxgboost - target_link_libraries(objxgboost PUBLIC oneapi_plugin) - # Add all objects of oneapi_plugin to objxgboost - target_sources(objxgboost INTERFACE $) + # Get compilation and link flags of plugin_sycl and propagate to objxgboost + target_link_libraries(objxgboost PUBLIC plugin_sycl) + # Add all objects of plugin_sycl to objxgboost + target_sources(objxgboost INTERFACE $) endif() # Add the Federate Learning plugin if enabled. diff --git a/plugin/sycl/README.md b/plugin/sycl/README.md new file mode 100755 index 000000000..b5dc07a1a --- /dev/null +++ b/plugin/sycl/README.md @@ -0,0 +1,40 @@ + + +# SYCL-based Algorithm for Tree Construction +This plugin adds support of SYCL programming model for prediction algorithms to XGBoost. + +## Usage +Specify the 'device' parameter as described in the table below to offload model training and inference on SYCL device. + +### Algorithms +| device | Description | +| --- | --- | +sycl | use default sycl device | +sycl:gpu | use default sycl gpu | +sycl:cpu | use default sycl cpu | +sycl:gpu:N | use sycl gpu number N | +sycl:cpu:N | use sycl cpu number N | + +Python example: +```python +param['device'] = 'sycl:gpu:0' +``` +Note: 'sycl:cpu' devices have full functional support but can't provide good enough performance. We recommend use 'sycl:cpu' devices only for test purposes. +Note: if device is specified to be 'sycl', device type will be automatically chosen. In case the system has both sycl GPU and sycl CPU, GPU will on use. + +## Dependencies +To build and use the plugin, install [Intel® oneAPI DPC++/C++ Compiler](https://www.intel.com/content/www/us/en/developer/tools/oneapi/dpc-compiler.html). +See also [Intel® oneAPI Programming Guide](https://www.intel.com/content/www/us/en/docs/oneapi/programming-guide/2024-0/overview.html). + +## Build +From the ``xgboost`` directory, run: + +```bash +$ mkdir build +$ cd build +$ cmake .. -DPLUGIN_SYCL=ON +$ make -j +``` \ No newline at end of file diff --git a/plugin/sycl/data.h b/plugin/sycl/data.h new file mode 100644 index 000000000..179c7cd1f --- /dev/null +++ b/plugin/sycl/data.h @@ -0,0 +1,256 @@ +/*! + * Copyright by Contributors 2017-2023 + */ +#ifndef PLUGIN_SYCL_DATA_H_ +#define PLUGIN_SYCL_DATA_H_ + +#include +#include +#include +#include +#include +#include + +#include "xgboost/base.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" +#include "xgboost/data.h" +#pragma GCC diagnostic pop +#include "xgboost/logging.h" +#include "xgboost/host_device_vector.h" + +#include "../../src/common/threading_utils.h" + +#include "CL/sycl.hpp" + +namespace xgboost { +namespace sycl { +enum class MemoryType { shared, on_device}; + + +template +class USMDeleter { + public: + explicit USMDeleter(::sycl::queue qu) : qu_(qu) {} + + void operator()(T* data) const { + ::sycl::free(data, qu_); + } + + private: + ::sycl::queue qu_; +}; + +template +class USMVector { + static_assert(std::is_standard_layout::value, "USMVector admits only POD types"); + + std::shared_ptr allocate_memory_(::sycl::queue* qu, size_t size) { + if constexpr (memory_type == MemoryType::shared) { + return std::shared_ptr(::sycl::malloc_shared(size_, *qu), USMDeleter(*qu)); + } else { + return std::shared_ptr(::sycl::malloc_device(size_, *qu), USMDeleter(*qu)); + } + } + + void copy_vector_to_memory_(::sycl::queue* qu, const std::vector &vec) { + if constexpr (memory_type == MemoryType::shared) { + std::copy(vec.begin(), vec.end(), data_.get()); + } else { + qu->memcpy(data_.get(), vec.data(), size_ * sizeof(T)); + } + } + + + public: + USMVector() : size_(0), capacity_(0), data_(nullptr) {} + + USMVector(::sycl::queue& qu, size_t size) : size_(size), capacity_(size) { + data_ = allocate_memory_(qu, size_); + } + + USMVector(::sycl::queue& qu, size_t size, T v) : size_(size), capacity_(size) { + data_ = allocate_memory_(qu, size_); + qu.fill(data_.get(), v, size_).wait(); + } + + USMVector(::sycl::queue* qu, const std::vector &vec) { + size_ = vec.size(); + capacity_ = size_; + data_ = allocate_memory_(qu, size_); + copy_vector_to_memory_(qu, vec); + } + + ~USMVector() { + } + + USMVector& operator=(const USMVector& other) { + size_ = other.size_; + capacity_ = other.capacity_; + data_ = other.data_; + return *this; + } + + T* Data() { return data_.get(); } + const T* DataConst() const { return data_.get(); } + + size_t Size() const { return size_; } + + size_t Capacity() const { return capacity_; } + + T& operator[] (size_t i) { return data_.get()[i]; } + const T& operator[] (size_t i) const { return data_.get()[i]; } + + T* Begin () const { return data_.get(); } + T* End () const { return data_.get() + size_; } + + bool Empty() const { return (size_ == 0); } + + void Clear() { + data_.reset(); + size_ = 0; + capacity_ = 0; + } + + void Resize(::sycl::queue* qu, size_t size_new) { + if (size_new <= capacity_) { + size_ = size_new; + } else { + size_t size_old = size_; + auto data_old = data_; + size_ = size_new; + capacity_ = size_new; + data_ = allocate_memory_(qu, size_);; + if (size_old > 0) { + qu->memcpy(data_.get(), data_old.get(), sizeof(T) * size_old).wait(); + } + } + } + + void Resize(::sycl::queue* qu, size_t size_new, T v) { + if (size_new <= size_) { + size_ = size_new; + } else if (size_new <= capacity_) { + qu->fill(data_.get() + size_, v, size_new - size_).wait(); + size_ = size_new; + } else { + size_t size_old = size_; + auto data_old = data_; + size_ = size_new; + capacity_ = size_new; + data_ = allocate_memory_(qu, size_); + if (size_old > 0) { + qu->memcpy(data_.get(), data_old.get(), sizeof(T) * size_old).wait(); + } + qu->fill(data_.get() + size_old, v, size_new - size_old).wait(); + } + } + + ::sycl::event ResizeAsync(::sycl::queue* qu, size_t size_new, T v) { + if (size_new <= size_) { + size_ = size_new; + return ::sycl::event(); + } else if (size_new <= capacity_) { + auto event = qu->fill(data_.get() + size_, v, size_new - size_); + size_ = size_new; + return event; + } else { + size_t size_old = size_; + auto data_old = data_; + size_ = size_new; + capacity_ = size_new; + data_ = allocate_memory_(qu, size_); + ::sycl::event event; + if (size_old > 0) { + event = qu->memcpy(data_.get(), data_old.get(), sizeof(T) * size_old); + } + return qu->fill(data_.get() + size_old, v, size_new - size_old, event); + } + } + + ::sycl::event ResizeAndFill(::sycl::queue* qu, size_t size_new, int v) { + if (size_new <= size_) { + size_ = size_new; + return qu->memset(data_.get(), v, size_new * sizeof(T)); + } else if (size_new <= capacity_) { + size_ = size_new; + return qu->memset(data_.get(), v, size_new * sizeof(T)); + } else { + size_t size_old = size_; + auto data_old = data_; + size_ = size_new; + capacity_ = size_new; + data_ = allocate_memory_(qu, size_); + return qu->memset(data_.get(), v, size_new * sizeof(T)); + } + } + + ::sycl::event Fill(::sycl::queue* qu, T v) { + return qu->fill(data_.get(), v, size_); + } + + void Init(::sycl::queue* qu, const std::vector &vec) { + size_ = vec.size(); + capacity_ = size_; + data_ = allocate_memory_(qu, size_); + copy_vector_to_memory_(qu, vec); + } + + using value_type = T; // NOLINT + + private: + size_t size_; + size_t capacity_; + std::shared_ptr data_; +}; + +/* Wrapper for DMatrix which stores all batches in a single USM buffer */ +struct DeviceMatrix { + DMatrix* p_mat; // Pointer to the original matrix on the host + ::sycl::queue qu_; + USMVector row_ptr; + USMVector data; + size_t total_offset; + + DeviceMatrix(::sycl::queue qu, DMatrix* dmat) : p_mat(dmat), qu_(qu) { + size_t num_row = 0; + size_t num_nonzero = 0; + for (auto &batch : dmat->GetBatches()) { + const auto& data_vec = batch.data.HostVector(); + const auto& offset_vec = batch.offset.HostVector(); + num_nonzero += data_vec.size(); + num_row += batch.Size(); + } + + row_ptr.Resize(&qu_, num_row + 1); + data.Resize(&qu_, num_nonzero); + + size_t data_offset = 0; + for (auto &batch : dmat->GetBatches()) { + const auto& data_vec = batch.data.HostVector(); + const auto& offset_vec = batch.offset.HostVector(); + size_t batch_size = batch.Size(); + if (batch_size > 0) { + std::copy(offset_vec.data(), offset_vec.data() + batch_size, + row_ptr.Data() + batch.base_rowid); + if (batch.base_rowid > 0) { + for (size_t i = 0; i < batch_size; i++) + row_ptr[i + batch.base_rowid] += batch.base_rowid; + } + std::copy(data_vec.data(), data_vec.data() + offset_vec[batch_size], + data.Data() + data_offset); + data_offset += offset_vec[batch_size]; + } + } + row_ptr[num_row] = data_offset; + total_offset = data_offset; + } + + ~DeviceMatrix() { + } +}; +} // namespace sycl +} // namespace xgboost + +#endif // PLUGIN_SYCL_DATA_H_ diff --git a/plugin/sycl/device_manager.cc b/plugin/sycl/device_manager.cc new file mode 100644 index 000000000..0254cdd6a --- /dev/null +++ b/plugin/sycl/device_manager.cc @@ -0,0 +1,124 @@ +/*! + * Copyright 2017-2023 by Contributors + * \file device_manager.cc + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" +#include +#pragma GCC diagnostic pop + +#include "../sycl/device_manager.h" + +namespace xgboost { +namespace sycl { + +::sycl::device DeviceManager::GetDevice(const DeviceOrd& device_spec) const { + if (!device_spec.IsSycl()) { + LOG(WARNING) << "Sycl kernel is executed with non-sycl context: " + << device_spec.Name() << ". " + << "Default sycl device_selector will be used."; + } + + bool not_use_default_selector = (device_spec.ordinal != kDefaultOrdinal) || + (rabit::IsDistributed()); + if (not_use_default_selector) { + DeviceRegister& device_register = GetDevicesRegister(); + const int device_idx = rabit::IsDistributed() ? rabit::GetRank() : device_spec.ordinal; + if (device_spec.IsSyclDefault()) { + auto& devices = device_register.devices; + CHECK_LT(device_idx, devices.size()); + return devices[device_idx]; + } else if (device_spec.IsSyclCPU()) { + auto& cpu_devices = device_register.cpu_devices; + CHECK_LT(device_idx, cpu_devices.size()); + return cpu_devices[device_idx]; + } else { + auto& gpu_devices = device_register.gpu_devices; + CHECK_LT(device_idx, gpu_devices.size()); + return gpu_devices[device_idx]; + } + } else { + if (device_spec.IsSyclCPU()) { + return ::sycl::device(::sycl::cpu_selector_v); + } else if (device_spec.IsSyclGPU()) { + return ::sycl::device(::sycl::gpu_selector_v); + } else { + return ::sycl::device(::sycl::default_selector_v); + } + } +} + +::sycl::queue DeviceManager::GetQueue(const DeviceOrd& device_spec) const { + if (!device_spec.IsSycl()) { + LOG(WARNING) << "Sycl kernel is executed with non-sycl context: " + << device_spec.Name() << ". " + << "Default sycl device_selector will be used."; + } + + QueueRegister_t& queue_register = GetQueueRegister(); + if (queue_register.count(device_spec.Name()) > 0) { + return queue_register.at(device_spec.Name()); + } + + bool not_use_default_selector = (device_spec.ordinal != kDefaultOrdinal) || + (rabit::IsDistributed()); + std::lock_guard guard(queue_registering_mutex); + if (not_use_default_selector) { + DeviceRegister& device_register = GetDevicesRegister(); + const int device_idx = rabit::IsDistributed() ? rabit::GetRank() : device_spec.ordinal; + if (device_spec.IsSyclDefault()) { + auto& devices = device_register.devices; + CHECK_LT(device_idx, devices.size()); + queue_register[device_spec.Name()] = ::sycl::queue(devices[device_idx]); + } else if (device_spec.IsSyclCPU()) { + auto& cpu_devices = device_register.cpu_devices; + CHECK_LT(device_idx, cpu_devices.size()); + queue_register[device_spec.Name()] = ::sycl::queue(cpu_devices[device_idx]);; + } else if (device_spec.IsSyclGPU()) { + auto& gpu_devices = device_register.gpu_devices; + CHECK_LT(device_idx, gpu_devices.size()); + queue_register[device_spec.Name()] = ::sycl::queue(gpu_devices[device_idx]); + } + } else { + if (device_spec.IsSyclCPU()) { + queue_register[device_spec.Name()] = ::sycl::queue(::sycl::cpu_selector_v); + } else if (device_spec.IsSyclGPU()) { + queue_register[device_spec.Name()] = ::sycl::queue(::sycl::gpu_selector_v); + } else { + queue_register[device_spec.Name()] = ::sycl::queue(::sycl::default_selector_v); + } + } + return queue_register.at(device_spec.Name()); +} + +DeviceManager::DeviceRegister& DeviceManager::GetDevicesRegister() const { + static DeviceRegister device_register; + + if (device_register.devices.size() == 0) { + std::lock_guard guard(device_registering_mutex); + std::vector<::sycl::device> devices = ::sycl::device::get_devices(); + for (size_t i = 0; i < devices.size(); i++) { + LOG(INFO) << "device_index = " << i << ", name = " + << devices[i].get_info<::sycl::info::device::name>(); + } + + for (size_t i = 0; i < devices.size(); i++) { + device_register.devices.push_back(devices[i]); + if (devices[i].is_cpu()) { + device_register.cpu_devices.push_back(devices[i]); + } else if (devices[i].is_gpu()) { + device_register.gpu_devices.push_back(devices[i]); + } + } + } + return device_register; +} + +DeviceManager::QueueRegister_t& DeviceManager::GetQueueRegister() const { + static QueueRegister_t queue_register; + return queue_register; +} + +} // namespace sycl +} // namespace xgboost diff --git a/plugin/sycl/device_manager.h b/plugin/sycl/device_manager.h new file mode 100644 index 000000000..0ae2ee9fe --- /dev/null +++ b/plugin/sycl/device_manager.h @@ -0,0 +1,47 @@ +/*! + * Copyright 2017-2023 by Contributors + * \file device_manager.h + */ +#ifndef PLUGIN_SYCL_DEVICE_MANAGER_H_ +#define PLUGIN_SYCL_DEVICE_MANAGER_H_ + +#include +#include +#include +#include + +#include + +#include "xgboost/context.h" + +namespace xgboost { +namespace sycl { + +class DeviceManager { + public: + ::sycl::queue GetQueue(const DeviceOrd& device_spec) const; + + ::sycl::device GetDevice(const DeviceOrd& device_spec) const; + + private: + using QueueRegister_t = std::unordered_map; + constexpr static int kDefaultOrdinal = -1; + + struct DeviceRegister { + std::vector<::sycl::device> devices; + std::vector<::sycl::device> cpu_devices; + std::vector<::sycl::device> gpu_devices; + }; + + QueueRegister_t& GetQueueRegister() const; + + DeviceRegister& GetDevicesRegister() const; + + mutable std::mutex queue_registering_mutex; + mutable std::mutex device_registering_mutex; +}; + +} // namespace sycl +} // namespace xgboost + +#endif // PLUGIN_SYCL_DEVICE_MANAGER_H_ diff --git a/plugin/sycl/predictor/predictor.cc b/plugin/sycl/predictor/predictor.cc new file mode 100755 index 000000000..3ceb99f1e --- /dev/null +++ b/plugin/sycl/predictor/predictor.cc @@ -0,0 +1,342 @@ +/*! + * Copyright by Contributors 2017-2023 + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" +#include +#pragma GCC diagnostic pop + +#include +#include +#include + +#include + +#include "../data.h" + +#include "dmlc/registry.h" + +#include "xgboost/tree_model.h" +#include "xgboost/predictor.h" +#include "xgboost/tree_updater.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#include "../../src/data/adapter.h" +#pragma GCC diagnostic pop +#include "../../src/common/math.h" +#include "../../src/gbm/gbtree_model.h" + +#include "../device_manager.h" + +namespace xgboost { +namespace sycl { +namespace predictor { + +DMLC_REGISTRY_FILE_TAG(predictor_sycl); + +/* Wrapper for descriptor of a tree node */ +struct DeviceNode { + DeviceNode() + : fidx(-1), left_child_idx(-1), right_child_idx(-1) {} + + union NodeValue { + float leaf_weight; + float fvalue; + }; + + int fidx; + int left_child_idx; + int right_child_idx; + NodeValue val; + + explicit DeviceNode(const RegTree::Node& n) { + this->left_child_idx = n.LeftChild(); + this->right_child_idx = n.RightChild(); + this->fidx = n.SplitIndex(); + if (n.DefaultLeft()) { + fidx |= (1U << 31); + } + + if (n.IsLeaf()) { + this->val.leaf_weight = n.LeafValue(); + } else { + this->val.fvalue = n.SplitCond(); + } + } + + bool IsLeaf() const { return left_child_idx == -1; } + + int GetFidx() const { return fidx & ((1U << 31) - 1U); } + + bool MissingLeft() const { return (fidx >> 31) != 0; } + + int MissingIdx() const { + if (MissingLeft()) { + return this->left_child_idx; + } else { + return this->right_child_idx; + } + } + + float GetFvalue() const { return val.fvalue; } + + float GetWeight() const { return val.leaf_weight; } +}; + +/* SYCL implementation of a device model, + * storing tree structure in USM buffers to provide access from device kernels + */ +class DeviceModel { + public: + ::sycl::queue qu_; + USMVector nodes_; + USMVector tree_segments_; + USMVector tree_group_; + size_t tree_beg_; + size_t tree_end_; + int num_group_; + + DeviceModel() {} + + ~DeviceModel() {} + + void Init(::sycl::queue qu, const gbm::GBTreeModel& model, size_t tree_begin, size_t tree_end) { + qu_ = qu; + + tree_segments_.Resize(&qu_, (tree_end - tree_begin) + 1); + int sum = 0; + tree_segments_[0] = sum; + for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { + if (model.trees[tree_idx]->HasCategoricalSplit()) { + LOG(FATAL) << "Categorical features are not yet supported by sycl"; + } + sum += model.trees[tree_idx]->GetNodes().size(); + tree_segments_[tree_idx - tree_begin + 1] = sum; + } + + nodes_.Resize(&qu_, sum); + for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { + auto& src_nodes = model.trees[tree_idx]->GetNodes(); + for (size_t node_idx = 0; node_idx < src_nodes.size(); node_idx++) + nodes_[node_idx + tree_segments_[tree_idx - tree_begin]] = + static_cast(src_nodes[node_idx]); + } + + tree_group_.Resize(&qu_, model.tree_info.size()); + for (size_t tree_idx = 0; tree_idx < model.tree_info.size(); tree_idx++) + tree_group_[tree_idx] = model.tree_info[tree_idx]; + + tree_beg_ = tree_begin; + tree_end_ = tree_end; + num_group_ = model.learner_model_param->num_output_group; + } +}; + +float GetFvalue(int ridx, int fidx, Entry* data, size_t* row_ptr, bool* is_missing) { + // Binary search + auto begin_ptr = data + row_ptr[ridx]; + auto end_ptr = data + row_ptr[ridx + 1]; + Entry* previous_middle = nullptr; + while (end_ptr != begin_ptr) { + auto middle = begin_ptr + (end_ptr - begin_ptr) / 2; + if (middle == previous_middle) { + break; + } else { + previous_middle = middle; + } + + if (middle->index == fidx) { + *is_missing = false; + return middle->fvalue; + } else if (middle->index < fidx) { + begin_ptr = middle; + } else { + end_ptr = middle; + } + } + *is_missing = true; + return 0.0; +} + +float GetLeafWeight(int ridx, const DeviceNode* tree, Entry* data, size_t* row_ptr) { + DeviceNode n = tree[0]; + int node_id = 0; + bool is_missing; + while (!n.IsLeaf()) { + float fvalue = GetFvalue(ridx, n.GetFidx(), data, row_ptr, &is_missing); + // Missing value + if (is_missing) { + n = tree[n.MissingIdx()]; + } else { + if (fvalue < n.GetFvalue()) { + node_id = n.left_child_idx; + n = tree[n.left_child_idx]; + } else { + node_id = n.right_child_idx; + n = tree[n.right_child_idx]; + } + } + } + return n.GetWeight(); +} + +void DevicePredictInternal(::sycl::queue qu, + sycl::DeviceMatrix* dmat, + HostDeviceVector* out_preds, + const gbm::GBTreeModel& model, + size_t tree_begin, + size_t tree_end) { + if (tree_end - tree_begin == 0) return; + if (out_preds->HostVector().size() == 0) return; + + DeviceModel device_model; + device_model.Init(qu, model, tree_begin, tree_end); + + auto& out_preds_vec = out_preds->HostVector(); + + DeviceNode* nodes = device_model.nodes_.Data(); + ::sycl::buffer out_preds_buf(out_preds_vec.data(), out_preds_vec.size()); + size_t* tree_segments = device_model.tree_segments_.Data(); + int* tree_group = device_model.tree_group_.Data(); + size_t* row_ptr = dmat->row_ptr.Data(); + Entry* data = dmat->data.Data(); + int num_features = dmat->p_mat->Info().num_col_; + int num_rows = dmat->row_ptr.Size() - 1; + int num_group = model.learner_model_param->num_output_group; + + qu.submit([&](::sycl::handler& cgh) { + auto out_predictions = out_preds_buf.template get_access<::sycl::access::mode::read_write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(num_rows), [=](::sycl::id<1> pid) { + int global_idx = pid[0]; + if (global_idx >= num_rows) return; + if (num_group == 1) { + float sum = 0.0; + for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { + const DeviceNode* tree = nodes + tree_segments[tree_idx - tree_begin]; + sum += GetLeafWeight(global_idx, tree, data, row_ptr); + } + out_predictions[global_idx] += sum; + } else { + for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { + const DeviceNode* tree = nodes + tree_segments[tree_idx - tree_begin]; + int out_prediction_idx = global_idx * num_group + tree_group[tree_idx]; + out_predictions[out_prediction_idx] += GetLeafWeight(global_idx, tree, data, row_ptr); + } + } + }); + }).wait(); +} + +class Predictor : public xgboost::Predictor { + protected: + void InitOutPredictions(const MetaInfo& info, + HostDeviceVector* out_preds, + const gbm::GBTreeModel& model) const override { + CHECK_NE(model.learner_model_param->num_output_group, 0); + size_t n = model.learner_model_param->num_output_group * info.num_row_; + const auto& base_margin = info.base_margin_.Data()->HostVector(); + out_preds->Resize(n); + std::vector& out_preds_h = out_preds->HostVector(); + if (base_margin.size() == n) { + CHECK_EQ(out_preds->Size(), n); + std::copy(base_margin.begin(), base_margin.end(), out_preds_h.begin()); + } else { + auto base_score = model.learner_model_param->BaseScore(ctx_)(0); + if (!base_margin.empty()) { + std::ostringstream oss; + oss << "Ignoring the base margin, since it has incorrect length. " + << "The base margin must be an array of length "; + if (model.learner_model_param->num_output_group > 1) { + oss << "[num_class] * [number of data points], i.e. " + << model.learner_model_param->num_output_group << " * " << info.num_row_ + << " = " << n << ". "; + } else { + oss << "[number of data points], i.e. " << info.num_row_ << ". "; + } + oss << "Instead, all data points will use " + << "base_score = " << base_score; + LOG(WARNING) << oss.str(); + } + std::fill(out_preds_h.begin(), out_preds_h.end(), base_score); + } + } + + public: + explicit Predictor(Context const* context) : + xgboost::Predictor::Predictor{context}, + cpu_predictor(xgboost::Predictor::Create("cpu_predictor", context)) {} + + void PredictBatch(DMatrix *dmat, PredictionCacheEntry *predts, + const gbm::GBTreeModel &model, uint32_t tree_begin, + uint32_t tree_end = 0) const override { + ::sycl::queue qu = device_manager.GetQueue(ctx_->Device()); + // TODO(razdoburdin): remove temporary workaround after cache fix + sycl::DeviceMatrix device_matrix(qu, dmat); + + auto* out_preds = &predts->predictions; + if (tree_end == 0) { + tree_end = model.trees.size(); + } + + if (tree_begin < tree_end) { + DevicePredictInternal(qu, &device_matrix, out_preds, model, tree_begin, tree_end); + } + } + + bool InplacePredict(std::shared_ptr p_m, + const gbm::GBTreeModel &model, float missing, + PredictionCacheEntry *out_preds, uint32_t tree_begin, + unsigned tree_end) const override { + LOG(WARNING) << "InplacePredict is not yet implemented for SYCL. CPU Predictor is used."; + return cpu_predictor->InplacePredict(p_m, model, missing, out_preds, tree_begin, tree_end); + } + + void PredictInstance(const SparsePage::Inst& inst, + std::vector* out_preds, + const gbm::GBTreeModel& model, unsigned ntree_limit, + bool is_column_split) const override { + LOG(WARNING) << "PredictInstance is not yet implemented for SYCL. CPU Predictor is used."; + cpu_predictor->PredictInstance(inst, out_preds, model, ntree_limit, is_column_split); + } + + void PredictLeaf(DMatrix* p_fmat, HostDeviceVector* out_preds, + const gbm::GBTreeModel& model, unsigned ntree_limit) const override { + LOG(WARNING) << "PredictLeaf is not yet implemented for SYCL. CPU Predictor is used."; + cpu_predictor->PredictLeaf(p_fmat, out_preds, model, ntree_limit); + } + + void PredictContribution(DMatrix* p_fmat, HostDeviceVector* out_contribs, + const gbm::GBTreeModel& model, uint32_t ntree_limit, + const std::vector* tree_weights, + bool approximate, int condition, + unsigned condition_feature) const override { + LOG(WARNING) << "PredictContribution is not yet implemented for SYCL. CPU Predictor is used."; + cpu_predictor->PredictContribution(p_fmat, out_contribs, model, ntree_limit, tree_weights, + approximate, condition, condition_feature); + } + + void PredictInteractionContributions(DMatrix* p_fmat, HostDeviceVector* out_contribs, + const gbm::GBTreeModel& model, unsigned ntree_limit, + const std::vector* tree_weights, + bool approximate) const override { + LOG(WARNING) << "PredictInteractionContributions is not yet implemented for SYCL. " + << "CPU Predictor is used."; + cpu_predictor->PredictInteractionContributions(p_fmat, out_contribs, model, ntree_limit, + tree_weights, approximate); + } + + private: + DeviceManager device_manager; + + std::unique_ptr cpu_predictor; +}; + +XGBOOST_REGISTER_PREDICTOR(Predictor, "sycl_predictor") +.describe("Make predictions using SYCL.") +.set_body([](Context const* ctx) { return new Predictor(ctx); }); + +} // namespace predictor +} // namespace sycl +} // namespace xgboost diff --git a/plugin/updater_oneapi/README.md b/plugin/updater_oneapi/README.md deleted file mode 100755 index c2faf6574..000000000 --- a/plugin/updater_oneapi/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# DPC++-based Algorithm for Tree Construction -This plugin adds support of OneAPI programming model for tree construction and prediction algorithms to XGBoost. - -## Usage -Specify the 'objective' parameter as one of the following options to offload computation of objective function on OneAPI device. - -### Algorithms -| objective | Description | -| --- | --- | -reg:squarederror_oneapi | regression with squared loss | -reg:squaredlogerror_oneapi | regression with root mean squared logarithmic loss | -reg:logistic_oneapi | logistic regression for probability regression task | -binary:logistic_oneapi | logistic regression for binary classification task | -binary:logitraw_oneapi | logistic regression for classification, output score before logistic transformation | - -Specify the 'predictor' parameter as one of the following options to offload prediction stage on OneAPI device. - -### Algorithms -| predictor | Description | -| --- | --- | -predictor_oneapi | prediction using OneAPI device | - -Please note that parameter names are not finalized and can be changed during further integration of OneAPI support. - -Python example: -```python -param['predictor'] = 'predictor_oneapi' -param['objective'] = 'reg:squarederror_oneapi' -``` - -## Dependencies -Building the plugin requires Data Parallel C++ Compiler (https://software.intel.com/content/www/us/en/develop/tools/oneapi/components/dpc-compiler.html) - -## Build -From the command line on Linux starting from the xgboost directory: - -```bash -$ mkdir build -$ cd build -$ EXPORT CXX=dpcpp && cmake .. -DPLUGIN_UPDATER_ONEAPI=ON -$ make -j -``` diff --git a/plugin/updater_oneapi/predictor_oneapi.cc b/plugin/updater_oneapi/predictor_oneapi.cc deleted file mode 100755 index 25a14186c..000000000 --- a/plugin/updater_oneapi/predictor_oneapi.cc +++ /dev/null @@ -1,447 +0,0 @@ -/*! - * Copyright by Contributors 2017-2020 - */ -#include // for any -#include -#include -#include - -#include "../../src/common/math.h" -#include "../../src/data/adapter.h" -#include "../../src/gbm/gbtree_model.h" -#include "CL/sycl.hpp" -#include "xgboost/base.h" -#include "xgboost/data.h" -#include "xgboost/host_device_vector.h" -#include "xgboost/logging.h" -#include "xgboost/predictor.h" -#include "xgboost/tree_model.h" -#include "xgboost/tree_updater.h" - -namespace xgboost { -namespace predictor { - -DMLC_REGISTRY_FILE_TAG(predictor_oneapi); - -/*! \brief Element from a sparse vector */ -struct EntryOneAPI { - /*! \brief feature index */ - bst_feature_t index; - /*! \brief feature value */ - bst_float fvalue; - /*! \brief default constructor */ - EntryOneAPI() = default; - /*! - * \brief constructor with index and value - * \param index The feature or row index. - * \param fvalue The feature value. - */ - EntryOneAPI(bst_feature_t index, bst_float fvalue) : index(index), fvalue(fvalue) {} - - EntryOneAPI(const Entry& entry) : index(entry.index), fvalue(entry.fvalue) {} - - /*! \brief reversely compare feature values */ - inline static bool CmpValue(const EntryOneAPI& a, const EntryOneAPI& b) { - return a.fvalue < b.fvalue; - } - inline bool operator==(const EntryOneAPI& other) const { - return (this->index == other.index && this->fvalue == other.fvalue); - } -}; - -struct DeviceMatrixOneAPI { - DMatrix* p_mat; // Pointer to the original matrix on the host - cl::sycl::queue qu_; - size_t* row_ptr; - size_t row_ptr_size; - EntryOneAPI* data; - - DeviceMatrixOneAPI(DMatrix* dmat, cl::sycl::queue qu) : p_mat(dmat), qu_(qu) { - size_t num_row = 0; - size_t num_nonzero = 0; - for (auto &batch : dmat->GetBatches()) { - const auto& data_vec = batch.data.HostVector(); - const auto& offset_vec = batch.offset.HostVector(); - num_nonzero += data_vec.size(); - num_row += batch.Size(); - } - - row_ptr = cl::sycl::malloc_shared(num_row + 1, qu_); - data = cl::sycl::malloc_shared(num_nonzero, qu_); - - size_t data_offset = 0; - for (auto &batch : dmat->GetBatches()) { - const auto& data_vec = batch.data.HostVector(); - const auto& offset_vec = batch.offset.HostVector(); - size_t batch_size = batch.Size(); - if (batch_size > 0) { - std::copy(offset_vec.data(), offset_vec.data() + batch_size, - row_ptr + batch.base_rowid); - if (batch.base_rowid > 0) { - for(size_t i = 0; i < batch_size; i++) - row_ptr[i + batch.base_rowid] += batch.base_rowid; - } - std::copy(data_vec.data(), data_vec.data() + offset_vec[batch_size], - data + data_offset); - data_offset += offset_vec[batch_size]; - } - } - row_ptr[num_row] = data_offset; - row_ptr_size = num_row + 1; - } - - ~DeviceMatrixOneAPI() { - if (row_ptr) { - cl::sycl::free(row_ptr, qu_); - } - if (data) { - cl::sycl::free(data, qu_); - } - } -}; - -struct DeviceNodeOneAPI { - DeviceNodeOneAPI() - : fidx(-1), left_child_idx(-1), right_child_idx(-1) {} - - union NodeValue { - float leaf_weight; - float fvalue; - }; - - int fidx; - int left_child_idx; - int right_child_idx; - NodeValue val; - - DeviceNodeOneAPI(const RegTree::Node& n) { // NOLINT - this->left_child_idx = n.LeftChild(); - this->right_child_idx = n.RightChild(); - this->fidx = n.SplitIndex(); - if (n.DefaultLeft()) { - fidx |= (1U << 31); - } - - if (n.IsLeaf()) { - this->val.leaf_weight = n.LeafValue(); - } else { - this->val.fvalue = n.SplitCond(); - } - } - - bool IsLeaf() const { return left_child_idx == -1; } - - int GetFidx() const { return fidx & ((1U << 31) - 1U); } - - bool MissingLeft() const { return (fidx >> 31) != 0; } - - int MissingIdx() const { - if (MissingLeft()) { - return this->left_child_idx; - } else { - return this->right_child_idx; - } - } - - float GetFvalue() const { return val.fvalue; } - - float GetWeight() const { return val.leaf_weight; } -}; - -class DeviceModelOneAPI { - public: - cl::sycl::queue qu_; - DeviceNodeOneAPI* nodes; - size_t* tree_segments; - int* tree_group; - size_t tree_beg_; - size_t tree_end_; - int num_group; - - DeviceModelOneAPI() : nodes(nullptr), tree_segments(nullptr), tree_group(nullptr) {} - - ~DeviceModelOneAPI() { - Reset(); - } - - void Reset() { - if (nodes) - cl::sycl::free(nodes, qu_); - if (tree_segments) - cl::sycl::free(tree_segments, qu_); - if (tree_group) - cl::sycl::free(tree_group, qu_); - } - - void Init(const gbm::GBTreeModel& model, size_t tree_begin, size_t tree_end, cl::sycl::queue qu) { - qu_ = qu; - CHECK_EQ(model.param.size_leaf_vector, 0); - Reset(); - - tree_segments = cl::sycl::malloc_shared((tree_end - tree_begin) + 1, qu_); - int sum = 0; - tree_segments[0] = sum; - for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - sum += model.trees[tree_idx]->GetNodes().size(); - tree_segments[tree_idx - tree_begin + 1] = sum; - } - - nodes = cl::sycl::malloc_shared(sum, qu_); - for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - auto& src_nodes = model.trees[tree_idx]->GetNodes(); - for (size_t node_idx = 0; node_idx < src_nodes.size(); node_idx++) - nodes[node_idx + tree_segments[tree_idx - tree_begin]] = src_nodes[node_idx]; - } - - tree_group = cl::sycl::malloc_shared(model.tree_info.size(), qu_); - for (size_t tree_idx = 0; tree_idx < model.tree_info.size(); tree_idx++) - tree_group[tree_idx] = model.tree_info[tree_idx]; - - tree_beg_ = tree_begin; - tree_end_ = tree_end; - num_group = model.learner_model_param->num_output_group; - } -}; - -float GetFvalue(int ridx, int fidx, EntryOneAPI* data, size_t* row_ptr, bool& is_missing) { - // Binary search - auto begin_ptr = data + row_ptr[ridx]; - auto end_ptr = data + row_ptr[ridx + 1]; - EntryOneAPI* previous_middle = nullptr; - while (end_ptr != begin_ptr) { - auto middle = begin_ptr + (end_ptr - begin_ptr) / 2; - if (middle == previous_middle) { - break; - } else { - previous_middle = middle; - } - - if (middle->index == fidx) { - is_missing = false; - return middle->fvalue; - } else if (middle->index < fidx) { - begin_ptr = middle; - } else { - end_ptr = middle; - } - } - is_missing = true; - return 0.0; -} - -float GetLeafWeight(int ridx, const DeviceNodeOneAPI* tree, EntryOneAPI* data, size_t* row_ptr) { - DeviceNodeOneAPI n = tree[0]; - int node_id = 0; - bool is_missing; - while (!n.IsLeaf()) { - float fvalue = GetFvalue(ridx, n.GetFidx(), data, row_ptr, is_missing); - // Missing value - if (is_missing) { - n = tree[n.MissingIdx()]; - } else { - if (fvalue < n.GetFvalue()) { - node_id = n.left_child_idx; - n = tree[n.left_child_idx]; - } else { - node_id = n.right_child_idx; - n = tree[n.right_child_idx]; - } - } - } - return n.GetWeight(); -} - -class PredictorOneAPI : public Predictor { - protected: - void InitOutPredictions(const MetaInfo& info, - HostDeviceVector* out_preds, - const gbm::GBTreeModel& model) const { - CHECK_NE(model.learner_model_param->num_output_group, 0); - size_t n = model.learner_model_param->num_output_group * info.num_row_; - const auto& base_margin = info.base_margin_.HostVector(); - out_preds->Resize(n); - std::vector& out_preds_h = out_preds->HostVector(); - if (base_margin.size() == n) { - CHECK_EQ(out_preds->Size(), n); - std::copy(base_margin.begin(), base_margin.end(), out_preds_h.begin()); - } else { - if (!base_margin.empty()) { - std::ostringstream oss; - oss << "Ignoring the base margin, since it has incorrect length. " - << "The base margin must be an array of length "; - if (model.learner_model_param->num_output_group > 1) { - oss << "[num_class] * [number of data points], i.e. " - << model.learner_model_param->num_output_group << " * " << info.num_row_ - << " = " << n << ". "; - } else { - oss << "[number of data points], i.e. " << info.num_row_ << ". "; - } - oss << "Instead, all data points will use " - << "base_score = " << model.learner_model_param->base_score; - LOG(WARNING) << oss.str(); - } - std::fill(out_preds_h.begin(), out_preds_h.end(), - model.learner_model_param->base_score); - } - } - - void DevicePredictInternal(DeviceMatrixOneAPI* dmat, HostDeviceVector* out_preds, - const gbm::GBTreeModel& model, size_t tree_begin, - size_t tree_end) { - if (tree_end - tree_begin == 0) { - return; - } - model_.Init(model, tree_begin, tree_end, qu_); - - auto& out_preds_vec = out_preds->HostVector(); - - DeviceNodeOneAPI* nodes = model_.nodes; - cl::sycl::buffer out_preds_buf(out_preds_vec.data(), out_preds_vec.size()); - size_t* tree_segments = model_.tree_segments; - int* tree_group = model_.tree_group; - size_t* row_ptr = dmat->row_ptr; - EntryOneAPI* data = dmat->data; - int num_features = dmat->p_mat->Info().num_col_; - int num_rows = dmat->row_ptr_size - 1; - int num_group = model.learner_model_param->num_output_group; - - qu_.submit([&](cl::sycl::handler& cgh) { - auto out_predictions = out_preds_buf.get_access(cgh); - cgh.parallel_for(cl::sycl::range<1>(num_rows), [=](cl::sycl::id<1> pid) { - int global_idx = pid[0]; - if (global_idx >= num_rows) return; - if (num_group == 1) { - float sum = 0.0; - for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - const DeviceNodeOneAPI* tree = nodes + tree_segments[tree_idx - tree_begin]; - sum += GetLeafWeight(global_idx, tree, data, row_ptr); - } - out_predictions[global_idx] += sum; - } else { - for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - const DeviceNodeOneAPI* tree = nodes + tree_segments[tree_idx - tree_begin]; - int out_prediction_idx = global_idx * num_group + tree_group[tree_idx]; - out_predictions[out_prediction_idx] += GetLeafWeight(global_idx, tree, data, row_ptr); - } - } - }); - }).wait(); - } - - public: - explicit PredictorOneAPI(Context const* generic_param) : - Predictor::Predictor{generic_param}, cpu_predictor(Predictor::Create("cpu_predictor", generic_param)) { - cl::sycl::default_selector selector; - qu_ = cl::sycl::queue(selector); - } - - // ntree_limit is a very problematic parameter, as it's ambiguous in the context of - // multi-output and forest. Same problem exists for tree_begin - void PredictBatch(DMatrix* dmat, PredictionCacheEntry* predts, - const gbm::GBTreeModel& model, int tree_begin, - uint32_t const ntree_limit = 0) override { - if (this->device_matrix_cache_.find(dmat) == - this->device_matrix_cache_.end()) { - this->device_matrix_cache_.emplace( - dmat, std::unique_ptr( - new DeviceMatrixOneAPI(dmat, qu_))); - } - DeviceMatrixOneAPI* device_matrix = device_matrix_cache_.find(dmat)->second.get(); - - // tree_begin is not used, right now we just enforce it to be 0. - CHECK_EQ(tree_begin, 0); - auto* out_preds = &predts->predictions; - CHECK_GE(predts->version, tree_begin); - if (out_preds->Size() == 0 && dmat->Info().num_row_ != 0) { - CHECK_EQ(predts->version, 0); - } - if (predts->version == 0) { - // out_preds->Size() can be non-zero as it's initialized here before any tree is - // built at the 0^th iterator. - this->InitOutPredictions(dmat->Info(), out_preds, model); - } - - uint32_t const output_groups = model.learner_model_param->num_output_group; - CHECK_NE(output_groups, 0); - // Right now we just assume ntree_limit provided by users means number of tree layers - // in the context of multi-output model - uint32_t real_ntree_limit = ntree_limit * output_groups; - if (real_ntree_limit == 0 || real_ntree_limit > model.trees.size()) { - real_ntree_limit = static_cast(model.trees.size()); - } - - uint32_t const end_version = (tree_begin + real_ntree_limit) / output_groups; - // When users have provided ntree_limit, end_version can be lesser, cache is violated - if (predts->version > end_version) { - CHECK_NE(ntree_limit, 0); - this->InitOutPredictions(dmat->Info(), out_preds, model); - predts->version = 0; - } - uint32_t const beg_version = predts->version; - CHECK_LE(beg_version, end_version); - - if (beg_version < end_version) { - DevicePredictInternal(device_matrix, out_preds, model, - beg_version * output_groups, - end_version * output_groups); - } - - // delta means {size of forest} * {number of newly accumulated layers} - uint32_t delta = end_version - beg_version; - CHECK_LE(delta, model.trees.size()); - predts->Update(delta); - - CHECK(out_preds->Size() == output_groups * dmat->Info().num_row_ || - out_preds->Size() == dmat->Info().num_row_); - } - - void InplacePredict(std::any const& x, const gbm::GBTreeModel& model, float missing, - PredictionCacheEntry* out_preds, uint32_t tree_begin, - unsigned tree_end) const override { - cpu_predictor->InplacePredict(x, model, missing, out_preds, tree_begin, tree_end); - } - - void PredictInstance(const SparsePage::Inst& inst, - std::vector* out_preds, - const gbm::GBTreeModel& model, unsigned ntree_limit) override { - cpu_predictor->PredictInstance(inst, out_preds, model, ntree_limit); - } - - void PredictLeaf(DMatrix* p_fmat, std::vector* out_preds, - const gbm::GBTreeModel& model, unsigned ntree_limit) override { - cpu_predictor->PredictLeaf(p_fmat, out_preds, model, ntree_limit); - } - - void PredictContribution(DMatrix* p_fmat, std::vector* out_contribs, - const gbm::GBTreeModel& model, uint32_t ntree_limit, - std::vector* tree_weights, - bool approximate, int condition, - unsigned condition_feature) override { - cpu_predictor->PredictContribution(p_fmat, out_contribs, model, ntree_limit, tree_weights, approximate, condition, condition_feature); - } - - void PredictInteractionContributions(DMatrix* p_fmat, std::vector* out_contribs, - const gbm::GBTreeModel& model, unsigned ntree_limit, - std::vector* tree_weights, - bool approximate) override { - cpu_predictor->PredictInteractionContributions(p_fmat, out_contribs, model, ntree_limit, tree_weights, approximate); - } - - private: - cl::sycl::queue qu_; - DeviceModelOneAPI model_; - - std::mutex lock_; - std::unique_ptr cpu_predictor; - - std::unordered_map> - device_matrix_cache_; -}; - -XGBOOST_REGISTER_PREDICTOR(PredictorOneAPI, "oneapi_predictor") -.describe("Make predictions using DPC++.") -.set_body([](Context const* generic_param) { - return new PredictorOneAPI(generic_param); - }); -} // namespace predictor -} // namespace xgboost diff --git a/plugin/updater_oneapi/regression_loss_oneapi.h b/plugin/updater_oneapi/regression_loss_oneapi.h deleted file mode 100755 index b0299ff7f..000000000 --- a/plugin/updater_oneapi/regression_loss_oneapi.h +++ /dev/null @@ -1,145 +0,0 @@ -/*! - * Copyright 2017-2020 XGBoost contributors - */ -#ifndef XGBOOST_OBJECTIVE_REGRESSION_LOSS_ONEAPI_H_ -#define XGBOOST_OBJECTIVE_REGRESSION_LOSS_ONEAPI_H_ - -#include -#include -#include - -#include "CL/sycl.hpp" - -namespace xgboost { -namespace obj { - -/*! - * \brief calculate the sigmoid of the input. - * \param x input parameter - * \return the transformed value. - */ -inline float SigmoidOneAPI(float x) { - return 1.0f / (1.0f + cl::sycl::exp(-x)); -} - -// common regressions -// linear regression -struct LinearSquareLossOneAPI { - static bst_float PredTransform(bst_float x) { return x; } - static bool CheckLabel(bst_float x) { return true; } - static bst_float FirstOrderGradient(bst_float predt, bst_float label) { - return predt - label; - } - static bst_float SecondOrderGradient(bst_float predt, bst_float label) { - return 1.0f; - } - static bst_float ProbToMargin(bst_float base_score) { return base_score; } - static const char* LabelErrorMsg() { return ""; } - static const char* DefaultEvalMetric() { return "rmse"; } - - static const char* Name() { return "reg:squarederror_oneapi"; } -}; - -// TODO: DPC++ does not fully support std math inside offloaded kernels -struct SquaredLogErrorOneAPI { - static bst_float PredTransform(bst_float x) { return x; } - static bool CheckLabel(bst_float label) { - return label > -1; - } - static bst_float FirstOrderGradient(bst_float predt, bst_float label) { - predt = std::max(predt, (bst_float)(-1 + 1e-6)); // ensure correct value for log1p - return (cl::sycl::log1p(predt) - cl::sycl::log1p(label)) / (predt + 1); - } - static bst_float SecondOrderGradient(bst_float predt, bst_float label) { - predt = std::max(predt, (bst_float)(-1 + 1e-6)); - float res = (-cl::sycl::log1p(predt) + cl::sycl::log1p(label) + 1) / - cl::sycl::pow(predt + 1, (bst_float)2); - res = std::max(res, (bst_float)1e-6f); - return res; - } - static bst_float ProbToMargin(bst_float base_score) { return base_score; } - static const char* LabelErrorMsg() { - return "label must be greater than -1 for rmsle so that log(label + 1) can be valid."; - } - static const char* DefaultEvalMetric() { return "rmsle"; } - - static const char* Name() { return "reg:squaredlogerror_oneapi"; } -}; - -// logistic loss for probability regression task -struct LogisticRegressionOneAPI { - // duplication is necessary, as __device__ specifier - // cannot be made conditional on template parameter - static bst_float PredTransform(bst_float x) { return SigmoidOneAPI(x); } - static bool CheckLabel(bst_float x) { return x >= 0.0f && x <= 1.0f; } - static bst_float FirstOrderGradient(bst_float predt, bst_float label) { - return predt - label; - } - static bst_float SecondOrderGradient(bst_float predt, bst_float label) { - const bst_float eps = 1e-16f; - return std::max(predt * (1.0f - predt), eps); - } - template - static T PredTransform(T x) { return SigmoidOneAPI(x); } - template - static T FirstOrderGradient(T predt, T label) { return predt - label; } - template - static T SecondOrderGradient(T predt, T label) { - const T eps = T(1e-16f); - return std::max(predt * (T(1.0f) - predt), eps); - } - static bst_float ProbToMargin(bst_float base_score) { - CHECK(base_score > 0.0f && base_score < 1.0f) - << "base_score must be in (0,1) for logistic loss, got: " << base_score; - return -logf(1.0f / base_score - 1.0f); - } - static const char* LabelErrorMsg() { - return "label must be in [0,1] for logistic regression"; - } - static const char* DefaultEvalMetric() { return "rmse"; } - - static const char* Name() { return "reg:logistic_oneapi"; } -}; - -// logistic loss for binary classification task -struct LogisticClassificationOneAPI : public LogisticRegressionOneAPI { - static const char* DefaultEvalMetric() { return "logloss"; } - static const char* Name() { return "binary:logistic_oneapi"; } -}; - -// logistic loss, but predict un-transformed margin -struct LogisticRawOneAPI : public LogisticRegressionOneAPI { - // duplication is necessary, as __device__ specifier - // cannot be made conditional on template parameter - static bst_float PredTransform(bst_float x) { return x; } - static bst_float FirstOrderGradient(bst_float predt, bst_float label) { - predt = SigmoidOneAPI(predt); - return predt - label; - } - static bst_float SecondOrderGradient(bst_float predt, bst_float label) { - const bst_float eps = 1e-16f; - predt = SigmoidOneAPI(predt); - return std::max(predt * (1.0f - predt), eps); - } - template - static T PredTransform(T x) { return x; } - template - static T FirstOrderGradient(T predt, T label) { - predt = SigmoidOneAPI(predt); - return predt - label; - } - template - static T SecondOrderGradient(T predt, T label) { - const T eps = T(1e-16f); - predt = SigmoidOneAPI(predt); - return std::max(predt * (T(1.0f) - predt), eps); - } - static const char* DefaultEvalMetric() { return "logloss"; } - - static const char* Name() { return "binary:logitraw_oneapi"; } -}; - -} // namespace obj -} // namespace xgboost - -#endif // XGBOOST_OBJECTIVE_REGRESSION_LOSS_ONEAPI_H_ diff --git a/plugin/updater_oneapi/regression_obj_oneapi.cc b/plugin/updater_oneapi/regression_obj_oneapi.cc deleted file mode 100755 index 3ee5741e7..000000000 --- a/plugin/updater_oneapi/regression_obj_oneapi.cc +++ /dev/null @@ -1,182 +0,0 @@ -#include -#include -#include -#include -#include - -#include "xgboost/host_device_vector.h" -#include "xgboost/json.h" -#include "xgboost/parameter.h" -#include "xgboost/span.h" - -#include "../../src/common/transform.h" -#include "../../src/common/common.h" -#include "./regression_loss_oneapi.h" - -#include "CL/sycl.hpp" - -namespace xgboost { -namespace obj { - -DMLC_REGISTRY_FILE_TAG(regression_obj_oneapi); - -struct RegLossParamOneAPI : public XGBoostParameter { - float scale_pos_weight; - // declare parameters - DMLC_DECLARE_PARAMETER(RegLossParamOneAPI) { - DMLC_DECLARE_FIELD(scale_pos_weight).set_default(1.0f).set_lower_bound(0.0f) - .describe("Scale the weight of positive examples by this factor"); - } -}; - -template -class RegLossObjOneAPI : public ObjFunction { - protected: - HostDeviceVector label_correct_; - - public: - RegLossObjOneAPI() = default; - - void Configure(const std::vector >& args) override { - param_.UpdateAllowUnknown(args); - - cl::sycl::default_selector selector; - qu_ = cl::sycl::queue(selector); - } - - void GetGradient(const HostDeviceVector& preds, - const MetaInfo &info, - int iter, - HostDeviceVector* out_gpair) override { - if (info.labels_.Size() == 0U) { - LOG(WARNING) << "Label set is empty."; - } - CHECK_EQ(preds.Size(), info.labels_.Size()) - << " " << "labels are not correctly provided" - << "preds.size=" << preds.Size() << ", label.size=" << info.labels_.Size() << ", " - << "Loss: " << Loss::Name(); - - size_t const ndata = preds.Size(); - out_gpair->Resize(ndata); - - // TODO: add label_correct check - label_correct_.Resize(1); - label_correct_.Fill(1); - - bool is_null_weight = info.weights_.Size() == 0; - - cl::sycl::buffer preds_buf(preds.HostPointer(), preds.Size()); - cl::sycl::buffer labels_buf(info.labels_.HostPointer(), info.labels_.Size()); - cl::sycl::buffer out_gpair_buf(out_gpair->HostPointer(), out_gpair->Size()); - cl::sycl::buffer weights_buf(is_null_weight ? NULL : info.weights_.HostPointer(), - is_null_weight ? 1 : info.weights_.Size()); - - cl::sycl::buffer additional_input_buf(1); - { - auto additional_input_acc = additional_input_buf.get_access(); - additional_input_acc[0] = 1; // Fill the label_correct flag - } - - auto scale_pos_weight = param_.scale_pos_weight; - if (!is_null_weight) { - CHECK_EQ(info.weights_.Size(), ndata) - << "Number of weights should be equal to number of data points."; - } - - qu_.submit([&](cl::sycl::handler& cgh) { - auto preds_acc = preds_buf.get_access(cgh); - auto labels_acc = labels_buf.get_access(cgh); - auto weights_acc = weights_buf.get_access(cgh); - auto out_gpair_acc = out_gpair_buf.get_access(cgh); - auto additional_input_acc = additional_input_buf.get_access(cgh); - cgh.parallel_for<>(cl::sycl::range<1>(ndata), [=](cl::sycl::id<1> pid) { - int idx = pid[0]; - bst_float p = Loss::PredTransform(preds_acc[idx]); - bst_float w = is_null_weight ? 1.0f : weights_acc[idx]; - bst_float label = labels_acc[idx]; - if (label == 1.0f) { - w *= scale_pos_weight; - } - if (!Loss::CheckLabel(label)) { - // If there is an incorrect label, the host code will know. - additional_input_acc[0] = 0; - } - out_gpair_acc[idx] = GradientPair(Loss::FirstOrderGradient(p, label) * w, - Loss::SecondOrderGradient(p, label) * w); - }); - }).wait(); - - int flag = 1; - { - auto additional_input_acc = additional_input_buf.get_access(); - flag = additional_input_acc[0]; - } - - if (flag == 0) { - LOG(FATAL) << Loss::LabelErrorMsg(); - } - - } - - public: - const char* DefaultEvalMetric() const override { - return Loss::DefaultEvalMetric(); - } - - void PredTransform(HostDeviceVector *io_preds) override { - size_t const ndata = io_preds->Size(); - - cl::sycl::buffer io_preds_buf(io_preds->HostPointer(), io_preds->Size()); - - qu_.submit([&](cl::sycl::handler& cgh) { - auto io_preds_acc = io_preds_buf.get_access(cgh); - cgh.parallel_for<>(cl::sycl::range<1>(ndata), [=](cl::sycl::id<1> pid) { - int idx = pid[0]; - io_preds_acc[idx] = Loss::PredTransform(io_preds_acc[idx]); - }); - }).wait(); - } - - float ProbToMargin(float base_score) const override { - return Loss::ProbToMargin(base_score); - } - - void SaveConfig(Json* p_out) const override { - auto& out = *p_out; - out["name"] = String(Loss::Name()); - out["reg_loss_param"] = ToJson(param_); - } - - void LoadConfig(Json const& in) override { - FromJson(in["reg_loss_param"], ¶m_); - } - - protected: - RegLossParamOneAPI param_; - - cl::sycl::queue qu_; -}; - -// register the objective functions -DMLC_REGISTER_PARAMETER(RegLossParamOneAPI); - -// TODO: Find a better way to dispatch names of DPC++ kernels with various template parameters of loss function -XGBOOST_REGISTER_OBJECTIVE(SquaredLossRegressionOneAPI, LinearSquareLossOneAPI::Name()) -.describe("Regression with squared error with DPC++ backend.") -.set_body([]() { return new RegLossObjOneAPI(); }); -XGBOOST_REGISTER_OBJECTIVE(SquareLogErrorOneAPI, SquaredLogErrorOneAPI::Name()) -.describe("Regression with root mean squared logarithmic error with DPC++ backend.") -.set_body([]() { return new RegLossObjOneAPI(); }); -XGBOOST_REGISTER_OBJECTIVE(LogisticRegressionOneAPI, LogisticRegressionOneAPI::Name()) -.describe("Logistic regression for probability regression task with DPC++ backend.") -.set_body([]() { return new RegLossObjOneAPI(); }); -XGBOOST_REGISTER_OBJECTIVE(LogisticClassificationOneAPI, LogisticClassificationOneAPI::Name()) -.describe("Logistic regression for binary classification task with DPC++ backend.") -.set_body([]() { return new RegLossObjOneAPI(); }); -XGBOOST_REGISTER_OBJECTIVE(LogisticRawOneAPI, LogisticRawOneAPI::Name()) -.describe("Logistic regression for classification, output score " - "before logistic transformation with DPC++ backend.") -.set_body([]() { return new RegLossObjOneAPI(); }); - -} // namespace obj -} // namespace xgboost diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f0dfe061f..161889f9e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,10 @@ if(USE_CUDA) target_sources(objxgboost PRIVATE ${CUDA_SOURCES}) endif() +if(PLUGIN_SYCL) + target_compile_definitions(objxgboost PRIVATE -DXGBOOST_USE_SYCL=1) +endif() + target_include_directories(objxgboost PRIVATE ${xgboost_SOURCE_DIR}/include diff --git a/src/common/common.h b/src/common/common.h index ed6ceceb8..4b20ce7c2 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -169,10 +169,10 @@ inline void AssertNCCLSupport() { #endif // !defined(XGBOOST_USE_NCCL) } -inline void AssertOneAPISupport() { -#ifndef XGBOOST_USE_ONEAPI - LOG(FATAL) << "XGBoost version not compiled with OneAPI support."; -#endif // XGBOOST_USE_ONEAPI +inline void AssertSYCLSupport() { +#ifndef XGBOOST_USE_SYCL + LOG(FATAL) << "XGBoost version not compiled with SYCL support."; +#endif // XGBOOST_USE_SYCL } void SetDevice(std::int32_t device); diff --git a/src/gbm/gbtree.cc b/src/gbm/gbtree.cc index b0327da15..9ff4abb4d 100644 --- a/src/gbm/gbtree.cc +++ b/src/gbm/gbtree.cc @@ -113,13 +113,13 @@ void GBTree::Configure(Args const& cfg) { } #endif // defined(XGBOOST_USE_CUDA) -#if defined(XGBOOST_USE_ONEAPI) - if (!oneapi_predictor_) { - oneapi_predictor_ = - std::unique_ptr(Predictor::Create("oneapi_predictor", this->ctx_)); +#if defined(XGBOOST_USE_SYCL) + if (!sycl_predictor_) { + sycl_predictor_ = + std::unique_ptr(Predictor::Create("sycl_predictor", this->ctx_)); } - oneapi_predictor_->Configure(cfg); -#endif // defined(XGBOOST_USE_ONEAPI) + sycl_predictor_->Configure(cfg); +#endif // defined(XGBOOST_USE_SYCL) // `updater` parameter was manually specified specified_updater_ = @@ -553,6 +553,11 @@ void GBTree::InplacePredict(std::shared_ptr p_m, float missing, }, [&, begin = tree_begin, end = tree_end] { return this->gpu_predictor_->InplacePredict(p_m, model_, missing, out_preds, begin, end); +#if defined(XGBOOST_USE_SYCL) + }, + [&, begin = tree_begin, end = tree_end] { + return this->sycl_predictor_->InplacePredict(p_m, model_, missing, out_preds, begin, end); +#endif // defined(XGBOOST_USE_SYCL) }); if (!known_type) { auto proxy = std::dynamic_pointer_cast(p_m); @@ -568,10 +573,16 @@ void GBTree::InplacePredict(std::shared_ptr p_m, float missing, if (f_dmat && !f_dmat->SingleColBlock()) { if (ctx_->IsCPU()) { return cpu_predictor_; - } else { + } else if (ctx_->IsCUDA()) { common::AssertGPUSupport(); CHECK(gpu_predictor_); return gpu_predictor_; + } else { +#if defined(XGBOOST_USE_SYCL) + common::AssertSYCLSupport(); + CHECK(sycl_predictor_); + return sycl_predictor_; +#endif // defined(XGBOOST_USE_SYCL) } } @@ -606,10 +617,16 @@ void GBTree::InplacePredict(std::shared_ptr p_m, float missing, if (ctx_->IsCPU()) { return cpu_predictor_; - } else { + } else if (ctx_->IsCUDA()) { common::AssertGPUSupport(); CHECK(gpu_predictor_); return gpu_predictor_; + } else { +#if defined(XGBOOST_USE_SYCL) + common::AssertSYCLSupport(); + CHECK(sycl_predictor_); + return sycl_predictor_; +#endif // defined(XGBOOST_USE_SYCL) } return cpu_predictor_; @@ -814,6 +831,11 @@ class Dart : public GBTree { }, [&] { return gpu_predictor_->InplacePredict(p_fmat, model_, missing, &predts, i, i + 1); +#if defined(XGBOOST_USE_SYCL) + }, + [&] { + return sycl_predictor_->InplacePredict(p_fmat, model_, missing, &predts, i, i + 1); +#endif // defined(XGBOOST_USE_SYCL) }); CHECK(success) << msg; }; @@ -830,6 +852,12 @@ class Dart : public GBTree { [&] { this->gpu_predictor_->InitOutPredictions(p_fmat->Info(), &p_out_preds->predictions, model_); +#if defined(XGBOOST_USE_SYCL) + }, + [&] { + this->sycl_predictor_->InitOutPredictions(p_fmat->Info(), &p_out_preds->predictions, + model_); +#endif // defined(XGBOOST_USE_SYCL) }); } // Multiple the tree weight diff --git a/src/gbm/gbtree.h b/src/gbm/gbtree.h index 827d85217..a2d84d848 100644 --- a/src/gbm/gbtree.h +++ b/src/gbm/gbtree.h @@ -349,9 +349,9 @@ class GBTree : public GradientBooster { // Predictors std::unique_ptr cpu_predictor_; std::unique_ptr gpu_predictor_{nullptr}; -#if defined(XGBOOST_USE_ONEAPI) - std::unique_ptr oneapi_predictor_; -#endif // defined(XGBOOST_USE_ONEAPI) +#if defined(XGBOOST_USE_SYCL) + std::unique_ptr sycl_predictor_; +#endif // defined(XGBOOST_USE_SYCL) common::Monitor monitor_; }; diff --git a/tests/ci_build/conda_env/linux_sycl_test.yml b/tests/ci_build/conda_env/linux_sycl_test.yml new file mode 100644 index 000000000..bb14c1e77 --- /dev/null +++ b/tests/ci_build/conda_env/linux_sycl_test.yml @@ -0,0 +1,20 @@ +name: linux_sycl_test +channels: +- conda-forge +- intel +dependencies: +- python=3.8 +- cmake +- c-compiler +- cxx-compiler +- pip +- wheel +- numpy +- scipy +- scikit-learn +- pandas +- hypothesis>=6.46 +- pytest +- pytest-timeout +- pytest-cov +- dpcpp_linux-64 diff --git a/tests/ci_build/lint_cpp.py b/tests/ci_build/lint_cpp.py index 6ec2b4e7f..d4775d6b6 100644 --- a/tests/ci_build/lint_cpp.py +++ b/tests/ci_build/lint_cpp.py @@ -138,7 +138,7 @@ def main(): "path", nargs="*", help="Path to traverse", - default=["src", "include", os.path.join("R-package", "src"), "python-package"], + default=["src", "include", os.path.join("R-package", "src"), "python-package", "plugin/sycl"], ) parser.add_argument( "--exclude_path", diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index e0d16efd4..fdd643da0 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -33,6 +33,7 @@ class LintersPaths: "tests/python-gpu/test_gpu_pickling.py", "tests/python-gpu/test_gpu_eval_metrics.py", "tests/python-gpu/test_gpu_with_sklearn.py", + "tests/python-sycl/test_sycl_prediction.py", "tests/test_distributed/test_with_spark/", "tests/test_distributed/test_gpu_with_spark/", # demo diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index ab82b6494..08862feee 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -13,9 +13,9 @@ if(USE_CUDA) list(APPEND TEST_SOURCES ${CUDA_TEST_SOURCES}) endif() -file(GLOB_RECURSE ONEAPI_TEST_SOURCES "plugin/*_oneapi.cc") -if(NOT PLUGIN_UPDATER_ONEAPI) - list(REMOVE_ITEM TEST_SOURCES ${ONEAPI_TEST_SOURCES}) +file(GLOB_RECURSE SYCL_TEST_SOURCES "plugin/test_sycl_*.cc") +if(NOT PLUGIN_SYCL) + list(REMOVE_ITEM TEST_SOURCES ${SYCL_TEST_SOURCES}) endif() if(PLUGIN_FEDERATED) diff --git a/tests/cpp/plugin/test_predictor_oneapi.cc b/tests/cpp/plugin/test_predictor_oneapi.cc deleted file mode 100755 index 52edd4a12..000000000 --- a/tests/cpp/plugin/test_predictor_oneapi.cc +++ /dev/null @@ -1,168 +0,0 @@ -/*! - * Copyright 2017-2020 XGBoost contributors - */ -#include -#include - -#include "../../../src/data/adapter.h" -#include "../../../src/gbm/gbtree_model.h" -#include "../filesystem.h" // dmlc::TemporaryDirectory -#include "../helpers.h" -#include "../predictor/test_predictor.h" - -namespace xgboost { -TEST(Plugin, OneAPIPredictorBasic) { - auto lparam = MakeCUDACtx(0); - std::unique_ptr oneapi_predictor = - std::unique_ptr(Predictor::Create("oneapi_predictor", &lparam)); - - int kRows = 5; - int kCols = 5; - - LearnerModelParam param; - param.num_feature = kCols; - param.base_score = 0.0; - param.num_output_group = 1; - - gbm::GBTreeModel model = CreateTestModel(¶m); - - auto dmat = RandomDataGenerator(kRows, kCols, 0).GenerateDMatrix(); - - // Test predict batch - PredictionCacheEntry out_predictions; - oneapi_predictor->PredictBatch(dmat.get(), &out_predictions, model, 0); - ASSERT_EQ(model.trees.size(), out_predictions.version); - std::vector& out_predictions_h = out_predictions.predictions.HostVector(); - for (size_t i = 0; i < out_predictions.predictions.Size(); i++) { - ASSERT_EQ(out_predictions_h[i], 1.5); - } - - // Test predict instance - auto const &batch = *dmat->GetBatches().begin(); - for (size_t i = 0; i < batch.Size(); i++) { - std::vector instance_out_predictions; - oneapi_predictor->PredictInstance(batch[i], &instance_out_predictions, model); - ASSERT_EQ(instance_out_predictions[0], 1.5); - } - - // Test predict leaf - std::vector leaf_out_predictions; - oneapi_predictor->PredictLeaf(dmat.get(), &leaf_out_predictions, model); - for (auto v : leaf_out_predictions) { - ASSERT_EQ(v, 0); - } - - // Test predict contribution - std::vector out_contribution; - oneapi_predictor->PredictContribution(dmat.get(), &out_contribution, model); - ASSERT_EQ(out_contribution.size(), kRows * (kCols + 1)); - for (size_t i = 0; i < out_contribution.size(); ++i) { - auto const& contri = out_contribution[i]; - // shift 1 for bias, as test tree is a decision dump, only global bias is filled with LeafValue(). - if ((i+1) % (kCols+1) == 0) { - ASSERT_EQ(out_contribution.back(), 1.5f); - } else { - ASSERT_EQ(contri, 0); - } - } - // Test predict contribution (approximate method) - oneapi_predictor->PredictContribution(dmat.get(), &out_contribution, model, 0, nullptr, true); - for (size_t i = 0; i < out_contribution.size(); ++i) { - auto const& contri = out_contribution[i]; - // shift 1 for bias, as test tree is a decision dump, only global bias is filled with LeafValue(). - if ((i+1) % (kCols+1) == 0) { - ASSERT_EQ(out_contribution.back(), 1.5f); - } else { - ASSERT_EQ(contri, 0); - } - } -} - -TEST(Plugin, OneAPIPredictorExternalMemory) { - dmlc::TemporaryDirectory tmpdir; - std::string filename = tmpdir.path + "/big.libsvm"; - std::unique_ptr dmat = CreateSparsePageDMatrix(12, 64, filename); - auto lparam = MakeCUDACtx(0); - - std::unique_ptr oneapi_predictor = - std::unique_ptr(Predictor::Create("oneapi_predictor", &lparam)); - - LearnerModelParam param; - param.base_score = 0; - param.num_feature = dmat->Info().num_col_; - param.num_output_group = 1; - - gbm::GBTreeModel model = CreateTestModel(¶m); - - // Test predict batch - PredictionCacheEntry out_predictions; - oneapi_predictor->PredictBatch(dmat.get(), &out_predictions, model, 0); - std::vector &out_predictions_h = out_predictions.predictions.HostVector(); - ASSERT_EQ(out_predictions.predictions.Size(), dmat->Info().num_row_); - for (const auto& v : out_predictions_h) { - ASSERT_EQ(v, 1.5); - } - - // Test predict leaf - std::vector leaf_out_predictions; - oneapi_predictor->PredictLeaf(dmat.get(), &leaf_out_predictions, model); - ASSERT_EQ(leaf_out_predictions.size(), dmat->Info().num_row_); - for (const auto& v : leaf_out_predictions) { - ASSERT_EQ(v, 0); - } - - // Test predict contribution - std::vector out_contribution; - oneapi_predictor->PredictContribution(dmat.get(), &out_contribution, model); - ASSERT_EQ(out_contribution.size(), dmat->Info().num_row_ * (dmat->Info().num_col_ + 1)); - for (size_t i = 0; i < out_contribution.size(); ++i) { - auto const& contri = out_contribution[i]; - // shift 1 for bias, as test tree is a decision dump, only global bias is filled with LeafValue(). - if ((i + 1) % (dmat->Info().num_col_ + 1) == 0) { - ASSERT_EQ(out_contribution.back(), 1.5f); - } else { - ASSERT_EQ(contri, 0); - } - } - - // Test predict contribution (approximate method) - std::vector out_contribution_approximate; - oneapi_predictor->PredictContribution(dmat.get(), &out_contribution_approximate, model, 0, nullptr, true); - ASSERT_EQ(out_contribution_approximate.size(), - dmat->Info().num_row_ * (dmat->Info().num_col_ + 1)); - for (size_t i = 0; i < out_contribution.size(); ++i) { - auto const& contri = out_contribution[i]; - // shift 1 for bias, as test tree is a decision dump, only global bias is filled with LeafValue(). - if ((i + 1) % (dmat->Info().num_col_ + 1) == 0) { - ASSERT_EQ(out_contribution.back(), 1.5f); - } else { - ASSERT_EQ(contri, 0); - } - } -} - -TEST(Plugin, OneAPIPredictorInplacePredict) { - bst_row_t constexpr kRows{128}; - bst_feature_t constexpr kCols{64}; - auto gen = RandomDataGenerator{kRows, kCols, 0.5}.Device(-1); - { - HostDeviceVector data; - gen.GenerateDense(&data); - ASSERT_EQ(data.Size(), kRows * kCols); - std::shared_ptr x{ - new data::DenseAdapter(data.HostPointer(), kRows, kCols)}; - TestInplacePrediction(x, "oneapi_predictor", kRows, kCols, -1); - } - - { - HostDeviceVector data; - HostDeviceVector rptrs; - HostDeviceVector columns; - gen.GenerateCSR(&data, &rptrs, &columns); - std::shared_ptr x{new data::CSRAdapter( - rptrs.HostPointer(), columns.HostPointer(), data.HostPointer(), kRows, - data.Size(), kCols)}; - TestInplacePrediction(x, "oneapi_predictor", kRows, kCols, -1); - } -} -} // namespace xgboost diff --git a/tests/cpp/plugin/test_regression_obj_oneapi.cc b/tests/cpp/plugin/test_regression_obj_oneapi.cc deleted file mode 100755 index c01d9d951..000000000 --- a/tests/cpp/plugin/test_regression_obj_oneapi.cc +++ /dev/null @@ -1,176 +0,0 @@ -/*! - * Copyright 2017-2019 XGBoost contributors - */ -#include -#include -#include -#include -#include "../helpers.h" -namespace xgboost { - -TEST(Plugin, LinearRegressionGPairOneAPI) { - Context tparam = MakeCUDACtx(0); - std::vector> args; - - std::unique_ptr obj { - ObjFunction::Create("reg:squarederror_oneapi", &tparam) - }; - - obj->Configure(args); - CheckObjFunction(obj, - {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - {0, 0, 0, 0, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1}, - {0, 0.1f, 0.9f, 1.0f, -1.0f, -0.9f, -0.1f, 0}, - {1, 1, 1, 1, 1, 1, 1, 1}); - CheckObjFunction(obj, - {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - {0, 0, 0, 0, 1, 1, 1, 1}, - {}, // empty weight - {0, 0.1f, 0.9f, 1.0f, -1.0f, -0.9f, -0.1f, 0}, - {1, 1, 1, 1, 1, 1, 1, 1}); - ASSERT_NO_THROW(obj->DefaultEvalMetric()); -} - -TEST(Plugin, SquaredLogOneAPI) { - Context tparam = MakeCUDACtx(0); - std::vector> args; - - std::unique_ptr obj { ObjFunction::Create("reg:squaredlogerror_oneapi", &tparam) }; - obj->Configure(args); - CheckConfigReload(obj, "reg:squaredlogerror_oneapi"); - - CheckObjFunction(obj, - {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights - {-0.5435f, -0.4257f, -0.25475f, -0.05855f, 0.1009f}, - { 1.3205f, 1.0492f, 0.69215f, 0.34115f, 0.1091f}); - CheckObjFunction(obj, - {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {}, // empty weights - {-0.5435f, -0.4257f, -0.25475f, -0.05855f, 0.1009f}, - { 1.3205f, 1.0492f, 0.69215f, 0.34115f, 0.1091f}); - ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"rmsle"}); -} - -TEST(Plugin, LogisticRegressionGPairOneAPI) { - Context tparam = MakeCUDACtx(0); - std::vector> args; - std::unique_ptr obj { ObjFunction::Create("reg:logistic_oneapi", &tparam) }; - - obj->Configure(args); - CheckConfigReload(obj, "reg:logistic_oneapi"); - - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, // preds - { 0, 0, 0, 0, 1, 1, 1, 1}, // labels - { 1, 1, 1, 1, 1, 1, 1, 1}, // weights - { 0.5f, 0.52f, 0.71f, 0.73f, -0.5f, -0.47f, -0.28f, -0.26f}, // out_grad - {0.25f, 0.24f, 0.20f, 0.19f, 0.25f, 0.24f, 0.20f, 0.19f}); // out_hess -} - -TEST(Plugin, LogisticRegressionBasicOneAPI) { - Context lparam = MakeCUDACtx(0); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("reg:logistic_oneapi", &lparam) - }; - - obj->Configure(args); - CheckConfigReload(obj, "reg:logistic_oneapi"); - - // test label validation - EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {10}, {1}, {0}, {0})) - << "Expected error when label not in range [0,1f] for LogisticRegression"; - - // test ProbToMargin - EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.197f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.5f), 0, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.9f), 2.197f, 0.01f); - EXPECT_ANY_THROW(obj->ProbToMargin(10)) - << "Expected error when base_score not in range [0,1f] for LogisticRegression"; - - // test PredTransform - HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; - std::vector out_preds = {0.5f, 0.524f, 0.622f, 0.710f, 0.731f}; - obj->PredTransform(&io_preds); - auto& preds = io_preds.HostVector(); - for (int i = 0; i < static_cast(io_preds.Size()); ++i) { - EXPECT_NEAR(preds[i], out_preds[i], 0.01f); - } -} - -TEST(Plugin, LogisticRawGPairOneAPI) { - Context lparam = MakeCUDACtx(0); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("binary:logitraw_oneapi", &lparam) - }; - - obj->Configure(args); - - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - { 1, 1, 1, 1, 1, 1, 1, 1}, - { 0.5f, 0.52f, 0.71f, 0.73f, -0.5f, -0.47f, -0.28f, -0.26f}, - {0.25f, 0.24f, 0.20f, 0.19f, 0.25f, 0.24f, 0.20f, 0.19f}); -} - -TEST(Plugin, CPUvsOneAPI) { - Context ctx = MakeCUDACtx(0); - - ObjFunction * obj_cpu = - ObjFunction::Create("reg:squarederror", &ctx); - ObjFunction * obj_oneapi = - ObjFunction::Create("reg:squarederror_oneapi", &ctx); - HostDeviceVector cpu_out_preds; - HostDeviceVector oneapi_out_preds; - - constexpr size_t kRows = 400; - constexpr size_t kCols = 100; - auto pdmat = RandomDataGenerator(kRows, kCols, 0).Seed(0).GenerateDMatrix(); - HostDeviceVector preds; - preds.Resize(kRows); - auto& h_preds = preds.HostVector(); - for (size_t i = 0; i < h_preds.size(); ++i) { - h_preds[i] = static_cast(i); - } - auto& info = pdmat->Info(); - - info.labels.Reshape(kRows, 1); - auto& h_labels = info.labels.Data()->HostVector(); - for (size_t i = 0; i < h_labels.size(); ++i) { - h_labels[i] = 1 / static_cast(i+1); - } - - { - // CPU - ctx = ctx.MakeCPU(); - obj_cpu->GetGradient(preds, info, 0, &cpu_out_preds); - } - { - // oneapi - ctx.gpu_id = 0; - obj_oneapi->GetGradient(preds, info, 0, &oneapi_out_preds); - } - - auto& h_cpu_out = cpu_out_preds.HostVector(); - auto& h_oneapi_out = oneapi_out_preds.HostVector(); - - float sgrad = 0; - float shess = 0; - for (size_t i = 0; i < kRows; ++i) { - sgrad += std::pow(h_cpu_out[i].GetGrad() - h_oneapi_out[i].GetGrad(), 2); - shess += std::pow(h_cpu_out[i].GetHess() - h_oneapi_out[i].GetHess(), 2); - } - ASSERT_NEAR(sgrad, 0.0f, kRtEps); - ASSERT_NEAR(shess, 0.0f, kRtEps); - - delete obj_cpu; - delete obj_oneapi; -} - -} // namespace xgboost diff --git a/tests/cpp/plugin/test_sycl_predictor.cc b/tests/cpp/plugin/test_sycl_predictor.cc new file mode 100755 index 000000000..f82a9f33d --- /dev/null +++ b/tests/cpp/plugin/test_sycl_predictor.cc @@ -0,0 +1,101 @@ +/*! + * Copyright 2017-2023 XGBoost contributors + */ +#include +#include + +#include "../../../src/data/adapter.h" +#include "../../../src/data/proxy_dmatrix.h" +#include "../../../src/gbm/gbtree.h" +#include "../../../src/gbm/gbtree_model.h" +#include "../filesystem.h" // dmlc::TemporaryDirectory +#include "../helpers.h" +#include "../predictor/test_predictor.h" + +namespace xgboost { + +TEST(SyclPredictor, Basic) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + + size_t constexpr kRows = 5; + size_t constexpr kCols = 5; + auto dmat = RandomDataGenerator(kRows, kCols, 0).GenerateDMatrix(); + TestBasic(dmat.get(), &ctx); +} + +TEST(SyclPredictor, ExternalMemory) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + + size_t constexpr kPageSize = 64, kEntriesPerCol = 3; + size_t constexpr kEntries = kPageSize * kEntriesPerCol * 2; + std::unique_ptr dmat = CreateSparsePageDMatrix(kEntries); + TestBasic(dmat.get(), &ctx); +} + +TEST(SyclPredictor, InplacePredict) { + bst_row_t constexpr kRows{128}; + bst_feature_t constexpr kCols{64}; + Context ctx; + auto gen = RandomDataGenerator{kRows, kCols, 0.5}.Device(ctx.Device()); + { + HostDeviceVector data; + gen.GenerateDense(&data); + ASSERT_EQ(data.Size(), kRows * kCols); + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + std::shared_ptr x{new data::DMatrixProxy{}}; + auto array_interface = GetArrayInterface(&data, kRows, kCols); + std::string arr_str; + Json::Dump(array_interface, &arr_str); + x->SetArrayData(arr_str.data()); + TestInplacePrediction(&ctx, x, kRows, kCols); + } +} + +TEST(SyclPredictor, IterationRange) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestIterationRange(&ctx); +} + +TEST(SyclPredictor, GHistIndexTraining) { + size_t constexpr kRows{128}, kCols{16}, kBins{64}; + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + auto p_hist = RandomDataGenerator{kRows, kCols, 0.0}.Bins(kBins).GenerateDMatrix(false); + HostDeviceVector storage(kRows * kCols); + auto columnar = RandomDataGenerator{kRows, kCols, 0.0}.GenerateArrayInterface(&storage); + auto adapter = data::ArrayAdapter(columnar.c_str()); + std::shared_ptr p_full{ + DMatrix::Create(&adapter, std::numeric_limits::quiet_NaN(), 1)}; + TestTrainingPrediction(&ctx, kRows, kBins, p_full, p_hist); +} + +TEST(SyclPredictor, CategoricalPredictLeaf) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestCategoricalPredictLeaf(&ctx, false); +} + +TEST(SyclPredictor, LesserFeatures) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestPredictionWithLesserFeatures(&ctx); +} + +TEST(SyclPredictor, Sparse) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSparsePrediction(&ctx, 0.2); + TestSparsePrediction(&ctx, 0.8); +} + +TEST(SyclPredictor, Multi) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestVectorLeafPrediction(&ctx); +} + +} // namespace xgboost \ No newline at end of file diff --git a/tests/cpp/predictor/test_cpu_predictor.cc b/tests/cpp/predictor/test_cpu_predictor.cc index 07f33d72e..8f3955c05 100644 --- a/tests/cpp/predictor/test_cpu_predictor.cc +++ b/tests/cpp/predictor/test_cpu_predictor.cc @@ -18,92 +18,17 @@ namespace xgboost { -namespace { -void TestBasic(DMatrix* dmat) { - Context ctx; - std::unique_ptr cpu_predictor = - std::unique_ptr(Predictor::Create("cpu_predictor", &ctx)); - - size_t const kRows = dmat->Info().num_row_; - size_t const kCols = dmat->Info().num_col_; - - LearnerModelParam mparam{MakeMP(kCols, .0, 1)}; - - ctx.UpdateAllowUnknown(Args{}); - gbm::GBTreeModel model = CreateTestModel(&mparam, &ctx); - - // Test predict batch - PredictionCacheEntry out_predictions; - cpu_predictor->InitOutPredictions(dmat->Info(), &out_predictions.predictions, model); - cpu_predictor->PredictBatch(dmat, &out_predictions, model, 0); - - std::vector& out_predictions_h = out_predictions.predictions.HostVector(); - for (size_t i = 0; i < out_predictions.predictions.Size(); i++) { - ASSERT_EQ(out_predictions_h[i], 1.5); - } - - // Test predict instance - auto const& batch = *dmat->GetBatches().begin(); - auto page = batch.GetView(); - for (size_t i = 0; i < batch.Size(); i++) { - std::vector instance_out_predictions; - cpu_predictor->PredictInstance(page[i], &instance_out_predictions, model, 0, - dmat->Info().IsColumnSplit()); - ASSERT_EQ(instance_out_predictions[0], 1.5); - } - - // Test predict leaf - HostDeviceVector leaf_out_predictions; - cpu_predictor->PredictLeaf(dmat, &leaf_out_predictions, model); - auto const& h_leaf_out_predictions = leaf_out_predictions.ConstHostVector(); - for (auto v : h_leaf_out_predictions) { - ASSERT_EQ(v, 0); - } - - if (dmat->Info().IsColumnSplit()) { - // Predict contribution is not supported for column split. - return; - } - - // Test predict contribution - HostDeviceVector out_contribution_hdv; - auto& out_contribution = out_contribution_hdv.HostVector(); - cpu_predictor->PredictContribution(dmat, &out_contribution_hdv, model); - ASSERT_EQ(out_contribution.size(), kRows * (kCols + 1)); - for (size_t i = 0; i < out_contribution.size(); ++i) { - auto const& contri = out_contribution[i]; - // shift 1 for bias, as test tree is a decision dump, only global bias is - // filled with LeafValue(). - if ((i + 1) % (kCols + 1) == 0) { - ASSERT_EQ(out_contribution.back(), 1.5f); - } else { - ASSERT_EQ(contri, 0); - } - } - // Test predict contribution (approximate method) - cpu_predictor->PredictContribution(dmat, &out_contribution_hdv, model, 0, nullptr, true); - for (size_t i = 0; i < out_contribution.size(); ++i) { - auto const& contri = out_contribution[i]; - // shift 1 for bias, as test tree is a decision dump, only global bias is - // filled with LeafValue(). - if ((i + 1) % (kCols + 1) == 0) { - ASSERT_EQ(out_contribution.back(), 1.5f); - } else { - ASSERT_EQ(contri, 0); - } - } -} -} // anonymous namespace - TEST(CpuPredictor, Basic) { + Context ctx; size_t constexpr kRows = 5; size_t constexpr kCols = 5; auto dmat = RandomDataGenerator(kRows, kCols, 0).GenerateDMatrix(); - TestBasic(dmat.get()); + TestBasic(dmat.get(), &ctx); } namespace { void TestColumnSplit() { + Context ctx; size_t constexpr kRows = 5; size_t constexpr kCols = 5; auto dmat = RandomDataGenerator(kRows, kCols, 0).GenerateDMatrix(); @@ -112,7 +37,7 @@ void TestColumnSplit() { auto const rank = collective::GetRank(); dmat = std::unique_ptr{dmat->SliceCol(world_size, rank)}; - TestBasic(dmat.get()); + TestBasic(dmat.get(), &ctx); } } // anonymous namespace @@ -132,10 +57,11 @@ TEST(CpuPredictor, IterationRangeColmnSplit) { } TEST(CpuPredictor, ExternalMemory) { + Context ctx; size_t constexpr kPageSize = 64, kEntriesPerCol = 3; size_t constexpr kEntries = kPageSize * kEntriesPerCol * 2; std::unique_ptr dmat = CreateSparsePageDMatrix(kEntries); - TestBasic(dmat.get()); + TestBasic(dmat.get(), &ctx); } TEST(CpuPredictor, InplacePredict) { @@ -235,12 +161,14 @@ TEST(CPUPredictor, CategoricalPredictionColumnSplit) { } TEST(CPUPredictor, CategoricalPredictLeaf) { - TestCategoricalPredictLeaf(false, false); + Context ctx; + TestCategoricalPredictLeaf(&ctx, false); } TEST(CPUPredictor, CategoricalPredictLeafColumnSplit) { auto constexpr kWorldSize = 2; - RunWithInMemoryCommunicator(kWorldSize, TestCategoricalPredictLeaf, false, true); + Context ctx; + RunWithInMemoryCommunicator(kWorldSize, TestCategoricalPredictLeaf, &ctx, true); } TEST(CpuPredictor, UpdatePredictionCache) { diff --git a/tests/cpp/predictor/test_gpu_predictor.cu b/tests/cpp/predictor/test_gpu_predictor.cu index 883e6e01c..50e036b90 100644 --- a/tests/cpp/predictor/test_gpu_predictor.cu +++ b/tests/cpp/predictor/test_gpu_predictor.cu @@ -289,11 +289,13 @@ TEST_F(MGPUPredictorTest, CategoricalPredictionColumnSplit) { } TEST(GPUPredictor, CategoricalPredictLeaf) { - TestCategoricalPredictLeaf(true, false); + auto ctx = MakeCUDACtx(common::AllVisibleGPUs() == 1 ? 0 : collective::GetRank()); + TestCategoricalPredictLeaf(&ctx, false); } TEST_F(MGPUPredictorTest, CategoricalPredictionLeafColumnSplit) { - RunWithInMemoryCommunicator(world_size_, TestCategoricalPredictLeaf, true, true); + auto ctx = MakeCUDACtx(common::AllVisibleGPUs() == 1 ? 0 : collective::GetRank()); + RunWithInMemoryCommunicator(world_size_, TestCategoricalPredictLeaf, &ctx, true); } TEST(GPUPredictor, PredictLeafBasic) { diff --git a/tests/cpp/predictor/test_predictor.cc b/tests/cpp/predictor/test_predictor.cc index 21aa483e4..6ee34ae69 100644 --- a/tests/cpp/predictor/test_predictor.cc +++ b/tests/cpp/predictor/test_predictor.cc @@ -26,6 +26,79 @@ #include "xgboost/tree_model.h" // for RegTree namespace xgboost { + +void TestBasic(DMatrix* dmat, Context const *ctx) { + auto predictor = std::unique_ptr(CreatePredictorForTest(ctx)); + + size_t const kRows = dmat->Info().num_row_; + size_t const kCols = dmat->Info().num_col_; + + LearnerModelParam mparam{MakeMP(kCols, .0, 1)}; + + gbm::GBTreeModel model = CreateTestModel(&mparam, ctx); + + // Test predict batch + PredictionCacheEntry out_predictions; + predictor->InitOutPredictions(dmat->Info(), &out_predictions.predictions, model); + predictor->PredictBatch(dmat, &out_predictions, model, 0); + + std::vector& out_predictions_h = out_predictions.predictions.HostVector(); + for (size_t i = 0; i < out_predictions.predictions.Size(); i++) { + ASSERT_EQ(out_predictions_h[i], 1.5); + } + + // Test predict instance + auto const& batch = *dmat->GetBatches().begin(); + auto page = batch.GetView(); + for (size_t i = 0; i < batch.Size(); i++) { + std::vector instance_out_predictions; + predictor->PredictInstance(page[i], &instance_out_predictions, model, 0, + dmat->Info().IsColumnSplit()); + ASSERT_EQ(instance_out_predictions[0], 1.5); + } + + // Test predict leaf + HostDeviceVector leaf_out_predictions; + predictor->PredictLeaf(dmat, &leaf_out_predictions, model); + auto const& h_leaf_out_predictions = leaf_out_predictions.ConstHostVector(); + for (auto v : h_leaf_out_predictions) { + ASSERT_EQ(v, 0); + } + + if (dmat->Info().IsColumnSplit()) { + // Predict contribution is not supported for column split. + return; + } + + // Test predict contribution + HostDeviceVector out_contribution_hdv; + auto& out_contribution = out_contribution_hdv.HostVector(); + predictor->PredictContribution(dmat, &out_contribution_hdv, model); + ASSERT_EQ(out_contribution.size(), kRows * (kCols + 1)); + for (size_t i = 0; i < out_contribution.size(); ++i) { + auto const& contri = out_contribution[i]; + // shift 1 for bias, as test tree is a decision dump, only global bias is + // filled with LeafValue(). + if ((i + 1) % (kCols + 1) == 0) { + ASSERT_EQ(out_contribution.back(), 1.5f); + } else { + ASSERT_EQ(contri, 0); + } + } + // Test predict contribution (approximate method) + predictor->PredictContribution(dmat, &out_contribution_hdv, model, 0, nullptr, true); + for (size_t i = 0; i < out_contribution.size(); ++i) { + auto const& contri = out_contribution[i]; + // shift 1 for bias, as test tree is a decision dump, only global bias is + // filled with LeafValue(). + if ((i + 1) % (kCols + 1) == 0) { + ASSERT_EQ(out_contribution.back(), 1.5f); + } else { + ASSERT_EQ(contri, 0); + } + } +} + TEST(Predictor, PredictionCache) { size_t constexpr kRows = 16, kCols = 4; @@ -64,7 +137,7 @@ void TestTrainingPrediction(Context const *ctx, size_t rows, size_t bins, {"num_feature", std::to_string(kCols)}, {"num_class", std::to_string(kClasses)}, {"max_bin", std::to_string(bins)}, - {"device", ctx->DeviceName()}}); + {"device", ctx->IsSycl() ? "cpu" : ctx->DeviceName()}}); learner->Configure(); for (size_t i = 0; i < kIters; ++i) { @@ -151,7 +224,7 @@ std::unique_ptr LearnerForTest(Context const *ctx, std::shared_ptr learner{Learner::Create({dmat})}; learner->SetParams( - Args{{"num_parallel_tree", std::to_string(forest)}, {"device", ctx->DeviceName()}}); + Args{{"num_parallel_tree", std::to_string(forest)}, {"device", ctx->IsSycl() ? "cpu" : ctx->DeviceName()}}); for (size_t i = 0; i < iters; ++i) { learner->UpdateOneIter(i, dmat); } @@ -305,11 +378,7 @@ void TestCategoricalPrediction(bool use_gpu, bool is_column_split) { ASSERT_EQ(out_predictions.predictions.HostVector()[0], left_weight + score); } -void TestCategoricalPredictLeaf(bool use_gpu, bool is_column_split) { - Context ctx; - if (use_gpu) { - ctx = MakeCUDACtx(common::AllVisibleGPUs() == 1 ? 0 : collective::GetRank()); - } +void TestCategoricalPredictLeaf(Context const *ctx, bool is_column_split) { size_t constexpr kCols = 10; PredictionCacheEntry out_predictions; @@ -320,10 +389,10 @@ void TestCategoricalPredictLeaf(bool use_gpu, bool is_column_split) { float left_weight = 1.3f; float right_weight = 1.7f; - gbm::GBTreeModel model(&mparam, &ctx); + gbm::GBTreeModel model(&mparam, ctx); GBTreeModelForTest(&model, split_ind, split_cat, left_weight, right_weight); - std::unique_ptr predictor{CreatePredictorForTest(&ctx)}; + std::unique_ptr predictor{CreatePredictorForTest(ctx)}; std::vector row(kCols); row[split_ind] = split_cat; @@ -363,7 +432,6 @@ void TestIterationRange(Context const* ctx) { HostDeviceVector out_predt_sliced; HostDeviceVector out_predt_ranged; - // margin { sliced->Predict(dmat, true, &out_predt_sliced, 0, 0, false, false, false, false, false); learner->Predict(dmat, true, &out_predt_ranged, 0, lend, false, false, false, false, false); @@ -519,6 +587,8 @@ void TestSparsePrediction(Context const *ctx, float sparsity) { learner.reset(Learner::Create({Xy})); learner->LoadModel(model); + learner->SetParam("device", ctx->DeviceName()); + learner->Configure(); if (ctx->IsCUDA()) { learner->SetParam("tree_method", "gpu_hist"); diff --git a/tests/cpp/predictor/test_predictor.h b/tests/cpp/predictor/test_predictor.h index 9e0891d56..c2b28883a 100644 --- a/tests/cpp/predictor/test_predictor.h +++ b/tests/cpp/predictor/test_predictor.h @@ -34,6 +34,8 @@ inline gbm::GBTreeModel CreateTestModel(LearnerModelParam const* param, Context inline auto CreatePredictorForTest(Context const* ctx) { if (ctx->IsCPU()) { return Predictor::Create("cpu_predictor", ctx); + } else if (ctx->IsSycl()) { + return Predictor::Create("sycl_predictor", ctx); } else { return Predictor::Create("gpu_predictor", ctx); } @@ -83,6 +85,8 @@ void TestPredictionFromGradientIndex(Context const* ctx, size_t rows, size_t col } } +void TestBasic(DMatrix* dmat, Context const * ctx); + // p_full and p_hist should come from the same data set. void TestTrainingPrediction(Context const* ctx, size_t rows, size_t bins, std::shared_ptr p_full, std::shared_ptr p_hist); @@ -98,7 +102,7 @@ void TestCategoricalPrediction(bool use_gpu, bool is_column_split); void TestPredictionWithLesserFeaturesColumnSplit(bool use_gpu); -void TestCategoricalPredictLeaf(bool use_gpu, bool is_column_split); +void TestCategoricalPredictLeaf(Context const *ctx, bool is_column_split); void TestIterationRange(Context const* ctx); diff --git a/tests/python-sycl/test_sycl_prediction.py b/tests/python-sycl/test_sycl_prediction.py new file mode 100644 index 000000000..06167c6c0 --- /dev/null +++ b/tests/python-sycl/test_sycl_prediction.py @@ -0,0 +1,165 @@ +import sys +import unittest +import pytest + +import numpy as np +import xgboost as xgb +from hypothesis import given, strategies, assume, settings, note + +from xgboost import testing as tm + +rng = np.random.RandomState(1994) + +shap_parameter_strategy = strategies.fixed_dictionaries( + { + "max_depth": strategies.integers(1, 11), + "max_leaves": strategies.integers(0, 256), + "num_parallel_tree": strategies.sampled_from([1, 10]), + } +).filter(lambda x: x["max_depth"] > 0 or x["max_leaves"] > 0) + + +class TestSYCLPredict(unittest.TestCase): + def test_predict(self): + iterations = 10 + np.random.seed(1) + test_num_rows = [10, 1000, 5000] + test_num_cols = [10, 50, 500] + for num_rows in test_num_rows: + for num_cols in test_num_cols: + dtrain = xgb.DMatrix( + np.random.randn(num_rows, num_cols), + label=[0, 1] * int(num_rows / 2), + ) + dval = xgb.DMatrix( + np.random.randn(num_rows, num_cols), + label=[0, 1] * int(num_rows / 2), + ) + dtest = xgb.DMatrix( + np.random.randn(num_rows, num_cols), + label=[0, 1] * int(num_rows / 2), + ) + watchlist = [(dtrain, "train"), (dval, "validation")] + res = {} + param = { + "objective": "binary:logistic", + "eval_metric": "logloss", + "tree_method": "hist", + "device": "cpu", + "max_depth": 1, + "verbosity": 0, + } + bst = xgb.train( + param, dtrain, iterations, evals=watchlist, evals_result=res + ) + assert tm.non_increasing(res["train"]["logloss"]) + cpu_pred_train = bst.predict(dtrain, output_margin=True) + cpu_pred_test = bst.predict(dtest, output_margin=True) + cpu_pred_val = bst.predict(dval, output_margin=True) + + bst.set_param({"device": "sycl"}) + sycl_pred_train = bst.predict(dtrain, output_margin=True) + sycl_pred_test = bst.predict(dtest, output_margin=True) + sycl_pred_val = bst.predict(dval, output_margin=True) + + np.testing.assert_allclose(cpu_pred_train, sycl_pred_train, rtol=1e-6) + np.testing.assert_allclose(cpu_pred_val, sycl_pred_val, rtol=1e-6) + np.testing.assert_allclose(cpu_pred_test, sycl_pred_test, rtol=1e-6) + + @pytest.mark.skipif(**tm.no_sklearn()) + def test_multi_predict(self): + from sklearn.datasets import make_regression + from sklearn.model_selection import train_test_split + + n = 1000 + X, y = make_regression(n, random_state=rng) + X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=123) + dtrain = xgb.DMatrix(X_train, label=y_train) + dtest = xgb.DMatrix(X_test) + + params = {} + params["tree_method"] = "hist" + params["device"] = "cpu" + + bst = xgb.train(params, dtrain) + cpu_predict = bst.predict(dtest) + + bst.set_param({"device": "sycl"}) + + predict0 = bst.predict(dtest) + predict1 = bst.predict(dtest) + + assert np.allclose(predict0, predict1) + assert np.allclose(predict0, cpu_predict) + + @pytest.mark.skipif(**tm.no_sklearn()) + def test_sklearn(self): + m, n = 15000, 14 + tr_size = 2500 + X = np.random.rand(m, n) + y = 200 * np.matmul(X, np.arange(-3, -3 + n)) + X_train, y_train = X[:tr_size, :], y[:tr_size] + X_test, y_test = X[tr_size:, :], y[tr_size:] + + # First with cpu_predictor + params = { + "tree_method": "hist", + "device": "cpu", + "n_jobs": -1, + "verbosity": 0, + "seed": 123, + } + m = xgb.XGBRegressor(**params).fit(X_train, y_train) + cpu_train_score = m.score(X_train, y_train) + cpu_test_score = m.score(X_test, y_test) + + # Now with sycl_predictor + params["device"] = "sycl" + m.set_params(**params) + + sycl_train_score = m.score(X_train, y_train) + sycl_test_score = m.score(X_test, y_test) + + assert np.allclose(cpu_train_score, sycl_train_score) + assert np.allclose(cpu_test_score, sycl_test_score) + + @given( + strategies.integers(1, 10), tm.make_dataset_strategy(), shap_parameter_strategy + ) + @settings(deadline=None) + def test_shap(self, num_rounds, dataset, param): + if dataset.name.endswith("-l1"): # not supported by the exact tree method + return + param.update({"tree_method": "hist", "device": "cpu"}) + param = dataset.set_params(param) + dmat = dataset.get_dmat() + bst = xgb.train(param, dmat, num_rounds) + test_dmat = xgb.DMatrix(dataset.X, dataset.y, dataset.w, dataset.margin) + bst.set_param({"device": "sycl"}) + shap = bst.predict(test_dmat, pred_contribs=True) + margin = bst.predict(test_dmat, output_margin=True) + assume(len(dataset.y) > 0) + assert np.allclose(np.sum(shap, axis=len(shap.shape) - 1), margin, 1e-3, 1e-3) + + @given( + strategies.integers(1, 10), tm.make_dataset_strategy(), shap_parameter_strategy + ) + @settings(deadline=None, max_examples=20) + def test_shap_interactions(self, num_rounds, dataset, param): + if dataset.name.endswith("-l1"): # not supported by the exact tree method + return + param.update({"tree_method": "hist", "device": "cpu"}) + param = dataset.set_params(param) + dmat = dataset.get_dmat() + bst = xgb.train(param, dmat, num_rounds) + test_dmat = xgb.DMatrix(dataset.X, dataset.y, dataset.w, dataset.margin) + bst.set_param({"device": "sycl"}) + shap = bst.predict(test_dmat, pred_interactions=True) + margin = bst.predict(test_dmat, output_margin=True) + assume(len(dataset.y) > 0) + assert np.allclose( + np.sum(shap, axis=(len(shap.shape) - 1, len(shap.shape) - 2)), + margin, + 1e-3, + 1e-3, + ) From 9c56916fd7e8cd4d7c7aada9e627217d2adb8ae2 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 4 Dec 2023 11:40:45 +0100 Subject: [PATCH 032/109] [R] Very small performance tweaks (#9837) --- R-package/src/xgboost_R.cc | 40 ++++++++++++++------------------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index b267d7da6..8da00aa58 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -160,13 +160,6 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { using dmlc::BeginPtr; -xgboost::Context const *DMatrixCtx(DMatrixHandle handle) { - CHECK_HANDLE(); - auto p_m = static_cast *>(handle); - CHECK(p_m); - return p_m->get()->Ctx(); -} - XGB_DLL SEXP XGCheckNullPtr_R(SEXP handle) { return ScalarLogical(R_ExternalPtrAddr(handle) == NULL); } @@ -318,6 +311,9 @@ XGB_DLL SEXP XGDMatrixSliceDMatrix_R(SEXP handle, SEXP idxset) { int res_code; { std::vector idxvec(len); + #ifndef _MSC_VER + #pragma omp simd + #endif for (R_xlen_t i = 0; i < len; ++i) { idxvec[i] = idxset_[i] - 1; } @@ -375,6 +371,7 @@ XGB_DLL SEXP XGDMatrixSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP array) { int res_code; { std::vector str_info; + str_info.reserve(len); for (size_t i = 0; i < len; ++i) { str_info.emplace_back(CHAR(VECTOR_ELT(str_info_holder, i))); } @@ -457,9 +454,9 @@ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { int res_code; { - std::vector dvec; + std::vector dvec(len); for (R_xlen_t i = 0; i < len; ++i) { - dvec.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); + dvec[i] = R_ExternalPtrAddr(VECTOR_ELT(dmats, i)); } res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); } @@ -478,9 +475,9 @@ XGB_DLL SEXP XGBoosterCreateInEmptyObj_R(SEXP dmats, SEXP R_handle) { int res_code; { - std::vector dvec; + std::vector dvec(len); for (R_xlen_t i = 0; i < len; ++i) { - dvec.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); + dvec[i] = R_ExternalPtrAddr(VECTOR_ELT(dmats, i)); } res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); } @@ -552,15 +549,16 @@ XGB_DLL SEXP XGBoosterEvalOneIter_R(SEXP handle, SEXP iter, SEXP dmats, SEXP evn int res_code; { - std::vector vec_dmats; + std::vector vec_dmats(len); std::vector vec_names; - std::vector vec_sptr; + vec_names.reserve(len); + std::vector vec_sptr(len); for (R_xlen_t i = 0; i < len; ++i) { - vec_dmats.push_back(R_ExternalPtrAddr(VECTOR_ELT(dmats, i))); + vec_dmats[i] = R_ExternalPtrAddr(VECTOR_ELT(dmats, i)); vec_names.emplace_back(CHAR(VECTOR_ELT(evnames_lst, i))); } for (R_xlen_t i = 0; i < len; ++i) { - vec_sptr.push_back(vec_names[i].c_str()); + vec_sptr[i] = vec_names[i].c_str(); } res_code = XGBoosterEvalOneIter(R_ExternalPtrAddr(handle), asInteger(iter), @@ -598,11 +596,7 @@ XGB_DLL SEXP XGBoosterPredictFromDMatrix_R(SEXP handle, SEXP dmat, SEXP json_con len *= out_shape[i]; } r_out_result = PROTECT(allocVector(REALSXP, len)); - auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); - double *r_out_result_ = REAL(r_out_result); - xgboost::common::ParallelFor(len, ctx->Threads(), [&](xgboost::omp_ulong i) { - r_out_result_[i] = out_result[i]; - }); + std::copy(out_result, out_result + len, REAL(r_out_result)); SET_VECTOR_ELT(r_out, 0, r_out_shape); SET_VECTOR_ELT(r_out, 1, r_out_result); @@ -831,11 +825,7 @@ XGB_DLL SEXP XGBoosterFeatureScore_R(SEXP handle, SEXP json_config) { } out_scores_sexp = PROTECT(allocVector(REALSXP, len)); - auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); - double *out_scores_sexp_ = REAL(out_scores_sexp); - xgboost::common::ParallelFor(len, ctx->Threads(), [&](xgboost::omp_ulong i) { - out_scores_sexp_[i] = out_scores[i]; - }); + std::copy(out_scores, out_scores + len, REAL(out_scores_sexp)); SET_VECTOR_ELT(r_out, 0, out_features_sexp); SET_VECTOR_ELT(r_out, 1, out_shape_sexp); From 62571b79eb08398a031873c3704da4e9cfd2c301 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 5 Dec 2023 20:13:14 +0100 Subject: [PATCH 033/109] [R] Enable multi-output objectives (#9839) --- R-package/R/utils.R | 27 +++++++------- R-package/R/xgb.DMatrix.R | 7 ++-- R-package/src/init.c | 2 +- R-package/src/xgboost_R.cc | 18 ++++------ R-package/tests/testthat/test_basic.R | 51 +++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 27 deletions(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index 2fc2321ca..6f1a1b4ec 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -160,23 +160,24 @@ xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { ) gpair <- obj(pred, dtrain) n_samples <- dim(dtrain)[1] + grad <- gpair$grad + hess <- gpair$hess - msg <- paste( - "Since 2.1.0, the shape of the gradient and hessian is required to be ", - "(n_samples, n_targets) or (n_samples, n_classes).", - sep = "" - ) - if (is.matrix(gpair$grad) && dim(gpair$grad)[1] != n_samples) { - warning(msg) - } - if (is.numeric(gpair$grad) && length(gpair$grad) != n_samples) { - warning(msg) + if ((is.matrix(grad) && dim(grad)[1] != n_samples) || + (is.vector(grad) && length(grad) != n_samples) || + (is.vector(grad) != is.vector(hess))) { + warning(paste( + "Since 2.1.0, the shape of the gradient and hessian is required to be ", + "(n_samples, n_targets) or (n_samples, n_classes). Will reshape assuming ", + "column-major order.", + sep = "" + )) + grad <- matrix(grad, nrow = n_samples) + hess <- matrix(hess, nrow = n_samples) } - gpair$grad <- matrix(gpair$grad, nrow = n_samples) - gpair$hess <- matrix(gpair$hess, nrow = n_samples) .Call( - XGBoosterBoostOneIter_R, booster_handle, dtrain, iter, gpair$grad, gpair$hess + XGBoosterTrainOneIter_R, booster_handle, dtrain, iter, grad, hess ) } return(TRUE) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 8e19e87b0..3a1b6afc8 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -243,6 +243,9 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { ret <- .Call(XGDMatrixGetStrFeatureInfo_R, object, name) } else if (name != "nrow") { ret <- .Call(XGDMatrixGetInfo_R, object, name) + if (length(ret) > nrow(object)) { + ret <- matrix(ret, nrow = nrow(object), byrow = TRUE) + } } else { ret <- nrow(object) } @@ -286,9 +289,9 @@ setinfo <- function(object, ...) UseMethod("setinfo") #' @export setinfo.xgb.DMatrix <- function(object, name, info, ...) { if (name == "label") { - if (length(info) != nrow(object)) + if (NROW(info) != nrow(object)) stop("The length of labels must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "label_lower_bound") { diff --git a/R-package/src/init.c b/R-package/src/init.c index 5c8e179d6..afac524e3 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -52,7 +52,7 @@ extern SEXP XGBGetGlobalConfig_R(void); extern SEXP XGBoosterFeatureScore_R(SEXP, SEXP); static const R_CallMethodDef CallEntries[] = { - {"XGBoosterBoostOneIter_R", (DL_FUNC) &XGBoosterTrainOneIter_R, 5}, + {"XGBoosterTrainOneIter_R", (DL_FUNC) &XGBoosterTrainOneIter_R, 5}, {"XGBoosterCreate_R", (DL_FUNC) &XGBoosterCreate_R, 1}, {"XGBoosterCreateInEmptyObj_R", (DL_FUNC) &XGBoosterCreateInEmptyObj_R, 2}, {"XGBoosterDumpModel_R", (DL_FUNC) &XGBoosterDumpModel_R, 4}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 8da00aa58..1ab3e7641 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -342,9 +342,11 @@ XGB_DLL SEXP XGDMatrixSaveBinary_R(SEXP handle, SEXP fname, SEXP silent) { XGB_DLL SEXP XGDMatrixSetInfo_R(SEXP handle, SEXP field, SEXP array) { R_API_BEGIN(); SEXP field_ = PROTECT(Rf_asChar(field)); + SEXP arr_dim = Rf_getAttrib(array, R_DimSymbol); int res_code; { - const std::string array_str = MakeArrayInterfaceFromRVector(array); + const std::string array_str = Rf_isNull(arr_dim)? + MakeArrayInterfaceFromRVector(array) : MakeArrayInterfaceFromRMat(array); res_code = XGDMatrixSetInfoFromInterface( R_ExternalPtrAddr(handle), CHAR(field_), array_str.c_str()); } @@ -513,20 +515,14 @@ XGB_DLL SEXP XGBoosterTrainOneIter_R(SEXP handle, SEXP dtrain, SEXP iter, SEXP g R_API_BEGIN(); CHECK_EQ(Rf_xlength(grad), Rf_xlength(hess)) << "gradient and hess must have same length."; SEXP gdim = getAttrib(grad, R_DimSymbol); - auto n_samples = static_cast(INTEGER(gdim)[0]); - auto n_targets = static_cast(INTEGER(gdim)[1]); - SEXP hdim = getAttrib(hess, R_DimSymbol); - CHECK_EQ(INTEGER(hdim)[0], n_samples) << "mismatched size between gradient and hessian"; - CHECK_EQ(INTEGER(hdim)[1], n_targets) << "mismatched size between gradient and hessian"; - double const *d_grad = REAL(grad); - double const *d_hess = REAL(hess); int res_code; { - auto ctx = xgboost::detail::BoosterCtx(R_ExternalPtrAddr(handle)); - auto [s_grad, s_hess] = xgboost::detail::MakeGradientInterface( - ctx, d_grad, d_hess, xgboost::linalg::kF, n_samples, n_targets); + const std::string s_grad = Rf_isNull(gdim)? + MakeArrayInterfaceFromRVector(grad) : MakeArrayInterfaceFromRMat(grad); + const std::string s_hess = Rf_isNull(hdim)? + MakeArrayInterfaceFromRVector(hess) : MakeArrayInterfaceFromRMat(hess); res_code = XGBoosterTrainOneIter(R_ExternalPtrAddr(handle), R_ExternalPtrAddr(dtrain), asInteger(iter), s_grad.c_str(), s_hess.c_str()); } diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index b7e819738..97c1353dc 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -565,3 +565,54 @@ test_that("'predict' accepts CSR data", { expect_equal(p_csc, p_csr) expect_equal(p_csc, p_spv) }) + +test_that("Can use multi-output labels with built-in objectives", { + data("mtcars") + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + y_mirrored <- cbind(y, -y) + dm <- xgb.DMatrix(x, label = y_mirrored, nthread = n_threads) + model <- xgb.train( + params = list( + tree_method = "hist", + multi_strategy = "multi_output_tree", + objective = "reg:squarederror", + nthread = n_threads + ), + data = dm, + nrounds = 5 + ) + pred <- predict(model, x, reshape = TRUE) + expect_equal(pred[, 1], -pred[, 2]) + expect_true(cor(y, pred[, 1]) > 0.9) + expect_true(cor(y, pred[, 2]) < -0.9) +}) + +test_that("Can use multi-output labels with custom objectives", { + data("mtcars") + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + y_mirrored <- cbind(y, -y) + dm <- xgb.DMatrix(x, label = y_mirrored, nthread = n_threads) + model <- xgb.train( + params = list( + tree_method = "hist", + multi_strategy = "multi_output_tree", + base_score = 0, + objective = function(pred, dtrain) { + y <- getinfo(dtrain, "label") + grad <- pred - y + hess <- rep(1, nrow(grad) * ncol(grad)) + hess <- matrix(hess, nrow = nrow(grad)) + return(list(grad = grad, hess = hess)) + }, + nthread = n_threads + ), + data = dm, + nrounds = 5 + ) + pred <- predict(model, x, reshape = TRUE) + expect_equal(pred[, 1], -pred[, 2]) + expect_true(cor(y, pred[, 1]) > 0.9) + expect_true(cor(y, pred[, 2]) < -0.9) +}) From 0716c64ef75d62384437f587eab8fa3925763e38 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 10:43:51 +0100 Subject: [PATCH 034/109] [R] Error out on multidimensional arrays (#9852) --- R-package/src/xgboost_R.cc | 3 +++ R-package/tests/testthat/test_dmatrix.R | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 1ab3e7641..214ca6cb4 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -50,6 +50,9 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { [[nodiscard]] std::string MakeArrayInterfaceFromRMat(SEXP R_mat) { SEXP mat_dims = Rf_getAttrib(R_mat, R_DimSymbol); + if (Rf_xlength(mat_dims) > 2) { + LOG(FATAL) << "Passed input array with more than two dimensions, which is not supported."; + } const int *ptr_mat_dims = INTEGER(mat_dims); // Lambda for type dispatch. diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index a0cf90088..4db7aad08 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -297,3 +297,11 @@ test_that("xgb.DMatrix: Inf as missing", { file.remove("inf.dmatrix") file.remove("nan.dmatrix") }) + +test_that("xgb.DMatrix: error on three-dimensional array", { + set.seed(123) + x <- matrix(rnorm(500), nrow = 50) + y <- rnorm(400) + dim(y) <- c(50, 4, 2) + expect_error(xgb.DMatrix(data = x, label = y)) +}) From 1de3f4135cb56cebd1edb3f8f65e1acbd22ce829 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 13:32:20 +0100 Subject: [PATCH 035/109] [R] Enable vector-valued parameters (#9849) --- R-package/R/utils.R | 8 ++++++++ R-package/R/xgb.Booster.R | 8 +++++++- R-package/tests/testthat/test_basic.R | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index 6f1a1b4ec..bf08c481d 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -93,6 +93,14 @@ check.booster.params <- function(params, ...) { interaction_constraints <- sapply(params[['interaction_constraints']], function(x) paste0('[', paste(x, collapse = ','), ']')) params[['interaction_constraints']] <- paste0('[', paste(interaction_constraints, collapse = ','), ']') } + + # for evaluation metrics, should generate multiple entries per metric + if (NROW(params[['eval_metric']]) > 1) { + eval_metrics <- as.list(params[["eval_metric"]]) + names(eval_metrics) <- rep("eval_metric", length(eval_metrics)) + params_without_ev_metrics <- within(params, rm("eval_metric")) + params <- c(params_without_ev_metrics, eval_metrics) + } return(params) } diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 37cfc199e..a5c9e5088 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -697,7 +697,13 @@ xgb.config <- function(object) { stop("parameter names cannot be empty strings") } names(p) <- gsub(".", "_", names(p), fixed = TRUE) - p <- lapply(p, function(x) as.character(x)[1]) + p <- lapply(p, function(x) { + if (is.vector(x) && length(x) == 1) { + return(as.character(x)[1]) + } else { + return(jsonlite::toJSON(x, auto_unbox = TRUE)) + } + }) handle <- xgb.get.handle(object) for (i in seq_along(p)) { .Call(XGBoosterSetParam_R, handle, names(p[i]), p[[i]]) diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 97c1353dc..c96871e11 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -566,6 +566,33 @@ test_that("'predict' accepts CSR data", { expect_equal(p_csc, p_spv) }) +test_that("Quantile regression accepts multiple quantiles", { + data(mtcars) + y <- mtcars[, 1] + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(data = x, label = y) + model <- xgb.train( + data = dm, + params = list( + objective = "reg:quantileerror", + tree_method = "exact", + quantile_alpha = c(0.05, 0.5, 0.95), + nthread = n_threads + ), + nrounds = 15 + ) + pred <- predict(model, x, reshape = TRUE) + + expect_equal(dim(pred)[1], nrow(x)) + expect_equal(dim(pred)[2], 3) + expect_true(all(pred[, 1] <= pred[, 3])) + + cors <- cor(y, pred) + expect_true(cors[2] > cors[1]) + expect_true(cors[2] > cors[3]) + expect_true(cors[2] > 0.85) +}) + test_that("Can use multi-output labels with built-in objectives", { data("mtcars") y <- mtcars$mpg From 4bc1f3a388b9d5e4cb8260eb20aa56ab3f371bb8 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 7 Dec 2023 00:12:45 +0800 Subject: [PATCH 036/109] [R] Bump requirement to 4.3.0. (#9847) --- .github/workflows/r_tests.yml | 6 +++--- R-package/CMakeLists.txt | 9 +++++++++ R-package/DESCRIPTION | 2 +- tests/ci_build/test_r_package.py | 2 ++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/r_tests.yml b/.github/workflows/r_tests.yml index c353fd0d2..917245ec6 100644 --- a/.github/workflows/r_tests.yml +++ b/.github/workflows/r_tests.yml @@ -25,7 +25,7 @@ jobs: with: submodules: 'true' - - uses: r-lib/actions/setup-r@11a22a908006c25fe054c4ef0ac0436b1de3edbe # v2.6.4 + - uses: r-lib/actions/setup-r@e40ad904310fc92e96951c1b0d64f3de6cbe9e14 # v2.6.5 with: r-version: ${{ matrix.config.r }} @@ -54,7 +54,7 @@ jobs: matrix: config: - {os: windows-latest, r: 'release', compiler: 'mingw', build: 'autotools'} - - {os: windows-latest, r: '4.2.0', compiler: 'msvc', build: 'cmake'} + - {os: windows-latest, r: '4.3.0', compiler: 'msvc', build: 'cmake'} env: R_REMOTES_NO_ERRORS_FROM_WARNINGS: true RSPM: ${{ matrix.config.rspm }} @@ -64,7 +64,7 @@ jobs: with: submodules: 'true' - - uses: r-lib/actions/setup-r@11a22a908006c25fe054c4ef0ac0436b1de3edbe # v2.6.4 + - uses: r-lib/actions/setup-r@e40ad904310fc92e96951c1b0d64f3de6cbe9e14 # v2.6.5 with: r-version: ${{ matrix.config.r }} diff --git a/R-package/CMakeLists.txt b/R-package/CMakeLists.txt index a19e56f4e..d3a69abc2 100644 --- a/R-package/CMakeLists.txt +++ b/R-package/CMakeLists.txt @@ -14,6 +14,15 @@ if(ENABLE_ALL_WARNINGS) target_compile_options(xgboost-r PRIVATE -Wall -Wextra) endif() +if(MSVC) + # https://github.com/microsoft/LightGBM/pull/6061 + # MSVC doesn't work with anonymous types in structs. (R complex) + # + # syntax error: missing ';' before identifier 'private_data_c' + # + target_compile_definitions(xgboost-r PRIVATE -DR_LEGACY_RCOMPLEX) +endif() + target_compile_definitions( xgboost-r PUBLIC -DXGBOOST_STRICT_R_MODE=1 diff --git a/R-package/DESCRIPTION b/R-package/DESCRIPTION index d301b0a5c..92b53a660 100644 --- a/R-package/DESCRIPTION +++ b/R-package/DESCRIPTION @@ -58,7 +58,7 @@ Suggests: float, titanic Depends: - R (>= 3.3.0) + R (>= 4.3.0) Imports: Matrix (>= 1.1-0), methods, diff --git a/tests/ci_build/test_r_package.py b/tests/ci_build/test_r_package.py index 853bf0502..cc1225b03 100644 --- a/tests/ci_build/test_r_package.py +++ b/tests/ci_build/test_r_package.py @@ -261,6 +261,8 @@ def test_with_cmake(args: argparse.Namespace) -> None: "-DCMAKE_CONFIGURATION_TYPES=Release", "-A", "x64", + "-G", + "Visual Studio 17 2022", ] ) subprocess.check_call( From 9e9d41b95c0074c49c98e06fe1ee9716fbc123e7 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 20:11:15 +0100 Subject: [PATCH 037/109] [R] Add note about serialization of DMatrix objects (#9853) --- R-package/R/xgb.DMatrix.R | 6 ++++++ R-package/man/xgb.DMatrix.Rd | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 3a1b6afc8..0d9a89912 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -16,6 +16,12 @@ #' @param nthread Number of threads used for creating DMatrix. #' @param ... the \code{info} data could be passed directly as parameters, without creating an \code{info} list. #' +#' @details +#' Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. +#' If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching +#' chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed +#' from the original source of data. +#' #' @examples #' data(agaricus.train, package='xgboost') #' ## Keep the number of threads to 1 for examples diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 38a65c638..96f050c72 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -36,6 +36,12 @@ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a l Supported input file formats are either a LIBSVM text file or a binary file that was created previously by \code{\link{xgb.DMatrix.save}}). } +\details{ +Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. +If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching +chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed +from the original source of data. +} \examples{ data(agaricus.train, package='xgboost') ## Keep the number of threads to 1 for examples From 2c0fc9730600980a0571329e35772dac8e173ed2 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 6 Dec 2023 22:17:15 +0100 Subject: [PATCH 038/109] Remove note about multi-quantile being python-only (#9854) --- demo/guide-python/quantile_regression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/guide-python/quantile_regression.py b/demo/guide-python/quantile_regression.py index 4f69a8c80..5d186714c 100644 --- a/demo/guide-python/quantile_regression.py +++ b/demo/guide-python/quantile_regression.py @@ -9,7 +9,7 @@ https://scikit-learn.org/stable/auto_examples/ensemble/plot_gradient_boosting_qu .. note:: - The feature is only supported using the Python package. In addition, quantile + The feature is only supported using the Python, R, and C packages. In addition, quantile crossing can happen due to limitation in the algorithm. """ From 39c637ee199303d1c84ef25e55d2651d83c217e5 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 8 Dec 2023 03:42:14 +0800 Subject: [PATCH 039/109] Use array interface in Python prediction return. (#9855) --- include/xgboost/linalg.h | 4 +- python-package/xgboost/compat.py | 10 ++++ python-package/xgboost/core.py | 81 ++++++++++++++------------------ src/common/device_helpers.cuh | 2 + 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/include/xgboost/linalg.h b/include/xgboost/linalg.h index 8806818fb..581b2f080 100644 --- a/include/xgboost/linalg.h +++ b/include/xgboost/linalg.h @@ -683,7 +683,7 @@ using MatrixView = TensorView; * * `stream` is optionally included when data is on CUDA device. */ -template +template Json ArrayInterface(TensorView const &t) { Json array_interface{Object{}}; array_interface["data"] = std::vector(2); @@ -691,7 +691,7 @@ Json ArrayInterface(TensorView const &t) { array_interface["data"][1] = Boolean{true}; if (t.Device().IsCUDA()) { // Change this once we have different CUDA stream. - array_interface["stream"] = Null{}; + array_interface["stream"] = Integer{2}; } std::vector shape(t.Shape().size()); std::vector stride(t.Stride().size()); diff --git a/python-package/xgboost/compat.py b/python-package/xgboost/compat.py index c40dea5fd..7c11495f7 100644 --- a/python-package/xgboost/compat.py +++ b/python-package/xgboost/compat.py @@ -100,6 +100,16 @@ def is_cupy_available() -> bool: return False +def import_cupy() -> types.ModuleType: + """Import cupy.""" + if not is_cupy_available(): + raise ImportError("`cupy` is required for handling CUDA buffer.") + + import cupy # pylint: disable=import-error + + return cupy + + try: import scipy.sparse as scipy_sparse from scipy.sparse import csr_matrix as scipy_csr diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index bfc94aa04..ffc7db8dd 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -3,7 +3,6 @@ """Core XGBoost Library.""" import copy import ctypes -import importlib.util import json import os import re @@ -45,7 +44,6 @@ from ._typing import ( CStrPptr, CStrPtr, CTypeT, - CupyT, DataType, FeatureInfo, FeatureNames, @@ -55,7 +53,7 @@ from ._typing import ( TransformedData, c_bst_ulong, ) -from .compat import PANDAS_INSTALLED, DataFrame, py_str +from .compat import PANDAS_INSTALLED, DataFrame, import_cupy, py_str from .libpath import find_lib_path @@ -380,34 +378,6 @@ def ctypes2numpy(cptr: CNumericPtr, length: int, dtype: Type[np.number]) -> np.n return res -def ctypes2cupy(cptr: CNumericPtr, length: int, dtype: Type[np.number]) -> CupyT: - """Convert a ctypes pointer array to a cupy array.""" - # pylint: disable=import-error - import cupy - from cupy.cuda.memory import MemoryPointer, UnownedMemory - - CUPY_TO_CTYPES_MAPPING: Dict[Type[np.number], Type[CNumeric]] = { - cupy.float32: ctypes.c_float, - cupy.uint32: ctypes.c_uint, - } - if dtype not in CUPY_TO_CTYPES_MAPPING: - raise RuntimeError(f"Supported types: {CUPY_TO_CTYPES_MAPPING.keys()}") - addr = ctypes.cast(cptr, ctypes.c_void_p).value - # pylint: disable=c-extension-no-member,no-member - device = cupy.cuda.runtime.pointerGetAttributes(addr).device - # The owner field is just used to keep the memory alive with ref count. As - # unowned's life time is scoped within this function we don't need that. - unownd = UnownedMemory( - addr, length * ctypes.sizeof(CUPY_TO_CTYPES_MAPPING[dtype]), owner=None - ) - memptr = MemoryPointer(unownd, 0) - # pylint: disable=unexpected-keyword-arg - mem = cupy.ndarray((length,), dtype=dtype, memptr=memptr) - assert mem.device.id == device - arr = cupy.array(mem, copy=True) - return arr - - def ctypes2buffer(cptr: CStrPtr, length: int) -> bytearray: """Convert ctypes pointer to buffer type.""" if not isinstance(cptr, ctypes.POINTER(ctypes.c_char)): @@ -466,14 +436,8 @@ def from_array_interface(interface: dict) -> NumpyOrCupy: if "stream" in interface: # CUDA stream is presented, this is a __cuda_array_interface__. - spec = importlib.util.find_spec("cupy") - if spec is None: - raise ImportError("`cupy` is required for handling CUDA buffer.") - - import cupy as cp # pylint: disable=import-error - arr.__cuda_array_interface__ = interface - out = cp.array(arr, copy=True) + out = import_cupy().array(arr, copy=True) else: arr.__array_interface__ = interface out = np.array(arr, copy=True) @@ -481,17 +445,42 @@ def from_array_interface(interface: dict) -> NumpyOrCupy: return out +def make_array_interface( + ptr: CNumericPtr, shape: Tuple[int, ...], dtype: Type[np.number], is_cuda: bool +) -> Dict[str, Union[int, tuple, None]]: + """Make an __(cuda)_array_interface__ from a pointer.""" + # Use an empty array to handle typestr and descr + if is_cuda: + empty = import_cupy().empty(shape=(0,), dtype=dtype) + array = empty.__cuda_array_interface__ # pylint: disable=no-member + else: + empty = np.empty(shape=(0,), dtype=dtype) + array = empty.__array_interface__ # pylint: disable=no-member + + addr = ctypes.cast(ptr, ctypes.c_void_p).value + length = int(np.prod(shape)) + # Handle empty dataset. + assert addr is not None or length == 0 + + if addr is None: + return array + + array["data"] = (addr, True) + if is_cuda: + array["stream"] = 2 + array["shape"] = shape + array["strides"] = None + return array + + def _prediction_output( shape: CNumericPtr, dims: c_bst_ulong, predts: CFloatPtr, is_cuda: bool ) -> NumpyOrCupy: - arr_shape = ctypes2numpy(shape, dims.value, np.uint64) - length = int(np.prod(arr_shape)) - if is_cuda: - arr_predict = ctypes2cupy(predts, length, np.float32) - else: - arr_predict = ctypes2numpy(predts, length, np.float32) - arr_predict = arr_predict.reshape(arr_shape) - return arr_predict + arr_shape = tuple(ctypes2numpy(shape, dims.value, np.uint64).flatten()) + array = from_array_interface( + make_array_interface(predts, arr_shape, np.float32, is_cuda) + ) + return array class DataIter(ABC): # pylint: disable=too-many-instance-attributes diff --git a/src/common/device_helpers.cuh b/src/common/device_helpers.cuh index ffe61800e..46f76c415 100644 --- a/src/common/device_helpers.cuh +++ b/src/common/device_helpers.cuh @@ -1097,6 +1097,8 @@ inline void CUDAEvent::Record(CUDAStreamView stream) { // NOLINT dh::safe_cuda(cudaEventRecord(event_, cudaStream_t{stream})); } +// Changing this has effect on prediction return, where we need to pass the pointer to +// third-party libraries like cuPy inline CUDAStreamView DefaultStream() { #ifdef CUDA_API_PER_THREAD_DEFAULT_STREAM return CUDAStreamView{cudaStreamPerThread}; From 42de9206fca7e4674ae837020e758d87e12d63a4 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 8 Dec 2023 05:50:41 +0800 Subject: [PATCH 040/109] Support multi-target, fit intercept for hinge. (#9850) --- src/common/linalg_op.cuh | 54 ++++++++----- src/common/linalg_op.h | 30 ++++--- src/objective/hinge.cu | 126 ++++++++++++++++-------------- src/objective/quantile_obj.cu | 33 ++++---- src/objective/regression_obj.cu | 43 +++++----- tests/cpp/common/test_linalg.cu | 12 ++- tests/cpp/data/test_metainfo.h | 23 +++--- tests/cpp/objective/test_hinge.cc | 55 +++++++++---- 8 files changed, 221 insertions(+), 155 deletions(-) diff --git a/src/common/linalg_op.cuh b/src/common/linalg_op.cuh index 361f926e9..9ef36598d 100644 --- a/src/common/linalg_op.cuh +++ b/src/common/linalg_op.cuh @@ -1,31 +1,48 @@ -/*! - * Copyright 2021-2022 by XGBoost Contributors +/** + * Copyright 2021-2023, XGBoost Contributors */ #ifndef XGBOOST_COMMON_LINALG_OP_CUH_ #define XGBOOST_COMMON_LINALG_OP_CUH_ -#include "device_helpers.cuh" +#include // for int32_t +#include // for size_t +#include // for apply + +#include "device_helpers.cuh" // for LaunchN #include "linalg_op.h" -#include "xgboost/context.h" -#include "xgboost/linalg.h" +#include "xgboost/context.h" // for Context +#include "xgboost/linalg.h" // for TensorView namespace xgboost { namespace linalg { -template -void ElementWiseKernelDevice(linalg::TensorView t, Fn&& fn, cudaStream_t s = nullptr) { - dh::safe_cuda(cudaSetDevice(t.Device().ordinal)); - static_assert(std::is_void>::value, - "For function with return, use transform instead."); - if (t.Contiguous()) { - auto ptr = t.Values().data(); - dh::LaunchN(t.Size(), s, [=] __device__(size_t i) mutable { fn(i, ptr[i]); }); - } else { - dh::LaunchN(t.Size(), s, [=] __device__(size_t i) mutable { - T& v = detail::Apply(t, linalg::UnravelIndex(i, t.Shape())); - fn(i, v); +namespace cuda_impl { +// Use template specialization to dispatch, Windows + CUDA 11.8 doesn't support extended +// lambda inside constexpr if +template +struct ElementWiseImpl { + template + void operator()(linalg::TensorView t, Fn&& fn, cudaStream_t s) { + static_assert(D > 1); + dh::LaunchN(t.Size(), s, [=] __device__(std::size_t i) mutable { + std::apply(fn, linalg::UnravelIndex(i, t.Shape())); }); } +}; + +template +struct ElementWiseImpl { + template + void operator()(linalg::TensorView t, Fn&& fn, cudaStream_t s) { + dh::LaunchN(t.Size(), s, [=] __device__(std::size_t i) { fn(i); }); + } +}; + +template +void ElementWiseKernel(linalg::TensorView t, Fn&& fn, cudaStream_t s = nullptr) { + dh::safe_cuda(cudaSetDevice(t.Device().ordinal)); + cuda_impl::ElementWiseImpl{}(t, fn, s); } +} // namespace cuda_impl template void ElementWiseTransformDevice(linalg::TensorView t, Fn&& fn, cudaStream_t s = nullptr) { @@ -42,7 +59,8 @@ void ElementWiseTransformDevice(linalg::TensorView t, Fn&& fn, cudaStream_ template void ElementWiseKernel(Context const* ctx, linalg::TensorView t, Fn&& fn) { - ctx->IsCUDA() ? ElementWiseKernelDevice(t, fn) : ElementWiseKernelHost(t, ctx->Threads(), fn); + ctx->IsCUDA() ? cuda_impl::ElementWiseKernel(t, fn) + : ElementWiseKernelHost(t, ctx->Threads(), fn); } } // namespace linalg } // namespace xgboost diff --git a/src/common/linalg_op.h b/src/common/linalg_op.h index d89e5a736..52141164f 100644 --- a/src/common/linalg_op.h +++ b/src/common/linalg_op.h @@ -1,5 +1,5 @@ -/*! - * Copyright 2021-2022 by XGBoost Contributors +/** + * Copyright 2021-2023, XGBoost Contributors */ #ifndef XGBOOST_COMMON_LINALG_OP_H_ #define XGBOOST_COMMON_LINALG_OP_H_ @@ -27,17 +27,23 @@ void ElementWiseTransformHost(linalg::TensorView t, int32_t n_threads, Fn& } } -template -void ElementWiseKernelHost(linalg::TensorView t, int32_t n_threads, Fn&& fn) { - static_assert(std::is_void>::value, - "For function with return, use transform instead."); - if (t.Contiguous()) { - auto ptr = t.Values().data(); - common::ParallelFor(t.Size(), n_threads, [&](size_t i) { fn(i, ptr[i]); }); +template +void ElementWiseKernelHost(linalg::TensorView t, std::int32_t n_threads, Fn &&fn) { + if constexpr (D == 1) { + common::ParallelFor(t.Size(), n_threads, [&](std::size_t i) { fn(i); }); + } else if (D == 2 && t.CContiguous() && t.Shape(0) > t.Shape(1) * 64) { + // Heuristic. Tall, c-contiguous matrix, + auto n_rows = t.Shape(0); + auto n_columns = t.Shape(1); + common::ParallelFor(n_rows, n_threads, [&](std::size_t i) { + for (std::size_t j = 0; j < n_columns; ++j) { + fn(i, j); + } + }); } else { - common::ParallelFor(t.Size(), n_threads, [&](size_t i) { - auto& v = detail::Apply(t, linalg::UnravelIndex(i, t.Shape())); - fn(i, v); + common::ParallelFor(t.Size(), n_threads, [&](std::size_t i) { + auto idx = linalg::UnravelIndex(i, t.Shape()); + std::apply(fn, idx); }); } } diff --git a/src/objective/hinge.cu b/src/objective/hinge.cu index dd9a19b13..fb51f325b 100644 --- a/src/objective/hinge.cu +++ b/src/objective/hinge.cu @@ -4,71 +4,85 @@ * \brief Provides an implementation of the hinge loss function * \author Henry Gouk */ -#include "xgboost/objective.h" -#include "xgboost/json.h" -#include "xgboost/span.h" -#include "xgboost/host_device_vector.h" +#include // for max +#include // for size_t +#include // for int32_t -#include "../common/math.h" -#include "../common/transform.h" -#include "../common/common.h" +#include "../common/common.h" // for Range +#if defined(XGBOOST_USE_CUDA) +#include "../common/linalg_op.cuh" +#endif +#include "../common/linalg_op.h" +#include "../common/optional_weight.h" // for OptionalWeights +#include "../common/transform.h" // for Transform +#include "init_estimation.h" // for FitIntercept +#include "xgboost/data.h" // for MetaInfo +#include "xgboost/host_device_vector.h" // HostDeviceVector +#include "xgboost/json.h" // for Json +#include "xgboost/linalg.h" // for UnravelIndex +#include "xgboost/span.h" // for Span namespace xgboost::obj { - #if defined(XGBOOST_USE_CUDA) DMLC_REGISTRY_FILE_TAG(hinge_obj_gpu); #endif // defined(XGBOOST_USE_CUDA) -class HingeObj : public ObjFunction { +class HingeObj : public FitIntercept { public: HingeObj() = default; - void Configure(Args const&) override {} + void Configure(Args const &) override {} ObjInfo Task() const override { return ObjInfo::kRegression; } - void GetGradient(const HostDeviceVector &preds, const MetaInfo &info, - std::int32_t /*iter*/, linalg::Matrix *out_gpair) override { - CHECK_NE(info.labels.Size(), 0U) << "label set cannot be empty"; - CHECK_EQ(preds.Size(), info.labels.Size()) - << "labels are not correctly provided" - << "preds.size=" << preds.Size() - << ", label.size=" << info.labels.Size(); - - const size_t ndata = preds.Size(); - const bool is_null_weight = info.weights_.Size() == 0; - if (!is_null_weight) { - CHECK_EQ(info.weights_.Size(), ndata) - << "Number of weights should be equal to number of data points."; - } - CHECK_EQ(info.labels.Shape(1), 1) << "Multi-target for `binary:hinge` is not yet supported."; - out_gpair->Reshape(ndata, 1); - common::Transform<>::Init( - [=] XGBOOST_DEVICE(size_t _idx, - common::Span _out_gpair, - common::Span _preds, - common::Span _labels, - common::Span _weights) { - bst_float p = _preds[_idx]; - bst_float w = is_null_weight ? 1.0f : _weights[_idx]; - bst_float y = _labels[_idx] * 2.0 - 1.0; - bst_float g, h; - if (p * y < 1.0) { - g = -y * w; - h = w; - } else { - g = 0.0; - h = std::numeric_limits::min(); - } - _out_gpair[_idx] = GradientPair(g, h); - }, - common::Range{0, static_cast(ndata)}, this->ctx_->Threads(), - ctx_->Device()).Eval( - out_gpair->Data(), &preds, info.labels.Data(), &info.weights_); + [[nodiscard]] bst_target_t Targets(MetaInfo const &info) const override { + // Multi-target regression. + return std::max(static_cast(1), info.labels.Shape(1)); } - void PredTransform(HostDeviceVector *io_preds) const override { + void GetGradient(HostDeviceVector const &preds, MetaInfo const &info, + std::int32_t /*iter*/, linalg::Matrix *out_gpair) override { + CheckInitInputs(info); + CHECK_EQ(info.labels.Size(), preds.Size()) << "Invalid shape of labels."; + if (!info.weights_.Empty()) { + CHECK_EQ(info.weights_.Size(), info.num_row_) + << "Number of weights should be equal to number of data points."; + } + + bst_target_t n_targets = this->Targets(info); + out_gpair->Reshape(info.num_row_, n_targets); + auto gpair = out_gpair->View(ctx_->Device()); + + preds.SetDevice(ctx_->Device()); + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, n_targets); + + auto labels = info.labels.View(ctx_->Device()); + + info.weights_.SetDevice(ctx_->Device()); + common::OptionalWeights weight{ctx_->IsCUDA() ? info.weights_.ConstDeviceSpan() + : info.weights_.ConstHostSpan()}; + + linalg::ElementWiseKernel(this->ctx_, labels, + [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + auto w = weight[i]; + + auto p = predt(i, j); + auto y = labels(i, j) * 2.0 - 1.0; + + float g, h; + if (p * y < 1.0) { + g = -y * w; + h = w; + } else { + g = 0.0; + h = std::numeric_limits::min(); + } + gpair(i, j) = GradientPair{g, h}; + }); + } + + void PredTransform(HostDeviceVector *io_preds) const override { common::Transform<>::Init( - [] XGBOOST_DEVICE(size_t _idx, common::Span _preds) { + [] XGBOOST_DEVICE(std::size_t _idx, common::Span _preds) { _preds[_idx] = _preds[_idx] > 0.0 ? 1.0 : 0.0; }, common::Range{0, static_cast(io_preds->Size()), 1}, this->ctx_->Threads(), @@ -76,12 +90,10 @@ class HingeObj : public ObjFunction { .Eval(io_preds); } - [[nodiscard]] const char* DefaultEvalMetric() const override { - return "error"; - } + [[nodiscard]] const char *DefaultEvalMetric() const override { return "error"; } - void SaveConfig(Json* p_out) const override { - auto& out = *p_out; + void SaveConfig(Json *p_out) const override { + auto &out = *p_out; out["name"] = String("binary:hinge"); } void LoadConfig(Json const &) override {} @@ -89,7 +101,7 @@ class HingeObj : public ObjFunction { // register the objective functions XGBOOST_REGISTER_OBJECTIVE(HingeObj, "binary:hinge") -.describe("Hinge loss. Expects labels to be in [0,1f]") -.set_body([]() { return new HingeObj(); }); + .describe("Hinge loss. Expects labels to be in [0,1f]") + .set_body([]() { return new HingeObj(); }); } // namespace xgboost::obj diff --git a/src/objective/quantile_obj.cu b/src/objective/quantile_obj.cu index 57f432c7f..15ec72f95 100644 --- a/src/objective/quantile_obj.cu +++ b/src/objective/quantile_obj.cu @@ -75,28 +75,25 @@ class QuantileRegression : public ObjFunction { : info.weights_.ConstHostSpan()}; preds.SetDevice(ctx_->Device()); - auto predt = linalg::MakeVec(&preds); - auto n_samples = info.num_row_; + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, n_targets); alpha_.SetDevice(ctx_->Device()); auto alpha = ctx_->IsCUDA() ? alpha_.ConstDeviceSpan() : alpha_.ConstHostSpan(); - linalg::ElementWiseKernel( - ctx_, gpair, [=] XGBOOST_DEVICE(std::size_t i, GradientPair const&) mutable { - auto [sample_id, quantile_id, target_id] = - linalg::UnravelIndex(i, n_samples, alpha.size(), n_targets / alpha.size()); - assert(target_id == 0); - - auto d = predt(i) - labels(sample_id, target_id); - auto h = weight[sample_id]; - if (d >= 0) { - auto g = (1.0f - alpha[quantile_id]) * weight[sample_id]; - gpair(sample_id, quantile_id) = GradientPair{g, h}; - } else { - auto g = (-alpha[quantile_id] * weight[sample_id]); - gpair(sample_id, quantile_id) = GradientPair{g, h}; - } - }); + linalg::ElementWiseKernel(ctx_, gpair, + [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + // j is the quantile index + // 0 is the target index + auto d = predt(i, j) - labels(i, 0); + auto h = weight[i]; + if (d >= 0) { + auto g = (1.0f - alpha[j]) * weight[i]; + gpair(i, j) = GradientPair{g, h}; + } else { + auto g = (-alpha[j] * weight[i]); + gpair(i, j) = GradientPair{g, h}; + } + }); } void InitEstimation(MetaInfo const& info, linalg::Vector* base_score) const override { diff --git a/src/objective/regression_obj.cu b/src/objective/regression_obj.cu index f74d01acc..5627600fc 100644 --- a/src/objective/regression_obj.cu +++ b/src/objective/regression_obj.cu @@ -255,24 +255,24 @@ class PseudoHuberRegression : public FitIntercept { auto gpair = out_gpair->View(ctx_->Device()); preds.SetDevice(ctx_->Device()); - auto predt = linalg::MakeVec(&preds); + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, this->Targets(info)); info.weights_.SetDevice(ctx_->Device()); common::OptionalWeights weight{ctx_->IsCUDA() ? info.weights_.ConstDeviceSpan() : info.weights_.ConstHostSpan()}; - linalg::ElementWiseKernel(ctx_, labels, [=] XGBOOST_DEVICE(size_t i, float const y) mutable { - auto sample_id = std::get<0>(linalg::UnravelIndex(i, labels.Shape())); - const float z = predt(i) - y; - const float scale_sqrt = std::sqrt(1 + common::Sqr(z) / common::Sqr(slope)); - float grad = z / scale_sqrt; + linalg::ElementWiseKernel( + ctx_, labels, [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + float z = predt(i, j) - labels(i, j); + float scale_sqrt = std::sqrt(1 + common::Sqr(z) / common::Sqr(slope)); + float grad = z / scale_sqrt; - auto scale = common::Sqr(slope) + common::Sqr(z); - float hess = common::Sqr(slope) / (scale * scale_sqrt); + auto scale = common::Sqr(slope) + common::Sqr(z); + float hess = common::Sqr(slope) / (scale * scale_sqrt); - auto w = weight[sample_id]; - gpair(i) = {grad * w, hess * w}; - }); + auto w = weight[i]; + gpair(i) = {grad * w, hess * w}; + }); } [[nodiscard]] const char* DefaultEvalMetric() const override { return "mphe"; } @@ -635,20 +635,21 @@ class MeanAbsoluteError : public ObjFunction { auto gpair = out_gpair->View(ctx_->Device()); preds.SetDevice(ctx_->Device()); - auto predt = linalg::MakeVec(&preds); + auto predt = linalg::MakeTensorView(ctx_, &preds, info.num_row_, this->Targets(info)); info.weights_.SetDevice(ctx_->Device()); common::OptionalWeights weight{ctx_->IsCUDA() ? info.weights_.ConstDeviceSpan() : info.weights_.ConstHostSpan()}; - linalg::ElementWiseKernel(ctx_, labels, [=] XGBOOST_DEVICE(std::size_t i, float y) mutable { - auto sign = [](auto x) { - return (x > static_cast(0)) - (x < static_cast(0)); - }; - auto [sample_id, target_id] = linalg::UnravelIndex(i, labels.Shape()); - auto grad = sign(predt(i) - y) * weight[sample_id]; - auto hess = weight[sample_id]; - gpair(sample_id, target_id) = GradientPair{grad, hess}; - }); + linalg::ElementWiseKernel( + ctx_, labels, [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { + auto sign = [](auto x) { + return (x > static_cast(0)) - (x < static_cast(0)); + }; + auto y = labels(i, j); + auto hess = weight[i]; + auto grad = sign(predt(i, j) - y) * hess; + gpair(i, j) = GradientPair{grad, hess}; + }); } void InitEstimation(MetaInfo const& info, linalg::Tensor* base_margin) const override { diff --git a/tests/cpp/common/test_linalg.cu b/tests/cpp/common/test_linalg.cu index 4823b1191..5f8bab4a3 100644 --- a/tests/cpp/common/test_linalg.cu +++ b/tests/cpp/common/test_linalg.cu @@ -23,7 +23,7 @@ void TestElementWiseKernel() { ElementWiseTransformDevice(t, [] __device__(size_t i, float) { return i; }); // CPU view t = l.View(DeviceOrd::CPU()).Slice(linalg::All(), 1, linalg::All()); - size_t k = 0; + std::size_t k = 0; for (size_t i = 0; i < l.Shape(0); ++i) { for (size_t j = 0; j < l.Shape(2); ++j) { ASSERT_EQ(k++, t(i, j)); @@ -31,7 +31,15 @@ void TestElementWiseKernel() { } t = l.View(device).Slice(linalg::All(), 1, linalg::All()); - ElementWiseKernelDevice(t, [] XGBOOST_DEVICE(size_t i, float v) { SPAN_CHECK(v == i); }); + cuda_impl::ElementWiseKernel( + t, [=] XGBOOST_DEVICE(std::size_t i, std::size_t j) mutable { t(i, j) = i + j; }); + + t = l.Slice(linalg::All(), 1, linalg::All()); + for (size_t i = 0; i < l.Shape(0); ++i) { + for (size_t j = 0; j < l.Shape(2); ++j) { + ASSERT_EQ(i + j, t(i, j)); + } + } } { diff --git a/tests/cpp/data/test_metainfo.h b/tests/cpp/data/test_metainfo.h index fba882e0e..92cd6cb91 100644 --- a/tests/cpp/data/test_metainfo.h +++ b/tests/cpp/data/test_metainfo.h @@ -31,12 +31,10 @@ inline void TestMetaInfoStridedData(DeviceOrd device) { auto const& h_result = info.labels.View(DeviceOrd::CPU()); ASSERT_EQ(h_result.Shape().size(), 2); auto in_labels = labels.View(DeviceOrd::CPU()); - linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), [&](size_t i, float& v_0) { - auto tup = linalg::UnravelIndex(i, h_result.Shape()); - auto i0 = std::get<0>(tup); - auto i1 = std::get<1>(tup); + linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), [&](size_t i, std::size_t j) { // Sliced at second dimension. - auto v_1 = in_labels(i0, 0, i1); + auto v_0 = h_result(i, j); + auto v_1 = in_labels(i, 0, j); CHECK_EQ(v_0, v_1); }); } @@ -65,14 +63,13 @@ inline void TestMetaInfoStridedData(DeviceOrd device) { auto const& h_result = info.base_margin_.View(DeviceOrd::CPU()); ASSERT_EQ(h_result.Shape().size(), 2); auto in_margin = base_margin.View(DeviceOrd::CPU()); - linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), [&](size_t i, float v_0) { - auto tup = linalg::UnravelIndex(i, h_result.Shape()); - auto i0 = std::get<0>(tup); - auto i1 = std::get<1>(tup); - // Sliced at second dimension. - auto v_1 = in_margin(i0, 0, i1); - CHECK_EQ(v_0, v_1); - }); + linalg::ElementWiseKernelHost(h_result, omp_get_max_threads(), + [&](std::size_t i, std::size_t j) { + // Sliced at second dimension. + auto v_0 = h_result(i, j); + auto v_1 = in_margin(i, 0, j); + CHECK_EQ(v_0, v_1); + }); } } } // namespace xgboost diff --git a/tests/cpp/objective/test_hinge.cc b/tests/cpp/objective/test_hinge.cc index 17d2609d4..70e8b5626 100644 --- a/tests/cpp/objective/test_hinge.cc +++ b/tests/cpp/objective/test_hinge.cc @@ -1,28 +1,55 @@ -// Copyright by Contributors +/** + * Copyright 2018-2023, XGBoost Contributors + */ #include #include #include #include "../helpers.h" +#include "../../../src/common/linalg_op.h" namespace xgboost { TEST(Objective, DeclareUnifiedTest(HingeObj)) { Context ctx = MakeCUDACtx(GPUIDX); std::unique_ptr obj{ObjFunction::Create("binary:hinge", &ctx)}; float eps = std::numeric_limits::min(); - CheckObjFunction(obj, - {-1.0f, -0.5f, 0.5f, 1.0f, -1.0f, -0.5f, 0.5f, 1.0f}, - { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}, - { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, - { 0.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f}, - { eps, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, eps }); - CheckObjFunction(obj, - {-1.0f, -0.5f, 0.5f, 1.0f, -1.0f, -0.5f, 0.5f, 1.0f}, - { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}, - {}, // Empty weight. - { 0.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f}, - { eps, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, eps }); + std::vector predt{-1.0f, -0.5f, 0.5f, 1.0f, -1.0f, -0.5f, 0.5f, 1.0f}; + std::vector label{ 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + std::vector grad{0.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f}; + std::vector hess{eps, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, eps}; - ASSERT_NO_THROW(obj->DefaultEvalMetric()); + CheckObjFunction(obj, predt, label, {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, grad, hess); + CheckObjFunction(obj, predt, label, {/* Empty weight. */}, grad, hess); + + ASSERT_EQ(obj->DefaultEvalMetric(), StringView{"error"}); + + MetaInfo info; + info.num_row_ = label.size(); + info.labels.Reshape(info.num_row_, 3); + ASSERT_EQ(obj->Targets(info), 3); + auto h_labels = info.labels.HostView(); + for (std::size_t j = 0; j < obj->Targets(info); ++j) { + for (std::size_t i = 0; i < info.num_row_; ++i) { + h_labels(i, j) = label[i]; + } + } + linalg::Tensor t_predt{}; + t_predt.Reshape(info.labels.Shape()); + for (std::size_t j = 0; j < obj->Targets(info); ++j) { + for (std::size_t i = 0; i < info.num_row_; ++i) { + t_predt(i, j) = predt[i]; + } + } + linalg::Matrix out_gpair; + obj->GetGradient(*t_predt.Data(), info, 0, &out_gpair); + + for (std::size_t j = 0; j < obj->Targets(info); ++j) { + auto gh = out_gpair.Slice(linalg::All(), j); + ASSERT_EQ(gh.Size(), info.num_row_); + for (std::size_t i = 0; i < gh.Size(); ++i) { + ASSERT_EQ(gh(i).GetGrad(), grad[i]); + ASSERT_EQ(gh(i).GetHess(), hess[i]); + } + } } } // namespace xgboost From 1094d6015df7be6f5829e53a4c257f3481b43ee5 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 8 Dec 2023 17:23:16 +0800 Subject: [PATCH 041/109] [py] Use the first found native library. (#9860) --- python-package/xgboost/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index ffc7db8dd..bf72d95cc 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -211,6 +211,7 @@ def _load_lib() -> ctypes.CDLL: lib = ctypes.cdll.LoadLibrary(lib_path) setattr(lib, "path", os.path.normpath(lib_path)) lib_success = True + break except OSError as e: os_error_list.append(str(e)) continue From 562352101d5dd1630eded1522b9b4ed9b07e8692 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 9 Dec 2023 19:45:28 +0100 Subject: [PATCH 042/109] [R] Move all DMatrix fields to function arguments (#9862) --- R-package/R/xgb.DMatrix.R | 148 +++++++++++++----- R-package/man/getinfo.Rd | 18 ++- R-package/man/setinfo.Rd | 14 +- R-package/man/xgb.DMatrix.Rd | 35 ++++- R-package/src/init.c | 6 +- R-package/src/xgboost_R.cc | 21 ++- R-package/src/xgboost_R.h | 12 +- R-package/tests/testthat/test_basic.R | 31 +++- R-package/tests/testthat/test_dmatrix.R | 17 ++ .../testthat/test_interaction_constraints.R | 2 +- 10 files changed, 236 insertions(+), 68 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 0d9a89912..9be990670 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -8,13 +8,24 @@ #' a \code{dgRMatrix} object, #' a \code{dsparseVector} object (only when making predictions from a fitted model, will be #' interpreted as a row vector), or a character string representing a filename. -#' @param info a named list of additional information to store in the \code{xgb.DMatrix} object. -#' See \code{\link{setinfo}} for the specific allowed kinds of +#' @param label Label of the training data. +#' @param weight Weight for each instance. +#' +#' Note that, for ranking task, weights are per-group. In ranking task, one weight +#' is assigned to each group (not each data point). This is because we +#' only care about the relative ordering of data points within each group, +#' so it doesn't make sense to assign weights to individual data points. +#' @param base_margin Base margin used for boosting from existing model. #' @param missing a float value to represents missing values in data (used only when input is a dense matrix). #' It is useful when a 0 or some other extreme value represents missing values in data. #' @param silent whether to suppress printing an informational message after loading from a file. +#' @param feature_names Set names for features. #' @param nthread Number of threads used for creating DMatrix. -#' @param ... the \code{info} data could be passed directly as parameters, without creating an \code{info} list. +#' @param group Group size for all ranking group. +#' @param qid Query ID for data samples, used for ranking. +#' @param label_lower_bound Lower bound for survival training. +#' @param label_upper_bound Upper bound for survival training. +#' @param feature_weights Set feature weights for column sampling. #' #' @details #' Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. @@ -34,8 +45,24 @@ #' dtrain <- xgb.DMatrix('xgb.DMatrix.data') #' if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') #' @export -xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthread = NULL, ...) { - cnames <- NULL +xgb.DMatrix <- function( + data, + label = NULL, + weight = NULL, + base_margin = NULL, + missing = NA, + silent = FALSE, + feature_names = colnames(data), + nthread = NULL, + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL +) { + if (!is.null(group) && !is.null(qid)) { + stop("Either one of 'group' or 'qid' should be NULL") + } if (typeof(data) == "character") { if (length(data) > 1) stop("'data' has class 'character' and length ", length(data), @@ -44,7 +71,6 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre handle <- .Call(XGDMatrixCreateFromFile_R, data, as.integer(silent)) } else if (is.matrix(data)) { handle <- .Call(XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1))) - cnames <- colnames(data) } else if (inherits(data, "dgCMatrix")) { handle <- .Call( XGDMatrixCreateFromCSC_R, @@ -55,7 +81,6 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre missing, as.integer(NVL(nthread, -1)) ) - cnames <- colnames(data) } else if (inherits(data, "dgRMatrix")) { handle <- .Call( XGDMatrixCreateFromCSR_R, @@ -66,7 +91,6 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre missing, as.integer(NVL(nthread, -1)) ) - cnames <- colnames(data) } else if (inherits(data, "dsparseVector")) { indptr <- c(0L, as.integer(length(data@i))) ind <- as.integer(data@i) - 1L @@ -82,17 +106,38 @@ xgb.DMatrix <- function(data, info = list(), missing = NA, silent = FALSE, nthre } else { stop("xgb.DMatrix does not support construction from ", typeof(data)) } + dmat <- handle attributes(dmat) <- list(class = "xgb.DMatrix") - if (!is.null(cnames)) { - setinfo(dmat, "feature_name", cnames) + + if (!is.null(label)) { + setinfo(dmat, "label", label) + } + if (!is.null(weight)) { + setinfo(dmat, "weight", weight) + } + if (!is.null(base_margin)) { + setinfo(dmat, "base_margin", base_margin) + } + if (!is.null(feature_names)) { + setinfo(dmat, "feature_name", feature_names) + } + if (!is.null(group)) { + setinfo(dmat, "group", group) + } + if (!is.null(qid)) { + setinfo(dmat, "qid", qid) + } + if (!is.null(label_lower_bound)) { + setinfo(dmat, "label_lower_bound", label_lower_bound) + } + if (!is.null(label_upper_bound)) { + setinfo(dmat, "label_upper_bound", label_upper_bound) + } + if (!is.null(feature_weights)) { + setinfo(dmat, "feature_weights", feature_weights) } - info <- append(info, list(...)) - for (i in seq_along(info)) { - p <- info[i] - setinfo(dmat, names(p), p[[1]]) - } return(dmat) } @@ -211,14 +256,20 @@ dimnames.xgb.DMatrix <- function(x) { #' The \code{name} field can be one of the following: #' #' \itemize{ -#' \item \code{label}: label XGBoost learn from ; -#' \item \code{weight}: to do a weight rescale ; -#' \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; -#' \item \code{nrow}: number of rows of the \code{xgb.DMatrix}. -#' +#' \item \code{label} +#' \item \code{weight} +#' \item \code{base_margin} +#' \item \code{label_lower_bound} +#' \item \code{label_upper_bound} +#' \item \code{group} +#' \item \code{feature_type} +#' \item \code{feature_name} +#' \item \code{nrow} #' } +#' See the documentation for \link{xgb.DMatrix} for more information about these fields. #' -#' \code{group} can be setup by \code{setinfo} but can't be retrieved by \code{getinfo}. +#' Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' +#' for a DMatrix that had 'qid' assigned. #' #' @examples #' data(agaricus.train, package='xgboost') @@ -236,24 +287,37 @@ getinfo <- function(object, ...) UseMethod("getinfo") #' @rdname getinfo #' @export getinfo.xgb.DMatrix <- function(object, name, ...) { + allowed_int_fields <- 'group' + allowed_float_fields <- c( + 'label', 'weight', 'base_margin', + 'label_lower_bound', 'label_upper_bound' + ) + allowed_str_fields <- c("feature_type", "feature_name") + allowed_fields <- c(allowed_float_fields, allowed_int_fields, allowed_str_fields, 'nrow') + if (typeof(name) != "character" || length(name) != 1 || - !name %in% c('label', 'weight', 'base_margin', 'nrow', - 'label_lower_bound', 'label_upper_bound', "feature_type", "feature_name")) { - stop( - "getinfo: name must be one of the following\n", - " 'label', 'weight', 'base_margin', 'nrow', 'label_lower_bound', 'label_upper_bound', 'feature_type', 'feature_name'" - ) + !name %in% allowed_fields) { + stop("getinfo: name must be one of the following\n", + paste(paste0("'", allowed_fields, "'"), collapse = ", ")) } - if (name == "feature_name" || name == "feature_type") { + if (name == "nrow") { + ret <- nrow(object) + } else if (name %in% allowed_str_fields) { ret <- .Call(XGDMatrixGetStrFeatureInfo_R, object, name) - } else if (name != "nrow") { - ret <- .Call(XGDMatrixGetInfo_R, object, name) + } else if (name %in% allowed_float_fields) { + ret <- .Call(XGDMatrixGetFloatInfo_R, object, name) + if (length(ret) > nrow(object)) { + ret <- matrix(ret, nrow = nrow(object), byrow = TRUE) + } + } else if (name %in% allowed_int_fields) { + if (name == "group") { + name <- "group_ptr" + } + ret <- .Call(XGDMatrixGetUIntInfo_R, object, name) if (length(ret) > nrow(object)) { ret <- matrix(ret, nrow = nrow(object), byrow = TRUE) } - } else { - ret <- nrow(object) } if (length(ret) == 0) return(NULL) return(ret) @@ -270,13 +334,15 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' @param ... other parameters #' #' @details -#' The \code{name} field can be one of the following: +#' See the documentation for \link{xgb.DMatrix} for possible fields that can be set +#' (which correspond to arguments in that function). #' -#' \itemize{ -#' \item \code{label}: label XGBoost learn from ; -#' \item \code{weight}: to do a weight rescale ; -#' \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; -#' \item \code{group}: number of rows in each group (to use with \code{rank:pairwise} objective). +#' Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} +#' but \bold{aren't} allowed here:\itemize{ +#' \item data +#' \item missing +#' \item silent +#' \item nthread #' } #' #' @examples @@ -328,6 +394,12 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) return(TRUE) } + if (name == "qid") { + if (NROW(info) != nrow(object)) + stop("The length of qid assignments must equal to the number of rows in the input data") + .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) + return(TRUE) + } if (name == "feature_weights") { if (length(info) != ncol(object)) { stop("The number of feature weights must equal to the number of columns in the input data") diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index 63222f341..9503c2154 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -23,14 +23,20 @@ Get information of an xgb.DMatrix object The \code{name} field can be one of the following: \itemize{ - \item \code{label}: label XGBoost learn from ; - \item \code{weight}: to do a weight rescale ; - \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; - \item \code{nrow}: number of rows of the \code{xgb.DMatrix}. - + \item \code{label} + \item \code{weight} + \item \code{base_margin} + \item \code{label_lower_bound} + \item \code{label_upper_bound} + \item \code{group} + \item \code{feature_type} + \item \code{feature_name} + \item \code{nrow} } +See the documentation for \link{xgb.DMatrix} for more information about these fields. -\code{group} can be setup by \code{setinfo} but can't be retrieved by \code{getinfo}. +Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' +for a DMatrix that had 'qid' assigned. } \examples{ data(agaricus.train, package='xgboost') diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd index c29fcfa7e..a8bc56b02 100644 --- a/R-package/man/setinfo.Rd +++ b/R-package/man/setinfo.Rd @@ -22,13 +22,15 @@ setinfo(object, ...) Set information of an xgb.DMatrix object } \details{ -The \code{name} field can be one of the following: +See the documentation for \link{xgb.DMatrix} for possible fields that can be set +(which correspond to arguments in that function). -\itemize{ - \item \code{label}: label XGBoost learn from ; - \item \code{weight}: to do a weight rescale ; - \item \code{base_margin}: base margin is the base prediction XGBoost will boost from ; - \item \code{group}: number of rows in each group (to use with \code{rank:pairwise} objective). +Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} +but \bold{aren't} allowed here:\itemize{ +\item data +\item missing +\item silent +\item nthread } } \examples{ diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 96f050c72..13dc3d9f5 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -6,11 +6,18 @@ \usage{ xgb.DMatrix( data, - info = list(), + label = NULL, + weight = NULL, + base_margin = NULL, missing = NA, silent = FALSE, + feature_names = colnames(data), nthread = NULL, - ... + group = NULL, + qid = NULL, + label_lower_bound = NULL, + label_upper_bound = NULL, + feature_weights = NULL ) } \arguments{ @@ -19,17 +26,35 @@ a \code{dgRMatrix} object, a \code{dsparseVector} object (only when making predictions from a fitted model, will be interpreted as a row vector), or a character string representing a filename.} -\item{info}{a named list of additional information to store in the \code{xgb.DMatrix} object. -See \code{\link{setinfo}} for the specific allowed kinds of} +\item{label}{Label of the training data.} + +\item{weight}{Weight for each instance. + +Note that, for ranking task, weights are per-group. In ranking task, one weight +is assigned to each group (not each data point). This is because we +only care about the relative ordering of data points within each group, +so it doesn't make sense to assign weights to individual data points.} + +\item{base_margin}{Base margin used for boosting from existing model.} \item{missing}{a float value to represents missing values in data (used only when input is a dense matrix). It is useful when a 0 or some other extreme value represents missing values in data.} \item{silent}{whether to suppress printing an informational message after loading from a file.} +\item{feature_names}{Set names for features.} + \item{nthread}{Number of threads used for creating DMatrix.} -\item{...}{the \code{info} data could be passed directly as parameters, without creating an \code{info} list.} +\item{group}{Group size for all ranking group.} + +\item{qid}{Query ID for data samples, used for ranking.} + +\item{label_lower_bound}{Lower bound for survival training.} + +\item{label_upper_bound}{Upper bound for survival training.} + +\item{feature_weights}{Set feature weights for column sampling.} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/R-package/src/init.c b/R-package/src/init.c index afac524e3..c35e0ecf5 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -39,7 +39,8 @@ extern SEXP XGDMatrixCreateFromCSC_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP XGDMatrixCreateFromCSR_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP XGDMatrixCreateFromFile_R(SEXP, SEXP); extern SEXP XGDMatrixCreateFromMat_R(SEXP, SEXP, SEXP); -extern SEXP XGDMatrixGetInfo_R(SEXP, SEXP); +extern SEXP XGDMatrixGetFloatInfo_R(SEXP, SEXP); +extern SEXP XGDMatrixGetUIntInfo_R(SEXP, SEXP); extern SEXP XGDMatrixGetStrFeatureInfo_R(SEXP, SEXP); extern SEXP XGDMatrixNumCol_R(SEXP); extern SEXP XGDMatrixNumRow_R(SEXP); @@ -76,7 +77,8 @@ static const R_CallMethodDef CallEntries[] = { {"XGDMatrixCreateFromCSR_R", (DL_FUNC) &XGDMatrixCreateFromCSR_R, 6}, {"XGDMatrixCreateFromFile_R", (DL_FUNC) &XGDMatrixCreateFromFile_R, 2}, {"XGDMatrixCreateFromMat_R", (DL_FUNC) &XGDMatrixCreateFromMat_R, 3}, - {"XGDMatrixGetInfo_R", (DL_FUNC) &XGDMatrixGetInfo_R, 2}, + {"XGDMatrixGetFloatInfo_R", (DL_FUNC) &XGDMatrixGetFloatInfo_R, 2}, + {"XGDMatrixGetUIntInfo_R", (DL_FUNC) &XGDMatrixGetUIntInfo_R, 2}, {"XGDMatrixGetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixGetStrFeatureInfo_R, 2}, {"XGDMatrixNumCol_R", (DL_FUNC) &XGDMatrixNumCol_R, 1}, {"XGDMatrixNumRow_R", (DL_FUNC) &XGDMatrixNumRow_R, 1}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 214ca6cb4..9bebc5851 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -412,17 +413,27 @@ XGB_DLL SEXP XGDMatrixGetStrFeatureInfo_R(SEXP handle, SEXP field) { return ret; } -XGB_DLL SEXP XGDMatrixGetInfo_R(SEXP handle, SEXP field) { +XGB_DLL SEXP XGDMatrixGetFloatInfo_R(SEXP handle, SEXP field) { SEXP ret; R_API_BEGIN(); bst_ulong olen; const float *res; CHECK_CALL(XGDMatrixGetFloatInfo(R_ExternalPtrAddr(handle), CHAR(asChar(field)), &olen, &res)); ret = PROTECT(allocVector(REALSXP, olen)); - double *ret_ = REAL(ret); - for (size_t i = 0; i < olen; ++i) { - ret_[i] = res[i]; - } + std::copy(res, res + olen, REAL(ret)); + R_API_END(); + UNPROTECT(1); + return ret; +} + +XGB_DLL SEXP XGDMatrixGetUIntInfo_R(SEXP handle, SEXP field) { + SEXP ret; + R_API_BEGIN(); + bst_ulong olen; + const unsigned *res; + CHECK_CALL(XGDMatrixGetUIntInfo(R_ExternalPtrAddr(handle), CHAR(asChar(field)), &olen, &res)); + ret = PROTECT(allocVector(INTSXP, olen)); + std::copy(res, res + olen, INTEGER(ret)); R_API_END(); UNPROTECT(1); return ret; diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 7f0833b15..4ec80b5ff 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -106,12 +106,20 @@ XGB_DLL SEXP XGDMatrixSaveBinary_R(SEXP handle, SEXP fname, SEXP silent); XGB_DLL SEXP XGDMatrixSetInfo_R(SEXP handle, SEXP field, SEXP array); /*! - * \brief get info vector from matrix + * \brief get info vector (float type) from matrix * \param handle a instance of data matrix * \param field field name * \return info vector */ -XGB_DLL SEXP XGDMatrixGetInfo_R(SEXP handle, SEXP field); +XGB_DLL SEXP XGDMatrixGetFloatInfo_R(SEXP handle, SEXP field); + +/*! + * \brief get info vector (uint type) from matrix + * \param handle a instance of data matrix + * \param field field name + * \return info vector + */ +XGB_DLL SEXP XGDMatrixGetUIntInfo_R(SEXP handle, SEXP field); /*! * \brief return number of rows diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index c96871e11..8ecf86e87 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -56,7 +56,7 @@ test_that("parameter validation works", { y <- d[, "x1"] + d[, "x2"]^2 + ifelse(d[, "x3"] > .5, d[, "x3"]^2, 2^d[, "x3"]) + rnorm(10) - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) correct <- function() { params <- list( @@ -124,7 +124,7 @@ test_that("dart prediction works", { expect_false(all(matrix(pred_by_xgboost_0, byrow = TRUE) == matrix(pred_by_xgboost_2, byrow = TRUE))) set.seed(1994) - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) booster_by_train <- xgb.train( params = list( booster = "dart", @@ -186,7 +186,7 @@ test_that("train and predict softprob", { x3 = rnorm(100) ) y <- sample.int(10, 100, replace = TRUE) - 1 - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) booster <- xgb.train( params = list(tree_method = "hist", nthread = n_threads), data = dtrain, nrounds = 4, num_class = 10, @@ -643,3 +643,28 @@ test_that("Can use multi-output labels with custom objectives", { expect_true(cor(y, pred[, 1]) > 0.9) expect_true(cor(y, pred[, 2]) < -0.9) }) + +test_that("Can use ranking objectives with either 'qid' or 'group'", { + set.seed(123) + x <- matrix(rnorm(100 * 10), nrow = 100) + y <- sample(2, size = 100, replace = TRUE) - 1 + qid <- c(rep(1, 20), rep(2, 20), rep(3, 60)) + gr <- c(20, 20, 60) + + dmat_qid <- xgb.DMatrix(x, label = y, qid = qid) + dmat_gr <- xgb.DMatrix(x, label = y, group = gr) + + params <- list(tree_method = "hist", + lambdarank_num_pair_per_sample = 8, + objective = "rank:ndcg", + lambdarank_pair_method = "topk", + nthread = n_threads) + set.seed(123) + model_qid <- xgb.train(params, dmat_qid, nrounds = 5) + set.seed(123) + model_gr <- xgb.train(params, dmat_gr, nrounds = 5) + + pred_qid <- predict(model_qid, x) + pred_gr <- predict(model_gr, x) + expect_equal(pred_qid, pred_gr) +}) diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 4db7aad08..f1eaa9d80 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -305,3 +305,20 @@ test_that("xgb.DMatrix: error on three-dimensional array", { dim(y) <- c(50, 4, 2) expect_error(xgb.DMatrix(data = x, label = y)) }) + +test_that("xgb.DMatrix: can get group for both 'qid' and 'group' constructors", { + set.seed(123) + x <- matrix(rnorm(1000), nrow = 100) + group <- c(20, 20, 60) + qid <- c(rep(1, 20), rep(2, 20), rep(3, 60)) + + gr_mat <- xgb.DMatrix(x, group = group) + qid_mat <- xgb.DMatrix(x, qid = qid) + + info_gr <- getinfo(gr_mat, "group") + info_qid <- getinfo(qid_mat, "group") + expect_equal(info_gr, info_qid) + + expected_gr <- c(0, 20, 40, 100) + expect_equal(info_gr, expected_gr) +}) diff --git a/R-package/tests/testthat/test_interaction_constraints.R b/R-package/tests/testthat/test_interaction_constraints.R index 1ac804501..ee4c453b3 100644 --- a/R-package/tests/testthat/test_interaction_constraints.R +++ b/R-package/tests/testthat/test_interaction_constraints.R @@ -47,7 +47,7 @@ test_that("interaction constraints scientific representation", { d <- matrix(rexp(rows, rate = .1), nrow = rows, ncol = cols) y <- rnorm(rows) - dtrain <- xgb.DMatrix(data = d, info = list(label = y), nthread = n_threads) + dtrain <- xgb.DMatrix(data = d, label = y, nthread = n_threads) inc <- list(c(seq.int(from = 0, to = cols, by = 1))) with_inc <- xgb.train( From b3700bbb3fc057f4e6948a493b92c3e6ec28cc51 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 12 Dec 2023 07:34:01 +0800 Subject: [PATCH 043/109] Flexible find protobuf. (#9867) --- plugin/federated/CMakeLists.txt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin/federated/CMakeLists.txt b/plugin/federated/CMakeLists.txt index c4d5ea378..0865e756e 100644 --- a/plugin/federated/CMakeLists.txt +++ b/plugin/federated/CMakeLists.txt @@ -1,7 +1,16 @@ # gRPC needs to be installed first. See README.md. set(protobuf_MODULE_COMPATIBLE TRUE) set(protobuf_BUILD_SHARED_LIBS TRUE) -find_package(Protobuf CONFIG REQUIRED) + +find_package(Protobuf CONFIG) +if(NOT Protobuf_FOUND) + find_package(Protobuf) +endif() +if(NOT Protobuf_FOUND) + # let CMake emit error + find_package(Protobuf CONFIG REQUIRED) +endif() + find_package(gRPC CONFIG REQUIRED) message(STATUS "Found gRPC: ${gRPC_CONFIG}") From faf0f2df10ea2583063942b273b7cbaea5644696 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 12 Dec 2023 09:56:31 +0800 Subject: [PATCH 044/109] Support dataframe data format in native XGBoost. (#9828) - Implement a columnar adapter. - Refactor Python pandas handling code to avoid converting into a single numpy array. - Add support in R for transforming columns. - Support R data.frame and factor type. --- R-package/R/xgb.DMatrix.R | 58 ++++- R-package/man/xgb.DMatrix.Rd | 9 +- R-package/src/init.c | 2 + R-package/src/xgboost_R.cc | 64 +++++ R-package/src/xgboost_R.h | 10 + R-package/tests/testthat/test_dmatrix.R | 27 ++ demo/guide-python/cat_in_the_dat.py | 4 + include/xgboost/c_api.h | 45 ++++ python-package/xgboost/core.py | 23 +- python-package/xgboost/data.py | 327 +++++++++++++++--------- src/c_api/c_api.cc | 76 +++++- src/common/quantile.cc | 2 + src/data/adapter.h | 107 ++++++-- src/data/data.cc | 54 ++-- src/data/gradient_index.cc | 2 +- src/data/proxy_dmatrix.cc | 15 ++ src/data/proxy_dmatrix.h | 13 + src/data/simple_dmatrix.cc | 4 +- src/predictor/cpu_predictor.cc | 3 + tests/python/test_with_modin.py | 27 +- tests/python/test_with_pandas.py | 67 ++++- 21 files changed, 718 insertions(+), 221 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 9be990670..602164afe 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -19,7 +19,8 @@ #' @param missing a float value to represents missing values in data (used only when input is a dense matrix). #' It is useful when a 0 or some other extreme value represents missing values in data. #' @param silent whether to suppress printing an informational message after loading from a file. -#' @param feature_names Set names for features. +#' @param feature_names Set names for features. Overrides column names in data +#' frame and matrix. #' @param nthread Number of threads used for creating DMatrix. #' @param group Group size for all ranking group. #' @param qid Query ID for data samples, used for ranking. @@ -32,6 +33,8 @@ #' If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching #' chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed #' from the original source of data. +#' @param enable_categorical Experimental support of specializing for +#' categorical features. JSON/UBJSON serialization format is required. #' #' @examples #' data(agaricus.train, package='xgboost') @@ -58,19 +61,26 @@ xgb.DMatrix <- function( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL + feature_weights = NULL, + enable_categorical = FALSE ) { if (!is.null(group) && !is.null(qid)) { stop("Either one of 'group' or 'qid' should be NULL") } + ctypes <- NULL if (typeof(data) == "character") { - if (length(data) > 1) - stop("'data' has class 'character' and length ", length(data), - ".\n 'data' accepts either a numeric matrix or a single filename.") + if (length(data) > 1) { + stop( + "'data' has class 'character' and length ", length(data), + ".\n 'data' accepts either a numeric matrix or a single filename." + ) + } data <- path.expand(data) handle <- .Call(XGDMatrixCreateFromFile_R, data, as.integer(silent)) } else if (is.matrix(data)) { - handle <- .Call(XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1))) + handle <- .Call( + XGDMatrixCreateFromMat_R, data, missing, as.integer(NVL(nthread, -1)) + ) } else if (inherits(data, "dgCMatrix")) { handle <- .Call( XGDMatrixCreateFromCSC_R, @@ -103,6 +113,39 @@ xgb.DMatrix <- function( missing, as.integer(NVL(nthread, -1)) ) + } else if (is.data.frame(data)) { + ctypes <- sapply(data, function(x) { + if (is.factor(x)) { + if (!enable_categorical) { + stop( + "When factor type is used, the parameter `enable_categorical`", + " must be set to TRUE." + ) + } + "c" + } else if (is.integer(x)) { + "int" + } else if (is.logical(x)) { + "i" + } else { + if (!is.numeric(x)) { + stop("Invalid type in dataframe.") + } + "float" + } + }) + ## as.data.frame somehow converts integer/logical into real. + data <- as.data.frame(sapply(data, function(x) { + if (is.factor(x)) { + ## XGBoost uses 0-based indexing. + as.numeric(x) - 1 + } else { + x + } + })) + handle <- .Call( + XGDMatrixCreateFromDF_R, data, missing, as.integer(NVL(nthread, -1)) + ) } else { stop("xgb.DMatrix does not support construction from ", typeof(data)) } @@ -137,6 +180,9 @@ xgb.DMatrix <- function( if (!is.null(feature_weights)) { setinfo(dmat, "feature_weights", feature_weights) } + if (!is.null(ctypes)) { + setinfo(dmat, "feature_type", ctypes) + } return(dmat) } diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 13dc3d9f5..619f5d730 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -17,7 +17,8 @@ xgb.DMatrix( qid = NULL, label_lower_bound = NULL, label_upper_bound = NULL, - feature_weights = NULL + feature_weights = NULL, + enable_categorical = FALSE ) } \arguments{ @@ -42,7 +43,8 @@ It is useful when a 0 or some other extreme value represents missing values in d \item{silent}{whether to suppress printing an informational message after loading from a file.} -\item{feature_names}{Set names for features.} +\item{feature_names}{Set names for features. Overrides column names in data +frame and matrix.} \item{nthread}{Number of threads used for creating DMatrix.} @@ -55,6 +57,9 @@ It is useful when a 0 or some other extreme value represents missing values in d \item{label_upper_bound}{Upper bound for survival training.} \item{feature_weights}{Set feature weights for column sampling.} + +\item{enable_categorical}{Experimental support of specializing for +categorical features. JSON/UBJSON serialization format is required.} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/R-package/src/init.c b/R-package/src/init.c index c35e0ecf5..f957229af 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -41,6 +41,7 @@ extern SEXP XGDMatrixCreateFromFile_R(SEXP, SEXP); extern SEXP XGDMatrixCreateFromMat_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixGetFloatInfo_R(SEXP, SEXP); extern SEXP XGDMatrixGetUIntInfo_R(SEXP, SEXP); +extern SEXP XGDMatrixCreateFromDF_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixGetStrFeatureInfo_R(SEXP, SEXP); extern SEXP XGDMatrixNumCol_R(SEXP); extern SEXP XGDMatrixNumRow_R(SEXP); @@ -79,6 +80,7 @@ static const R_CallMethodDef CallEntries[] = { {"XGDMatrixCreateFromMat_R", (DL_FUNC) &XGDMatrixCreateFromMat_R, 3}, {"XGDMatrixGetFloatInfo_R", (DL_FUNC) &XGDMatrixGetFloatInfo_R, 2}, {"XGDMatrixGetUIntInfo_R", (DL_FUNC) &XGDMatrixGetUIntInfo_R, 2}, + {"XGDMatrixCreateFromDF_R", (DL_FUNC) &XGDMatrixCreateFromDF_R, 3}, {"XGDMatrixGetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixGetStrFeatureInfo_R, 2}, {"XGDMatrixNumCol_R", (DL_FUNC) &XGDMatrixNumCol_R, 1}, {"XGDMatrixNumRow_R", (DL_FUNC) &XGDMatrixNumRow_R, 1}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 9bebc5851..c27dc5e86 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -223,6 +223,69 @@ XGB_DLL SEXP XGDMatrixCreateFromMat_R(SEXP mat, SEXP missing, SEXP n_threads) { return ret; } +XGB_DLL SEXP XGDMatrixCreateFromDF_R(SEXP df, SEXP missing, SEXP n_threads) { + SEXP ret = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + R_API_BEGIN(); + + DMatrixHandle handle; + + auto make_vec = [&](auto const *ptr, std::int32_t len) { + auto v = xgboost::linalg::MakeVec(ptr, len); + return xgboost::linalg::ArrayInterface(v); + }; + + std::int32_t rc{0}; + { + using xgboost::Json; + auto n_features = Rf_xlength(df); + std::vector array(n_features); + CHECK_GT(n_features, 0); + auto len = Rf_xlength(VECTOR_ELT(df, 0)); + // The `data.frame` in R actually converts all data into numeric. The other type + // handlers here are not used. At the moment they are kept as a reference for when we + // can avoid making data copies during transformation. + for (decltype(n_features) i = 0; i < n_features; ++i) { + switch (TYPEOF(VECTOR_ELT(df, i))) { + case INTSXP: { + auto const *ptr = INTEGER(VECTOR_ELT(df, i)); + array[i] = make_vec(ptr, len); + break; + } + case REALSXP: { + auto const *ptr = REAL(VECTOR_ELT(df, i)); + array[i] = make_vec(ptr, len); + break; + } + case LGLSXP: { + auto const *ptr = LOGICAL(VECTOR_ELT(df, i)); + array[i] = make_vec(ptr, len); + break; + } + default: { + LOG(FATAL) << "data.frame has unsupported type."; + } + } + } + + Json jinterface{std::move(array)}; + auto sinterface = Json::Dump(jinterface); + Json jconfig{xgboost::Object{}}; + jconfig["missing"] = asReal(missing); + jconfig["nthread"] = asInteger(n_threads); + auto sconfig = Json::Dump(jconfig); + + rc = XGDMatrixCreateFromColumnar(sinterface.c_str(), sconfig.c_str(), &handle); + } + + CHECK_CALL(rc); + R_SetExternalPtrAddr(ret, handle); + R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); + R_API_END(); + Rf_unprotect(1); + + return ret; +} + namespace { void CreateFromSparse(SEXP indptr, SEXP indices, SEXP data, std::string *indptr_str, std::string *indices_str, std::string *data_str) { @@ -298,6 +361,7 @@ XGB_DLL SEXP XGDMatrixCreateFromCSR_R(SEXP indptr, SEXP indices, SEXP data, SEXP res_code = XGDMatrixCreateFromCSR(sindptr.c_str(), sindices.c_str(), sdata.c_str(), ncol, config.c_str(), &handle); } + CHECK_CALL(res_code); R_SetExternalPtrAddr(ret, handle); R_RegisterCFinalizerEx(ret, _DMatrixFinalizer, TRUE); R_API_END(); diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 4ec80b5ff..2e874e3a6 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -53,6 +53,16 @@ XGB_DLL SEXP XGDMatrixCreateFromFile_R(SEXP fname, SEXP silent); XGB_DLL SEXP XGDMatrixCreateFromMat_R(SEXP mat, SEXP missing, SEXP n_threads); + +/** + * @brief Create matrix content from a data frame. + * @param data R data.frame object + * @param missing which value to represent missing value + * @param n_threads Number of threads used to construct DMatrix from dense matrix. + * @return created dmatrix + */ +XGB_DLL SEXP XGDMatrixCreateFromDF_R(SEXP df, SEXP missing, SEXP n_threads); + /*! * \brief create a matrix content from CSC format * \param indptr pointer to column headers diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index f1eaa9d80..87a73d84b 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -322,3 +322,30 @@ test_that("xgb.DMatrix: can get group for both 'qid' and 'group' constructors", expected_gr <- c(0, 20, 40, 100) expect_equal(info_gr, expected_gr) }) + +test_that("xgb.DMatrix: data.frame", { + df <- data.frame( + a = (1:4) / 10, + num = c(1, NA, 3, 4), + as.int = as.integer(c(1, 2, 3, 4)), + lo = c(TRUE, FALSE, NA, TRUE), + str.fac = c("a", "b", "d", "c"), + as.fac = as.factor(c(3, 5, 8, 11)), + stringsAsFactors = TRUE + ) + + m <- xgb.DMatrix(df, enable_categorical = TRUE) + expect_equal(colnames(m), colnames(df)) + expect_equal( + getinfo(m, "feature_type"), c("float", "float", "int", "i", "c", "c") + ) + expect_error(xgb.DMatrix(df)) + + df <- data.frame( + missing = c("a", "b", "d", NA), + valid = c("a", "b", "d", "c"), + stringsAsFactors = TRUE + ) + m <- xgb.DMatrix(df, enable_categorical = TRUE) + expect_equal(getinfo(m, "feature_type"), c("c", "c")) +}) diff --git a/demo/guide-python/cat_in_the_dat.py b/demo/guide-python/cat_in_the_dat.py index a7b94cdca..2b2d20682 100644 --- a/demo/guide-python/cat_in_the_dat.py +++ b/demo/guide-python/cat_in_the_dat.py @@ -78,6 +78,10 @@ def categorical_model(X: pd.DataFrame, y: pd.Series, output_dir: str) -> None: X_train, X_test, y_train, y_test = train_test_split( X, y, random_state=1994, test_size=0.2 ) + # Be aware that the encoding for X_train and X_test are the same here. In practice, + # we should try to use an encoder like (sklearn OrdinalEncoder) to obtain the + # categorical values. + # Specify `enable_categorical` to True. clf = xgb.XGBClassifier( **params, diff --git a/include/xgboost/c_api.h b/include/xgboost/c_api.h index 59d4d0881..a56ead278 100644 --- a/include/xgboost/c_api.h +++ b/include/xgboost/c_api.h @@ -159,6 +159,16 @@ XGB_DLL int XGDMatrixCreateFromURI(char const *config, DMatrixHandle *out); XGB_DLL int XGDMatrixCreateFromCSREx(const size_t *indptr, const unsigned *indices, const float *data, size_t nindptr, size_t nelem, size_t num_col, DMatrixHandle *out); +/** + * @brief Create a DMatrix from columnar data. (table) + * + * @param data See @ref XGBoosterPredictFromColumnar for details. + * @param config See @ref XGDMatrixCreateFromDense for details. + * @param out The created dmatrix. + * + * @return 0 when success, -1 when failure happens + */ +XGB_DLL int XGDMatrixCreateFromColumnar(char const *data, char const *config, DMatrixHandle *out); /** * @example c-api-demo.c @@ -514,6 +524,16 @@ XGB_DLL int XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, const char *c_interface_str); +/** + * @brief Set columnar (table) data on a DMatrix proxy. + * + * @param handle A DMatrix proxy created by @ref XGProxyDMatrixCreate + * @param c_interface_str See @ref XGBoosterPredictFromColumnar for details. + * + * @return 0 when success, -1 when failure happens + */ +XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, char const *c_interface_str); + /*! * \brief Set data on a DMatrix proxy. * @@ -1113,6 +1133,31 @@ XGB_DLL int XGBoosterPredictFromDense(BoosterHandle handle, char const *values, * @example inference.c */ +/** + * @brief Inplace prediction from CPU columnar data. (Table) + * + * @note If the booster is configured to run on a CUDA device, XGBoost falls back to run + * prediction with DMatrix with a performance warning. + * + * @param handle Booster handle. + * @param values An JSON array of __array_interface__ for each column. + * @param config See @ref XGBoosterPredictFromDMatrix for more info. + * Additional fields for inplace prediction are: + * - "missing": float + * @param m An optional (NULL if not available) proxy DMatrix instance + * storing meta info. + * + * @param out_shape See @ref XGBoosterPredictFromDMatrix for more info. + * @param out_dim See @ref XGBoosterPredictFromDMatrix for more info. + * @param out_result See @ref XGBoosterPredictFromDMatrix for more info. + * + * @return 0 when success, -1 when failure happens + */ +XGB_DLL int XGBoosterPredictFromColumnar(BoosterHandle handle, char const *array_interface, + char const *c_json_config, DMatrixHandle m, + bst_ulong const **out_shape, bst_ulong *out_dim, + const float **out_result); + /** * \brief Inplace prediction from CPU CSR matrix. * diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index bf72d95cc..3c864a1c8 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -822,8 +822,7 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m .. note:: This parameter is experimental - Experimental support of specializing for categorical features. Do not set - to True unless you are interested in development. Also, JSON/UBJSON + Experimental support of specializing for categorical features. JSON/UBJSON serialization format is required. """ @@ -1431,6 +1430,12 @@ class _ProxyDMatrix(DMatrix): _LIB.XGProxyDMatrixSetDataDense(self.handle, _array_interface(data)) ) + def _set_data_from_pandas(self, data: DataType) -> None: + """Set data from a pandas DataFrame. The input is a PandasTransformed instance.""" + _check_call( + _LIB.XGProxyDMatrixSetDataColumnar(self.handle, data.array_interface()) + ) + def _set_data_from_csr(self, csr: scipy.sparse.csr_matrix) -> None: """Set data from scipy csr""" from .data import _array_interface @@ -2440,6 +2445,7 @@ class Booster: assert proxy is None or isinstance(proxy, _ProxyDMatrix) from .data import ( + PandasTransformed, _array_interface, _arrow_transform, _is_arrow, @@ -2494,6 +2500,19 @@ class Booster: ) ) return _prediction_output(shape, dims, preds, False) + if isinstance(data, PandasTransformed): + _check_call( + _LIB.XGBoosterPredictFromColumnar( + self.handle, + data.array_interface(), + args, + p_handle, + ctypes.byref(shape), + ctypes.byref(dims), + ctypes.byref(preds), + ) + ) + return _prediction_output(shape, dims, preds, False) if isinstance(data, scipy.sparse.csr_matrix): from .data import transform_scipy_sparse diff --git a/python-package/xgboost/data.py b/python-package/xgboost/data.py index 49287d817..74b2966fe 100644 --- a/python-package/xgboost/data.py +++ b/python-package/xgboost/data.py @@ -65,13 +65,18 @@ def _is_scipy_csr(data: DataType) -> bool: return isinstance(data, scipy.sparse.csr_matrix) -def _array_interface(data: np.ndarray) -> bytes: +def _array_interface_dict(data: np.ndarray) -> dict: assert ( data.dtype.hasobject is False ), "Input data contains `object` dtype. Expecting numeric data." interface = data.__array_interface__ if "mask" in interface: interface["mask"] = interface["mask"].__array_interface__ + return interface + + +def _array_interface(data: np.ndarray) -> bytes: + interface = _array_interface_dict(data) interface_str = bytes(json.dumps(interface), "utf-8") return interface_str @@ -265,24 +270,24 @@ pandas_nullable_mapper = { "Int16": "int", "Int32": "int", "Int64": "int", - "UInt8": "i", - "UInt16": "i", - "UInt32": "i", - "UInt64": "i", + "UInt8": "int", + "UInt16": "int", + "UInt32": "int", + "UInt64": "int", "Float32": "float", "Float64": "float", "boolean": "i", } pandas_pyarrow_mapper = { - "int8[pyarrow]": "i", - "int16[pyarrow]": "i", - "int32[pyarrow]": "i", - "int64[pyarrow]": "i", - "uint8[pyarrow]": "i", - "uint16[pyarrow]": "i", - "uint32[pyarrow]": "i", - "uint64[pyarrow]": "i", + "int8[pyarrow]": "int", + "int16[pyarrow]": "int", + "int32[pyarrow]": "int", + "int64[pyarrow]": "int", + "uint8[pyarrow]": "int", + "uint16[pyarrow]": "int", + "uint32[pyarrow]": "int", + "uint64[pyarrow]": "int", "float[pyarrow]": "float", "float32[pyarrow]": "float", "double[pyarrow]": "float", @@ -295,7 +300,7 @@ _pandas_dtype_mapper.update(pandas_pyarrow_mapper) _ENABLE_CAT_ERR = ( - "When categorical type is supplied, The experimental DMatrix parameter" + "When categorical type is supplied, the experimental DMatrix parameter" "`enable_categorical` must be set to `True`." ) @@ -407,89 +412,122 @@ def is_pd_sparse_dtype(dtype: PandasDType) -> bool: return is_sparse(dtype) -def pandas_cat_null(data: DataFrame) -> DataFrame: - """Handle categorical dtype and nullable extension types from pandas.""" - import pandas as pd - - # handle category codes and nullable. - cat_columns = [] - nul_columns = [] - # avoid an unnecessary conversion if possible - for col, dtype in zip(data.columns, data.dtypes): - if is_pd_cat_dtype(dtype): - cat_columns.append(col) - elif is_pa_ext_categorical_dtype(dtype): - raise ValueError( - "pyarrow dictionary type is not supported. Use pandas category instead." - ) - elif is_nullable_dtype(dtype): - nul_columns.append(col) - - if cat_columns or nul_columns: - # Avoid transformation due to: PerformanceWarning: DataFrame is highly - # fragmented - transformed = data.copy(deep=False) - else: - transformed = data - - def cat_codes(ser: pd.Series) -> pd.Series: - if is_pd_cat_dtype(ser.dtype): - return ser.cat.codes - assert is_pa_ext_categorical_dtype(ser.dtype) - # Not yet supported, the index is not ordered for some reason. Alternately: - # `combine_chunks().to_pandas().cat.codes`. The result is the same. - return ser.array.__arrow_array__().combine_chunks().dictionary_encode().indices - - if cat_columns: - # DF doesn't have the cat attribute, as a result, we use apply here - transformed[cat_columns] = ( - transformed[cat_columns] - .apply(cat_codes) - .astype(np.float32) - .replace(-1.0, np.NaN) - ) - if nul_columns: - transformed[nul_columns] = transformed[nul_columns].astype(np.float32) - - # TODO(jiamingy): Investigate the possibility of using dataframe protocol or arrow - # IPC format for pandas so that we can apply the data transformation inside XGBoost - # for better memory efficiency. - - return transformed - - -def pandas_ext_num_types(data: DataFrame) -> DataFrame: - """Experimental suppport for handling pyarrow extension numeric types.""" +def pandas_pa_type(ser: Any) -> np.ndarray: + """Handle pandas pyarrow extention.""" import pandas as pd import pyarrow as pa + # No copy, callstack: + # pandas.core.internals.managers.SingleBlockManager.array_values() + # pandas.core.internals.blocks.EABackedBlock.values + d_array: pd.arrays.ArrowExtensionArray = ser.array + # no copy in __arrow_array__ + # ArrowExtensionArray._data is a chunked array + aa: pa.ChunkedArray = d_array.__arrow_array__() + # combine_chunks takes the most significant amount of time + chunk: pa.Array = aa.combine_chunks() + # When there's null value, we have to use copy + zero_copy = chunk.null_count == 0 + # Alternately, we can use chunk.buffers(), which returns a list of buffers and + # we need to concatenate them ourselves. + # FIXME(jiamingy): Is there a better way to access the arrow buffer along with + # its mask? + # Buffers from chunk.buffers() have the address attribute, but don't expose the + # mask. + arr: np.ndarray = chunk.to_numpy(zero_copy_only=zero_copy, writable=False) + arr, _ = _ensure_np_dtype(arr, arr.dtype) + return arr + + +def pandas_transform_data(data: DataFrame) -> List[np.ndarray]: + """Handle categorical dtype and extension types from pandas.""" + import pandas as pd + from pandas import Float32Dtype, Float64Dtype + + result: List[np.ndarray] = [] + + def cat_codes(ser: pd.Series) -> np.ndarray: + if is_pd_cat_dtype(ser.dtype): + return _ensure_np_dtype( + ser.cat.codes.astype(np.float32) + .replace(-1.0, np.NaN) + .to_numpy(na_value=np.nan), + np.float32, + )[0] + # Not yet supported, the index is not ordered for some reason. Alternately: + # `combine_chunks().to_pandas().cat.codes`. The result is the same. + assert is_pa_ext_categorical_dtype(ser.dtype) + return ( + ser.array.__arrow_array__() + .combine_chunks() + .dictionary_encode() + .indices.astype(np.float32) + .replace(-1.0, np.NaN) + ) + + def nu_type(ser: pd.Series) -> np.ndarray: + # Avoid conversion when possible + if isinstance(dtype, Float32Dtype): + res_dtype: NumpyDType = np.float32 + elif isinstance(dtype, Float64Dtype): + res_dtype = np.float64 + else: + res_dtype = np.float32 + return _ensure_np_dtype( + ser.to_numpy(dtype=res_dtype, na_value=np.nan), res_dtype + )[0] + + def oth_type(ser: pd.Series) -> np.ndarray: + # The dtypes module is added in 1.25. + npdtypes = np.lib.NumpyVersion(np.__version__) > np.lib.NumpyVersion("1.25.0") + npdtypes = npdtypes and isinstance( + ser.dtype, + ( + # pylint: disable=no-member + np.dtypes.Float32DType, # type: ignore + # pylint: disable=no-member + np.dtypes.Float64DType, # type: ignore + ), + ) + + if npdtypes or dtype in {np.float32, np.float64}: + array = ser.to_numpy() + else: + # Specifying the dtype can significantly slow down the conversion (about + # 15% slow down for dense inplace-predict) + array = ser.to_numpy(dtype=np.float32, na_value=np.nan) + return _ensure_np_dtype(array, array.dtype)[0] + for col, dtype in zip(data.columns, data.dtypes): - if not is_pa_ext_dtype(dtype): - continue - # No copy, callstack: - # pandas.core.internals.managers.SingleBlockManager.array_values() - # pandas.core.internals.blocks.EABackedBlock.values - d_array: pd.arrays.ArrowExtensionArray = data[col].array - # no copy in __arrow_array__ - # ArrowExtensionArray._data is a chunked array - aa: pa.ChunkedArray = d_array.__arrow_array__() - chunk: pa.Array = aa.combine_chunks() - # Alternately, we can use chunk.buffers(), which returns a list of buffers and - # we need to concatenate them ourselves. - arr = chunk.__array__() - data[col] = arr - return data + if is_pa_ext_categorical_dtype(dtype): + raise ValueError( + "pyarrow dictionary type is not supported. Use pandas category instead." + ) + if is_pd_cat_dtype(dtype): + result.append(cat_codes(data[col])) + elif is_pa_ext_dtype(dtype): + result.append(pandas_pa_type(data[col])) + elif is_nullable_dtype(dtype): + result.append(nu_type(data[col])) + elif is_pd_sparse_dtype(dtype): + arr = cast(pd.arrays.SparseArray, data[col].values) + arr = arr.to_dense() + if _is_np_array_like(arr): + arr, _ = _ensure_np_dtype(arr, arr.dtype) + result.append(arr) + else: + result.append(oth_type(data[col])) + + # FIXME(jiamingy): Investigate the possibility of using dataframe protocol or arrow + # IPC format for pandas so that we can apply the data transformation inside XGBoost + # for better memory efficiency. + return result -def _transform_pandas_df( - data: DataFrame, - enable_categorical: bool, - feature_names: Optional[FeatureNames] = None, - feature_types: Optional[FeatureTypes] = None, - meta: Optional[str] = None, - meta_type: Optional[NumpyDType] = None, -) -> Tuple[np.ndarray, Optional[FeatureNames], Optional[FeatureTypes]]: - pyarrow_extension = False +def pandas_check_dtypes(data: DataFrame, enable_categorical: bool) -> None: + """Validate the input types, returns True if the dataframe is backed by arrow.""" + sparse_extension = False + for dtype in data.dtypes: if not ( (dtype.name in _pandas_dtype_mapper) @@ -498,27 +536,65 @@ def _transform_pandas_df( or is_pa_ext_dtype(dtype) ): _invalid_dataframe_dtype(data) - if is_pa_ext_dtype(dtype): - pyarrow_extension = True + + if is_pd_sparse_dtype(dtype): + sparse_extension = True + + if sparse_extension: + warnings.warn("Sparse arrays from pandas are converted into dense.") + + +class PandasTransformed: + """A storage class for transformed pandas DataFrame.""" + + def __init__(self, columns: List[np.ndarray]) -> None: + self.columns = columns + + def array_interface(self) -> bytes: + """Return a byte string for JSON encoded array interface.""" + aitfs = list(map(_array_interface_dict, self.columns)) + sarrays = bytes(json.dumps(aitfs), "utf-8") + return sarrays + + @property + def shape(self) -> Tuple[int, int]: + """Return shape of the transformed DataFrame.""" + return self.columns[0].shape[0], len(self.columns) + + +def _transform_pandas_df( + data: DataFrame, + enable_categorical: bool, + feature_names: Optional[FeatureNames] = None, + feature_types: Optional[FeatureTypes] = None, + meta: Optional[str] = None, +) -> Tuple[PandasTransformed, Optional[FeatureNames], Optional[FeatureTypes]]: + pandas_check_dtypes(data, enable_categorical) + if meta and len(data.columns) > 1 and meta not in _matrix_meta: + raise ValueError(f"DataFrame for {meta} cannot have multiple columns") feature_names, feature_types = pandas_feature_info( data, meta, feature_names, feature_types, enable_categorical ) - transformed = pandas_cat_null(data) - if pyarrow_extension: - if transformed is data: - transformed = data.copy(deep=False) - transformed = pandas_ext_num_types(transformed) + arrays = pandas_transform_data(data) + return PandasTransformed(arrays), feature_names, feature_types - if meta and len(data.columns) > 1 and meta not in _matrix_meta: - raise ValueError(f"DataFrame for {meta} cannot have multiple columns") - dtype = meta_type if meta_type else np.float32 - arr: np.ndarray = transformed.values - if meta_type: - arr = arr.astype(dtype) - return arr, feature_names, feature_types +def _meta_from_pandas_df( + data: DataType, + name: str, + dtype: Optional[NumpyDType], + handle: ctypes.c_void_p, +) -> None: + data, _, _ = _transform_pandas_df(data, False, meta=name) + if len(data.columns) == 1: + array = data.columns[0] + else: + array = np.stack(data.columns).T + + array, dtype = _ensure_np_dtype(array, dtype) + _meta_from_numpy(array, name, dtype, handle) def _from_pandas_df( @@ -530,12 +606,21 @@ def _from_pandas_df( feature_types: Optional[FeatureTypes], data_split_mode: DataSplitMode = DataSplitMode.ROW, ) -> DispatchedDataBackendReturnType: - data, feature_names, feature_types = _transform_pandas_df( + df, feature_names, feature_types = _transform_pandas_df( data, enable_categorical, feature_names, feature_types ) - return _from_numpy_array( - data, missing, nthread, feature_names, feature_types, data_split_mode + + handle = ctypes.c_void_p() + _check_call( + _LIB.XGDMatrixCreateFromColumnar( + df.array_interface(), + make_jcargs( + nthread=nthread, missing=missing, data_split_mode=data_split_mode + ), + ctypes.byref(handle), + ) ) + return handle, feature_names, feature_types def _is_pandas_series(data: DataType) -> bool: @@ -550,7 +635,12 @@ def _meta_from_pandas_series( data: DataType, name: str, dtype: Optional[NumpyDType], handle: ctypes.c_void_p ) -> None: """Help transform pandas series for meta data like labels""" - data = data.values.astype("float") + if is_pd_sparse_dtype(data.dtype): + data = data.values.to_dense().astype(np.float32) + elif is_pa_ext_dtype(data.dtype): + data = pandas_pa_type(data) + else: + data = data.to_numpy(np.float32, na_value=np.nan) if is_pd_sparse_dtype(getattr(data, "dtype", data)): data = data.to_dense() # type: ignore @@ -732,6 +822,8 @@ def _arrow_transform(data: DataType) -> Any: return pd.ArrowDtype(pa.bool_()) return None + # For common cases, this is zero-copy, can check with: + # pa.total_allocated_bytes() df = data.to_pandas(types_mapper=type_mapper) return df @@ -859,11 +951,10 @@ def _from_cudf_df( ) interfaces_str = _cudf_array_interfaces(data, cat_codes) handle = ctypes.c_void_p() - config = bytes(json.dumps({"missing": missing, "nthread": nthread}), "utf-8") _check_call( _LIB.XGDMatrixCreateFromCudaColumnar( interfaces_str, - config, + make_jcargs(nthread=nthread, missing=missing), ctypes.byref(handle), ) ) @@ -1221,8 +1312,7 @@ def dispatch_meta_backend( if _is_arrow(data): data = _arrow_transform(data) if _is_pandas_df(data): - data, _, _ = _transform_pandas_df(data, False, meta=name, meta_type=dtype) - _meta_from_numpy(data, name, dtype, handle) + _meta_from_pandas_df(data, name, dtype=dtype, handle=handle) return if _is_pandas_series(data): _meta_from_pandas_series(data, name, dtype, handle) @@ -1244,8 +1334,7 @@ def dispatch_meta_backend( _meta_from_dt(data, name, dtype, handle) return if _is_modin_df(data): - data, _, _ = _transform_pandas_df(data, False, meta=name, meta_type=dtype) - _meta_from_numpy(data, name, dtype, handle) + _meta_from_pandas_df(data, name, dtype=dtype, handle=handle) return if _is_modin_series(data): data = data.values.astype("float") @@ -1317,11 +1406,10 @@ def _proxy_transform( if _is_arrow(data): data = _arrow_transform(data) if _is_pandas_df(data): - arr, feature_names, feature_types = _transform_pandas_df( + df, feature_names, feature_types = _transform_pandas_df( data, enable_categorical, feature_names, feature_types ) - arr, _ = _ensure_np_dtype(arr, arr.dtype) - return arr, None, feature_names, feature_types + return df, None, feature_names, feature_types raise TypeError("Value type is not supported for data iterator:" + str(type(data))) @@ -1356,6 +1444,9 @@ def dispatch_proxy_set_data( if not allow_host: raise err + if isinstance(data, PandasTransformed): + proxy._set_data_from_pandas(data) # pylint: disable=W0212 + return if _is_np_array_like(data): _check_data_shape(data) proxy._set_data_from_array(data) # pylint: disable=W0212 diff --git a/src/c_api/c_api.cc b/src/c_api/c_api.cc index 22f03640e..9aedcef2e 100644 --- a/src/c_api/c_api.cc +++ b/src/c_api/c_api.cc @@ -361,49 +361,57 @@ XGB_DLL int XGProxyDMatrixCreate(DMatrixHandle *out) { API_END(); } -XGB_DLL int -XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, - char const *c_interface_str) { +XGB_DLL int XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, + char const *c_interface_str) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(c_interface_str); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetCUDAArray(c_interface_str); API_END(); } -XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, - char const *c_interface_str) { +XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, char const *c_interface_str) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(c_interface_str); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetCUDAArray(c_interface_str); API_END(); } -XGB_DLL int XGProxyDMatrixSetDataDense(DMatrixHandle handle, - char const *c_interface_str) { +XGB_DLL int XGProxyDMatrixSetDataColumnar(DMatrixHandle handle, char const *c_interface_str) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(c_interface_str); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); + CHECK(m) << "Current DMatrix type does not support set data."; + m->SetColumnarData(c_interface_str); + API_END(); +} + +XGB_DLL int XGProxyDMatrixSetDataDense(DMatrixHandle handle, char const *c_interface_str) { + API_BEGIN(); + CHECK_HANDLE(); + xgboost_CHECK_C_ARG_PTR(c_interface_str); + auto p_m = static_cast *>(handle); + CHECK(p_m); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetArrayData(c_interface_str); API_END(); } -XGB_DLL int XGProxyDMatrixSetDataCSR(DMatrixHandle handle, char const *indptr, - char const *indices, char const *data, - xgboost::bst_ulong ncol) { +XGB_DLL int XGProxyDMatrixSetDataCSR(DMatrixHandle handle, char const *indptr, char const *indices, + char const *data, xgboost::bst_ulong ncol) { API_BEGIN(); CHECK_HANDLE(); xgboost_CHECK_C_ARG_PTR(indptr); @@ -411,7 +419,7 @@ XGB_DLL int XGProxyDMatrixSetDataCSR(DMatrixHandle handle, char const *indptr, xgboost_CHECK_C_ARG_PTR(data); auto p_m = static_cast *>(handle); CHECK(p_m); - auto m = static_cast(p_m->get()); + auto m = static_cast(p_m->get()); CHECK(m) << "Current DMatrix type does not support set data."; m->SetCSRData(indptr, indices, data, ncol, true); API_END(); @@ -429,6 +437,25 @@ XGB_DLL int XGDMatrixCreateFromCSREx(const size_t *indptr, const unsigned *indic API_END(); } +XGB_DLL int XGDMatrixCreateFromColumnar(char const *data, char const *c_json_config, + DMatrixHandle *out) { + API_BEGIN(); + xgboost_CHECK_C_ARG_PTR(c_json_config); + xgboost_CHECK_C_ARG_PTR(data); + + auto config = Json::Load(c_json_config); + float missing = GetMissing(config); + auto n_threads = OptionalArg(config, "nthread", 0); + auto data_split_mode = + static_cast(OptionalArg(config, "data_split_mode", 0)); + + data::ColumnarAdapter adapter{data}; + *out = new std::shared_ptr( + DMatrix::Create(&adapter, missing, n_threads, "", data_split_mode)); + + API_END(); +} + XGB_DLL int XGDMatrixCreateFromCSR(char const *indptr, char const *indices, char const *data, xgboost::bst_ulong ncol, char const *c_json_config, DMatrixHandle *out) { @@ -1196,6 +1223,27 @@ XGB_DLL int XGBoosterPredictFromDense(BoosterHandle handle, char const *array_in API_END(); } +XGB_DLL int XGBoosterPredictFromColumnar(BoosterHandle handle, char const *array_interface, + char const *c_json_config, DMatrixHandle m, + xgboost::bst_ulong const **out_shape, + xgboost::bst_ulong *out_dim, const float **out_result) { + API_BEGIN(); + CHECK_HANDLE(); + std::shared_ptr p_m{nullptr}; + if (!m) { + p_m.reset(new data::DMatrixProxy); + } else { + p_m = *static_cast *>(m); + } + auto proxy = dynamic_cast(p_m.get()); + CHECK(proxy) << "Invalid input type for inplace predict."; + xgboost_CHECK_C_ARG_PTR(array_interface); + proxy->SetColumnarData(array_interface); + auto *learner = static_cast(handle); + InplacePredictImpl(p_m, c_json_config, learner, out_shape, out_dim, out_result); + API_END(); +} + XGB_DLL int XGBoosterPredictFromCSR(BoosterHandle handle, char const *indptr, char const *indices, char const *data, xgboost::bst_ulong cols, char const *c_json_config, DMatrixHandle m, diff --git a/src/common/quantile.cc b/src/common/quantile.cc index 0490add26..c74db99e4 100644 --- a/src/common/quantile.cc +++ b/src/common/quantile.cc @@ -97,6 +97,7 @@ void HostSketchContainer::PushAdapterBatch(Batch const &batch, size_t base_rowid // the nnz from info is not reliable as sketching might be the first place to go through // the data. auto is_dense = info.num_nonzero_ == info.num_col_ * info.num_row_; + CHECK(!this->columns_size_.empty()); this->PushRowPageImpl(batch, base_rowid, weights, info.num_nonzero_, info.num_col_, is_dense, is_valid); } @@ -110,6 +111,7 @@ INSTANTIATE(CSRArrayAdapterBatch) INSTANTIATE(CSCAdapterBatch) INSTANTIATE(DataTableAdapterBatch) INSTANTIATE(SparsePageAdapterBatch) +INSTANTIATE(ColumnarAdapterBatch) namespace { /** diff --git a/src/data/adapter.h b/src/data/adapter.h index 9e7058aba..e9a4ad9fc 100644 --- a/src/data/adapter.h +++ b/src/data/adapter.h @@ -25,9 +25,7 @@ #include "xgboost/span.h" #include "xgboost/string_view.h" -namespace xgboost { -namespace data { - +namespace xgboost::data { /** External data formats should implement an adapter as below. The * adapter provides a uniform access to data outside xgboost, allowing * construction of DMatrix objects from a range of sources without duplicating @@ -279,9 +277,9 @@ class ArrayAdapterBatch : public detail::NoMetaInfo { return Line{array_interface_, idx}; } - size_t NumRows() const { return array_interface_.Shape(0); } - size_t NumCols() const { return array_interface_.Shape(1); } - size_t Size() const { return this->NumRows(); } + [[nodiscard]] std::size_t NumRows() const { return array_interface_.Shape(0); } + [[nodiscard]] std::size_t NumCols() const { return array_interface_.Shape(1); } + [[nodiscard]] std::size_t Size() const { return this->NumRows(); } explicit ArrayAdapterBatch(ArrayInterface<2> array_interface) : array_interface_{std::move(array_interface)} {} @@ -326,11 +324,11 @@ class CSRArrayAdapterBatch : public detail::NoMetaInfo { : indices_{std::move(indices)}, values_{std::move(values)}, ridx_{ridx}, offset_{offset} {} - COOTuple GetElement(std::size_t idx) const { + [[nodiscard]] COOTuple GetElement(std::size_t idx) const { return {ridx_, TypedIndex{indices_}(offset_ + idx), values_(offset_ + idx)}; } - size_t Size() const { + [[nodiscard]] std::size_t Size() const { return values_.Shape(0); } }; @@ -539,9 +537,11 @@ class CSCArrayAdapter : public detail::SingleBatchDataIter batch_{CSCArrayAdapterBatch{indptr_, indices_, values_}} {} // JVM package sends 0 as unknown - size_t NumRows() const { return num_rows_ == 0 ? kAdapterUnknownSize : num_rows_; } - size_t NumColumns() const { return indptr_.n - 1; } - const CSCArrayAdapterBatch& Value() const override { return batch_; } + [[nodiscard]] std::size_t NumRows() const { + return num_rows_ == 0 ? kAdapterUnknownSize : num_rows_; + } + [[nodiscard]] std::size_t NumColumns() const { return indptr_.n - 1; } + [[nodiscard]] const CSCArrayAdapterBatch& Value() const override { return batch_; } }; class DataTableAdapterBatch : public detail::NoMetaInfo { @@ -634,15 +634,15 @@ class DataTableAdapterBatch : public detail::NoMetaInfo { public: Line(std::size_t ridx, void const* const* const data, std::vector const& ft) : row_idx_{ridx}, data_{data}, feature_types_{ft} {} - std::size_t Size() const { return feature_types_.size(); } - COOTuple GetElement(std::size_t idx) const { + [[nodiscard]] std::size_t Size() const { return feature_types_.size(); } + [[nodiscard]] COOTuple GetElement(std::size_t idx) const { return COOTuple{row_idx_, idx, DTGetValue(data_[idx], feature_types_[idx], row_idx_)}; } }; public: - size_t Size() const { return num_rows_; } - const Line GetLine(std::size_t ridx) const { return {ridx, data_, feature_types_}; } + [[nodiscard]] size_t Size() const { return num_rows_; } + [[nodiscard]] const Line GetLine(std::size_t ridx) const { return {ridx, data_, feature_types_}; } static constexpr bool kIsRowMajor = true; private: @@ -659,9 +659,9 @@ class DataTableAdapter : public detail::SingleBatchDataIter> columns_; + + class Line { + common::Span> const& columns_; + std::size_t ridx_; + + public: + explicit Line(common::Span> const& columns, std::size_t ridx) + : columns_{columns}, ridx_{ridx} {} + [[nodiscard]] std::size_t Size() const { return columns_.empty() ? 0 : columns_.size(); } + + [[nodiscard]] COOTuple GetElement(std::size_t idx) const { + return {ridx_, idx, columns_[idx](ridx_)}; + } + }; + + public: + ColumnarAdapterBatch() = default; + explicit ColumnarAdapterBatch(common::Span> columns) + : columns_{columns} {} + [[nodiscard]] Line GetLine(std::size_t ridx) const { return Line{columns_, ridx}; } + [[nodiscard]] std::size_t Size() const { + return columns_.empty() ? 0 : columns_.front().Shape(0); + } + [[nodiscard]] std::size_t NumCols() const { return columns_.empty() ? 0 : columns_.size(); } + [[nodiscard]] std::size_t NumRows() const { return this->Size(); } + + static constexpr bool kIsRowMajor = true; +}; + +class ColumnarAdapter : public detail::SingleBatchDataIter { + std::vector> columns_; + ColumnarAdapterBatch batch_; + + public: + explicit ColumnarAdapter(StringView columns) { + auto jarray = Json::Load(columns); + CHECK(IsA(jarray)); + auto const& array = get(jarray); + for (auto col : array) { + columns_.emplace_back(get(col)); + } + bool consistent = + columns_.empty() || + std::all_of(columns_.cbegin(), columns_.cend(), [&](ArrayInterface<1, false> const& array) { + return array.Shape(0) == columns_[0].Shape(0); + }); + CHECK(consistent) << "Size of columns should be the same."; + batch_ = ColumnarAdapterBatch{columns_}; + } + + [[nodiscard]] ColumnarAdapterBatch const& Value() const override { return batch_; } + + [[nodiscard]] std::size_t NumRows() const { + if (!columns_.empty()) { + return columns_.front().shape[0]; + } + return 0; + } + [[nodiscard]] std::size_t NumColumns() const { + if (!columns_.empty()) { + return columns_.size(); + } + return 0; + } +}; + class FileAdapterBatch { public: class Line { @@ -851,6 +919,5 @@ class SparsePageAdapterBatch { Line GetLine(size_t ridx) const { return Line{page_[ridx].data(), page_[ridx].size(), ridx}; } size_t Size() const { return page_.Size(); } }; -}; // namespace data -} // namespace xgboost +} // namespace xgboost::data #endif // XGBOOST_DATA_ADAPTER_H_ diff --git a/src/data/data.cc b/src/data/data.cc index 50f64a406..96560a11f 100644 --- a/src/data/data.cc +++ b/src/data/data.cc @@ -947,38 +947,24 @@ DMatrix* DMatrix::Create(AdapterT* adapter, float missing, int nthread, const st return new data::SimpleDMatrix(adapter, missing, nthread, data_split_mode); } -template DMatrix* DMatrix::Create(data::DenseAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::ArrayAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSRAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSCAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::DataTableAdapter* adapter, - float missing, std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::FileAdapter* adapter, float missing, - std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSRArrayAdapter* adapter, - float missing, std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); -template DMatrix* DMatrix::Create(data::CSCArrayAdapter* adapter, - float missing, std::int32_t nthread, - const std::string& cache_prefix, - DataSplitMode data_split_mode); +// Instantiate the factory function for various adapters +#define INSTANTIATION_CREATE(_AdapterT) \ + template DMatrix* DMatrix::Create( \ + data::_AdapterT * adapter, float missing, std::int32_t nthread, \ + const std::string& cache_prefix, DataSplitMode data_split_mode); + +INSTANTIATION_CREATE(DenseAdapter) +INSTANTIATION_CREATE(ArrayAdapter) +INSTANTIATION_CREATE(CSRAdapter) +INSTANTIATION_CREATE(CSCAdapter) +INSTANTIATION_CREATE(DataTableAdapter) +INSTANTIATION_CREATE(FileAdapter) +INSTANTIATION_CREATE(CSRArrayAdapter) +INSTANTIATION_CREATE(CSCArrayAdapter) +INSTANTIATION_CREATE(ColumnarAdapter) + +#undef INSTANTIATION_CREATE + template DMatrix* DMatrix::Create( data::IteratorAdapter* adapter, float missing, int nthread, const std::string& cache_prefix, DataSplitMode data_split_mode); @@ -1156,7 +1142,6 @@ uint64_t SparsePage::Push(const AdapterBatchT& batch, float missing, int nthread builder.InitStorage(); // Second pass over batch, placing elements in correct position - auto is_valid = data::IsValidFunctor{missing}; #pragma omp parallel num_threads(nthread) { @@ -1253,9 +1238,10 @@ template uint64_t SparsePage::Push(const data::CSCAdapterBatch& batch, float mis template uint64_t SparsePage::Push(const data::DataTableAdapterBatch& batch, float missing, int nthread); template uint64_t SparsePage::Push(const data::FileAdapterBatch& batch, float missing, int nthread); +template uint64_t SparsePage::Push(const data::ColumnarAdapterBatch& batch, float missing, + std::int32_t nthread); namespace data { - // List of files that will be force linked in static links. DMLC_REGISTRY_LINK_TAG(sparse_page_raw_format); DMLC_REGISTRY_LINK_TAG(gradient_index_format); diff --git a/src/data/gradient_index.cc b/src/data/gradient_index.cc index a2b3f3e54..1d3faf945 100644 --- a/src/data/gradient_index.cc +++ b/src/data/gradient_index.cc @@ -120,7 +120,7 @@ void GHistIndexMatrix::PushAdapterBatchColumns(Context const *ctx, Batch const & INSTANTIATION_PUSH(data::CSRArrayAdapterBatch) INSTANTIATION_PUSH(data::ArrayAdapterBatch) INSTANTIATION_PUSH(data::SparsePageAdapterBatch) - +INSTANTIATION_PUSH(data::ColumnarAdapterBatch) #undef INSTANTIATION_PUSH void GHistIndexMatrix::ResizeIndex(const size_t n_index, const bool isDense) { diff --git a/src/data/proxy_dmatrix.cc b/src/data/proxy_dmatrix.cc index c6e840539..a28448e3b 100644 --- a/src/data/proxy_dmatrix.cc +++ b/src/data/proxy_dmatrix.cc @@ -5,7 +5,22 @@ #include "proxy_dmatrix.h" +#include // for shared_ptr + +#include "xgboost/context.h" // for Context +#include "xgboost/data.h" // for DMatrix +#include "xgboost/logging.h" +#include "xgboost/string_view.h" // for StringView + namespace xgboost::data { +void DMatrixProxy::SetColumnarData(StringView interface_str) { + std::shared_ptr adapter{new ColumnarAdapter{interface_str}}; + this->batch_ = adapter; + this->Info().num_col_ = adapter->NumColumns(); + this->Info().num_row_ = adapter->NumRows(); + this->ctx_.Init(Args{{"device", "cpu"}}); +} + void DMatrixProxy::SetArrayData(StringView interface_str) { std::shared_ptr adapter{new ArrayAdapter{interface_str}}; this->batch_ = adapter; diff --git a/src/data/proxy_dmatrix.h b/src/data/proxy_dmatrix.h index 3bcdfbff3..544c4c81c 100644 --- a/src/data/proxy_dmatrix.h +++ b/src/data/proxy_dmatrix.h @@ -62,6 +62,8 @@ class DMatrixProxy : public DMatrix { #endif // defined(XGBOOST_USE_CUDA) } + void SetColumnarData(StringView interface_str); + void SetArrayData(StringView interface_str); void SetCSRData(char const* c_indptr, char const* c_indices, char const* c_values, bst_feature_t n_features, bool on_host); @@ -151,6 +153,17 @@ decltype(auto) HostAdapterDispatch(DMatrixProxy const* proxy, Fn fn, bool* type_ if (type_error) { *type_error = false; } + } else if (proxy->Adapter().type() == typeid(std::shared_ptr)) { + if constexpr (get_value) { + auto value = std::any_cast>(proxy->Adapter())->Value(); + return fn(value); + } else { + auto value = std::any_cast>(proxy->Adapter()); + return fn(value); + } + if (type_error) { + *type_error = false; + } } else { if (type_error) { *type_error = true; diff --git a/src/data/simple_dmatrix.cc b/src/data/simple_dmatrix.cc index 2bf81892f..99bf67ba0 100644 --- a/src/data/simple_dmatrix.cc +++ b/src/data/simple_dmatrix.cc @@ -1,5 +1,5 @@ /** - * Copyright 2014~2023 by XGBoost Contributors + * Copyright 2014~2023, XGBoost Contributors * \file simple_dmatrix.cc * \brief the input data structure for gradient boosting * \author Tianqi Chen @@ -356,6 +356,8 @@ template SimpleDMatrix::SimpleDMatrix(DataTableAdapter* adapter, float missing, DataSplitMode data_split_mode); template SimpleDMatrix::SimpleDMatrix(FileAdapter* adapter, float missing, int nthread, DataSplitMode data_split_mode); +template SimpleDMatrix::SimpleDMatrix(ColumnarAdapter* adapter, float missing, int nthread, + DataSplitMode data_split_mode); template SimpleDMatrix::SimpleDMatrix( IteratorAdapter* adapter, float missing, int nthread, DataSplitMode data_split_mode); diff --git a/src/predictor/cpu_predictor.cc b/src/predictor/cpu_predictor.cc index 20305850a..d97b527f0 100644 --- a/src/predictor/cpu_predictor.cc +++ b/src/predictor/cpu_predictor.cc @@ -761,6 +761,9 @@ class CPUPredictor : public Predictor { } else if (x.type() == typeid(std::shared_ptr)) { this->DispatchedInplacePredict(x, p_m, model, missing, out_preds, tree_begin, tree_end); + } else if (x.type() == typeid(std::shared_ptr)) { + this->DispatchedInplacePredict( + x, p_m, model, missing, out_preds, tree_begin, tree_end); } else { return false; } diff --git a/tests/python/test_with_modin.py b/tests/python/test_with_modin.py index 3f1f9cf97..75dae0654 100644 --- a/tests/python/test_with_modin.py +++ b/tests/python/test_with_modin.py @@ -16,7 +16,7 @@ pytestmark = pytest.mark.skipif(**tm.no_modin()) class TestModin: @pytest.mark.xfail - def test_modin(self): + def test_modin(self) -> None: df = md.DataFrame([[1, 2., True], [2, 3., False]], columns=['a', 'b', 'c']) dm = xgb.DMatrix(df, label=md.Series([1, 2])) @@ -67,8 +67,8 @@ class TestModin: enable_categorical=False) exp = np.array([[1., 1., 0., 0.], [2., 0., 1., 0.], - [3., 0., 0., 1.]]) - np.testing.assert_array_equal(result, exp) + [3., 0., 0., 1.]]).T + np.testing.assert_array_equal(result.columns, exp) dm = xgb.DMatrix(dummies) assert dm.feature_names == ['B', 'A_X', 'A_Y', 'A_Z'] assert dm.feature_types == ['int', 'int', 'int', 'int'] @@ -108,20 +108,23 @@ class TestModin: def test_modin_label(self): # label must be a single column - df = md.DataFrame({'A': ['X', 'Y', 'Z'], 'B': [1, 2, 3]}) + df = md.DataFrame({"A": ["X", "Y", "Z"], "B": [1, 2, 3]}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, 'label', 'float') + xgb.data._transform_pandas_df(df, False, None, None, "label") # label must be supported dtype - df = md.DataFrame({'A': np.array(['a', 'b', 'c'], dtype=object)}) + df = md.DataFrame({"A": np.array(["a", "b", "c"], dtype=object)}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, 'label', 'float') + xgb.data._transform_pandas_df(df, False, None, None, "label") - df = md.DataFrame({'A': np.array([1, 2, 3], dtype=int)}) - result, _, _ = xgb.data._transform_pandas_df(df, False, None, None, - 'label', 'float') - np.testing.assert_array_equal(result, np.array([[1.], [2.], [3.]], - dtype=float)) + df = md.DataFrame({"A": np.array([1, 2, 3], dtype=int)}) + result, _, _ = xgb.data._transform_pandas_df( + df, False, None, None, "label" + ) + np.testing.assert_array_equal( + np.stack(result.columns, axis=1), + np.array([[1.0], [2.0], [3.0]], dtype=float), + ) dm = xgb.DMatrix(np.random.randn(3, 2), label=df) assert dm.num_row() == 3 assert dm.num_col() == 2 diff --git a/tests/python/test_with_pandas.py b/tests/python/test_with_pandas.py index 6a9ed4a84..c7b99a991 100644 --- a/tests/python/test_with_pandas.py +++ b/tests/python/test_with_pandas.py @@ -105,8 +105,8 @@ class TestPandas: result, _, _ = xgb.data._transform_pandas_df(dummies, enable_categorical=False) exp = np.array( [[1.0, 1.0, 0.0, 0.0], [2.0, 0.0, 1.0, 0.0], [3.0, 0.0, 0.0, 1.0]] - ) - np.testing.assert_array_equal(result, exp) + ).T + np.testing.assert_array_equal(result.columns, exp) dm = xgb.DMatrix(dummies, data_split_mode=data_split_mode) assert dm.num_row() == 3 if data_split_mode == DataSplitMode.ROW: @@ -202,6 +202,20 @@ class TestPandas: else: assert dm.num_col() == 1 * world_size + @pytest.mark.skipif(**tm.no_sklearn()) + def test_multi_target(self) -> None: + from sklearn.datasets import make_regression + + X, y = make_regression(n_samples=1024, n_features=4, n_targets=3) + ydf = pd.DataFrame({i: y[:, i] for i in range(y.shape[1])}) + + Xy = xgb.DMatrix(X, ydf) + assert Xy.num_row() == y.shape[0] + assert Xy.get_label().size == y.shape[0] * y.shape[1] + Xy = xgb.QuantileDMatrix(X, ydf) + assert Xy.num_row() == y.shape[0] + assert Xy.get_label().size == y.shape[0] * y.shape[1] + def test_slice(self): rng = np.random.RandomState(1994) rows = 100 @@ -233,13 +247,14 @@ class TestPandas: X, enable_categorical=True ) - assert transformed[:, 0].min() == 0 + assert transformed.columns[0].min() == 0 # test missing value X = pd.DataFrame({"f0": ["a", "b", np.NaN]}) X["f0"] = X["f0"].astype("category") arr, _, _ = xgb.data._transform_pandas_df(X, enable_categorical=True) - assert not np.any(arr == -1.0) + for c in arr.columns: + assert not np.any(c == -1.0) X = X["f0"] y = y[: X.shape[0]] @@ -273,24 +288,25 @@ class TestPandas: predt_dense = booster.predict(xgb.DMatrix(X.sparse.to_dense())) np.testing.assert_allclose(predt_sparse, predt_dense) - def test_pandas_label(self, data_split_mode=DataSplitMode.ROW): + def test_pandas_label( + self, data_split_mode: DataSplitMode = DataSplitMode.ROW + ) -> None: world_size = xgb.collective.get_world_size() # label must be a single column df = pd.DataFrame({"A": ["X", "Y", "Z"], "B": [1, 2, 3]}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, "label", "float") + xgb.data._transform_pandas_df(df, False, None, None, "label") # label must be supported dtype df = pd.DataFrame({"A": np.array(["a", "b", "c"], dtype=object)}) with pytest.raises(ValueError): - xgb.data._transform_pandas_df(df, False, None, None, "label", "float") + xgb.data._transform_pandas_df(df, False, None, None, "label") df = pd.DataFrame({"A": np.array([1, 2, 3], dtype=int)}) - result, _, _ = xgb.data._transform_pandas_df( - df, False, None, None, "label", "float" - ) + result, _, _ = xgb.data._transform_pandas_df(df, False, None, None, "label") np.testing.assert_array_equal( - result, np.array([[1.0], [2.0], [3.0]], dtype=float) + np.stack(result.columns, axis=1), + np.array([[1.0], [2.0], [3.0]], dtype=float), ) dm = xgb.DMatrix( np.random.randn(3, 2), label=df, data_split_mode=data_split_mode @@ -507,6 +523,35 @@ class TestPandas: np.testing.assert_allclose(m_orig.get_label(), m_etype.get_label()) np.testing.assert_allclose(m_etype.get_label(), y.values) + @pytest.mark.parametrize("DMatrixT", [xgb.DMatrix, xgb.QuantileDMatrix]) + def test_mixed_type(self, DMatrixT: Type[xgb.DMatrix]) -> None: + f0 = np.arange(0, 4) + f1 = pd.Series(f0, dtype="int64[pyarrow]") + f2l = list(f0) + f2l[0] = pd.NA + f2 = pd.Series(f2l, dtype=pd.Int64Dtype()) + + df = pd.DataFrame({"f0": f0}) + df["f2"] = f2 + + m = DMatrixT(df) + assert m.num_col() == df.shape[1] + + df["f1"] = f1 + m = DMatrixT(df) + assert m.num_col() == df.shape[1] + assert m.num_row() == df.shape[0] + assert m.num_nonmissing() == df.size - 1 + assert m.feature_names == list(map(str, df.columns)) + assert m.feature_types == ["int"] * df.shape[1] + + y = f0 + m.set_info(label=y) + booster = xgb.train({}, m) + p0 = booster.inplace_predict(df) + p1 = booster.predict(m) + np.testing.assert_allclose(p0, p1) + @pytest.mark.skipif(tm.is_windows(), reason="Rabit does not run on windows") def test_pandas_column_split(self): tm.run_with_rabit( From ddab49a8be6c37c4d72395a610d7ac72fd4bca15 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 12 Dec 2023 05:13:47 +0100 Subject: [PATCH 045/109] [doc][R] Update arguments for ellipsis in predict (#9868) --- R-package/R/xgb.Booster.R | 2 +- R-package/man/predict.xgb.Booster.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index a5c9e5088..aa5e65e40 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -194,7 +194,7 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' @param strict_shape Default is \code{FALSE}. When it's set to \code{TRUE}, output #' type and shape of prediction are invariant to model type. #' -#' @param ... Parameters passed to \code{predict.xgb.Booster} +#' @param ... Not used. #' #' @details #' diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index ee3b370c4..c1e58f63b 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -66,7 +66,7 @@ to \code{c(1, 1)} XGBoost will use all trees.} \item{strict_shape}{Default is \code{FALSE}. When it's set to \code{TRUE}, output type and shape of prediction are invariant to model type.} -\item{...}{Parameters passed to \code{predict.xgb.Booster}} +\item{...}{Not used.} } \value{ The return type is different depending whether \code{strict_shape} is set to \code{TRUE}. By default, From 43897b829680d241491abe1ecd46b2ba9d338967 Mon Sep 17 00:00:00 2001 From: Dmitry Razdoburdin Date: Tue, 12 Dec 2023 07:41:50 +0100 Subject: [PATCH 046/109] Sycl implementation for objective functions (#9846) --------- Co-authored-by: Dmitry Razdoburdin <> --- include/xgboost/objective.h | 6 + plugin/CMakeLists.txt | 2 + plugin/sycl/objective/multiclass_obj.cc | 210 +++++++++ plugin/sycl/objective/regression_obj.cc | 197 +++++++++ src/objective/multiclass_obj.cu | 11 +- src/objective/multiclass_param.h | 25 ++ src/objective/objective.cc | 22 +- src/objective/regression_obj.cu | 11 +- src/objective/regression_param.h | 25 ++ tests/cpp/objective/test_multiclass_obj.cc | 19 +- tests/cpp/objective/test_multiclass_obj.h | 19 + .../cpp/objective/test_multiclass_obj_cpu.cc | 25 ++ .../cpp/objective/test_multiclass_obj_gpu.cu | 2 +- tests/cpp/objective/test_regression_obj.cc | 414 +----------------- tests/cpp/objective/test_regression_obj.h | 23 + .../cpp/objective/test_regression_obj_cpu.cc | 412 +++++++++++++++++ .../cpp/objective/test_regression_obj_gpu.cu | 2 +- tests/cpp/plugin/test_sycl_multiclass_obj.cc | 28 ++ tests/cpp/plugin/test_sycl_regression_obj.cc | 99 +++++ 19 files changed, 1129 insertions(+), 423 deletions(-) create mode 100644 plugin/sycl/objective/multiclass_obj.cc create mode 100644 plugin/sycl/objective/regression_obj.cc create mode 100644 src/objective/multiclass_param.h create mode 100644 src/objective/regression_param.h create mode 100644 tests/cpp/objective/test_multiclass_obj.h create mode 100644 tests/cpp/objective/test_multiclass_obj_cpu.cc create mode 100644 tests/cpp/objective/test_regression_obj.h create mode 100644 tests/cpp/objective/test_regression_obj_cpu.cc create mode 100644 tests/cpp/plugin/test_sycl_multiclass_obj.cc create mode 100644 tests/cpp/plugin/test_sycl_regression_obj.cc diff --git a/include/xgboost/objective.h b/include/xgboost/objective.h index d2623ee01..b88c30552 100644 --- a/include/xgboost/objective.h +++ b/include/xgboost/objective.h @@ -129,6 +129,12 @@ class ObjFunction : public Configurable { * \param name Name of the objective. */ static ObjFunction* Create(const std::string& name, Context const* ctx); + + /*! + * \brief Return sycl specific implementation name if possible. + * \param name Name of the objective. + */ + static std::string GetSyclImplementationName(const std::string& name); }; /*! diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 0fecb4fb2..e575f1a41 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -1,6 +1,8 @@ if(PLUGIN_SYCL) set(CMAKE_CXX_COMPILER "icpx") add_library(plugin_sycl OBJECT + ${xgboost_SOURCE_DIR}/plugin/sycl/objective/regression_obj.cc + ${xgboost_SOURCE_DIR}/plugin/sycl/objective/multiclass_obj.cc ${xgboost_SOURCE_DIR}/plugin/sycl/device_manager.cc ${xgboost_SOURCE_DIR}/plugin/sycl/predictor/predictor.cc) target_include_directories(plugin_sycl diff --git a/plugin/sycl/objective/multiclass_obj.cc b/plugin/sycl/objective/multiclass_obj.cc new file mode 100644 index 000000000..3104dd35e --- /dev/null +++ b/plugin/sycl/objective/multiclass_obj.cc @@ -0,0 +1,210 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file multiclass_obj.cc + * \brief Definition of multi-class classification objectives. + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" +#include +#pragma GCC diagnostic pop + +#include +#include +#include +#include + +#include "xgboost/parameter.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#include "xgboost/data.h" +#include "../../src/common/math.h" +#pragma GCC diagnostic pop +#include "xgboost/logging.h" +#include "xgboost/objective.h" +#include "xgboost/json.h" +#include "xgboost/span.h" + +#include "../../../src/objective/multiclass_param.h" + +#include "../device_manager.h" +#include + +namespace xgboost { +namespace sycl { +namespace obj { + +DMLC_REGISTRY_FILE_TAG(multiclass_obj_sycl); + +class SoftmaxMultiClassObj : public ObjFunction { + public: + explicit SoftmaxMultiClassObj(bool output_prob) + : output_prob_(output_prob) {} + + void Configure(Args const& args) override { + param_.UpdateAllowUnknown(args); + qu_ = device_manager.GetQueue(ctx_->Device()); + } + + void GetGradient(const HostDeviceVector& preds, + const MetaInfo& info, + int iter, + linalg::Matrix* out_gpair) override { + if (preds.Size() == 0) return; + if (info.labels.Size() == 0) return; + + CHECK(preds.Size() == (static_cast(param_.num_class) * info.labels.Size())) + << "SoftmaxMultiClassObj: label size and pred size does not match.\n" + << "label.Size() * num_class: " + << info.labels.Size() * static_cast(param_.num_class) << "\n" + << "num_class: " << param_.num_class << "\n" + << "preds.Size(): " << preds.Size(); + + const int nclass = param_.num_class; + const auto ndata = static_cast(preds.Size() / nclass); + + out_gpair->Reshape(info.num_row_, static_cast(nclass)); + + const bool is_null_weight = info.weights_.Size() == 0; + if (!is_null_weight) { + CHECK_EQ(info.weights_.Size(), ndata) + << "Number of weights should be equal to number of data points."; + } + + ::sycl::buffer preds_buf(preds.HostPointer(), preds.Size()); + ::sycl::buffer labels_buf(info.labels.Data()->HostPointer(), info.labels.Size()); + ::sycl::buffer out_gpair_buf(out_gpair->Data()->HostPointer(), + out_gpair->Size()); + ::sycl::buffer weights_buf(is_null_weight ? NULL : info.weights_.HostPointer(), + is_null_weight ? 1 : info.weights_.Size()); + + int flag = 1; + { + ::sycl::buffer flag_buf(&flag, 1); + qu_.submit([&](::sycl::handler& cgh) { + auto preds_acc = preds_buf.get_access<::sycl::access::mode::read>(cgh); + auto labels_acc = labels_buf.get_access<::sycl::access::mode::read>(cgh); + auto weights_acc = weights_buf.get_access<::sycl::access::mode::read>(cgh); + auto out_gpair_acc = out_gpair_buf.get_access<::sycl::access::mode::write>(cgh); + auto flag_buf_acc = flag_buf.get_access<::sycl::access::mode::write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + + bst_float const * point = &preds_acc[idx * nclass]; + + // Part of Softmax function + bst_float wmax = std::numeric_limits::min(); + for (int k = 0; k < nclass; k++) { wmax = ::sycl::max(point[k], wmax); } + float wsum = 0.0f; + for (int k = 0; k < nclass; k++) { wsum += ::sycl::exp(point[k] - wmax); } + auto label = labels_acc[idx]; + if (label < 0 || label >= nclass) { + flag_buf_acc[0] = 0; + label = 0; + } + bst_float wt = is_null_weight ? 1.0f : weights_acc[idx]; + for (int k = 0; k < nclass; ++k) { + bst_float p = expf(point[k] - wmax) / static_cast(wsum); + const float eps = 1e-16f; + const bst_float h = ::sycl::max(2.0f * p * (1.0f - p) * wt, eps); + p = label == k ? p - 1.0f : p; + out_gpair_acc[idx * nclass + k] = GradientPair(p * wt, h); + } + }); + }).wait(); + } + // flag_buf is destroyed, content is copyed to the "flag" + + if (flag == 0) { + LOG(FATAL) << "SYCL::SoftmaxMultiClassObj: label must be in [0, num_class)."; + } + } + void PredTransform(HostDeviceVector* io_preds) const override { + this->Transform(io_preds, output_prob_); + } + void EvalTransform(HostDeviceVector* io_preds) override { + this->Transform(io_preds, true); + } + const char* DefaultEvalMetric() const override { + return "mlogloss"; + } + + inline void Transform(HostDeviceVector *io_preds, bool prob) const { + if (io_preds->Size() == 0) return; + const int nclass = param_.num_class; + const auto ndata = static_cast(io_preds->Size() / nclass); + max_preds_.Resize(ndata); + + { + ::sycl::buffer io_preds_buf(io_preds->HostPointer(), io_preds->Size()); + + if (prob) { + qu_.submit([&](::sycl::handler& cgh) { + auto io_preds_acc = io_preds_buf.get_access<::sycl::access::mode::read_write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + auto it = io_preds_acc.begin() + idx * nclass; + common::Softmax(it, it + nclass); + }); + }).wait(); + } else { + ::sycl::buffer max_preds_buf(max_preds_.HostPointer(), max_preds_.Size()); + + qu_.submit([&](::sycl::handler& cgh) { + auto io_preds_acc = io_preds_buf.get_access<::sycl::access::mode::read>(cgh); + auto max_preds_acc = max_preds_buf.get_access<::sycl::access::mode::read_write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + auto it = io_preds_acc.begin() + idx * nclass; + max_preds_acc[idx] = common::FindMaxIndex(it, it + nclass) - it; + }); + }).wait(); + } + } + + if (!prob) { + io_preds->Resize(max_preds_.Size()); + io_preds->Copy(max_preds_); + } + } + + struct ObjInfo Task() const override {return {ObjInfo::kClassification}; } + + void SaveConfig(Json* p_out) const override { + auto& out = *p_out; + if (this->output_prob_) { + out["name"] = String("multi:softprob"); + } else { + out["name"] = String("multi:softmax"); + } + out["softmax_multiclass_param"] = ToJson(param_); + } + + void LoadConfig(Json const& in) override { + FromJson(in["softmax_multiclass_param"], ¶m_); + } + + private: + // output probability + bool output_prob_; + // parameter + xgboost::obj::SoftmaxMultiClassParam param_; + // Cache for max_preds + mutable HostDeviceVector max_preds_; + + sycl::DeviceManager device_manager; + + mutable ::sycl::queue qu_; +}; + +XGBOOST_REGISTER_OBJECTIVE(SoftmaxMultiClass, "multi:softmax_sycl") +.describe("Softmax for multi-class classification, output class index.") +.set_body([]() { return new SoftmaxMultiClassObj(false); }); + +XGBOOST_REGISTER_OBJECTIVE(SoftprobMultiClass, "multi:softprob_sycl") +.describe("Softmax for multi-class classification, output probability distribution.") +.set_body([]() { return new SoftmaxMultiClassObj(true); }); + +} // namespace obj +} // namespace sycl +} // namespace xgboost diff --git a/plugin/sycl/objective/regression_obj.cc b/plugin/sycl/objective/regression_obj.cc new file mode 100644 index 000000000..985498717 --- /dev/null +++ b/plugin/sycl/objective/regression_obj.cc @@ -0,0 +1,197 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file regression_obj.cc + * \brief Definition of regression objectives. + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#pragma GCC diagnostic ignored "-W#pragma-messages" +#include +#include +#pragma GCC diagnostic pop +#include + +#include +#include +#include + +#include "xgboost/host_device_vector.h" +#include "xgboost/json.h" +#include "xgboost/parameter.h" +#include "xgboost/span.h" + +#include "../../src/common/transform.h" +#include "../../src/common/common.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtautological-constant-compare" +#include "../../../src/objective/regression_loss.h" +#pragma GCC diagnostic pop +#include "../../../src/objective/regression_param.h" + +#include "../device_manager.h" + +#include + +namespace xgboost { +namespace sycl { +namespace obj { + +DMLC_REGISTRY_FILE_TAG(regression_obj_sycl); + +template +class RegLossObj : public ObjFunction { + protected: + HostDeviceVector label_correct_; + + public: + RegLossObj() = default; + + void Configure(const std::vector >& args) override { + param_.UpdateAllowUnknown(args); + qu_ = device_manager.GetQueue(ctx_->Device()); + } + + void GetGradient(const HostDeviceVector& preds, + const MetaInfo &info, + int iter, + linalg::Matrix* out_gpair) override { + if (info.labels.Size() == 0) return; + CHECK_EQ(preds.Size(), info.labels.Size()) + << " " << "labels are not correctly provided" + << "preds.size=" << preds.Size() << ", label.size=" << info.labels.Size() << ", " + << "Loss: " << Loss::Name(); + + size_t const ndata = preds.Size(); + auto const n_targets = this->Targets(info); + out_gpair->Reshape(info.num_row_, n_targets); + + // TODO(razdoburdin): add label_correct check + label_correct_.Resize(1); + label_correct_.Fill(1); + + bool is_null_weight = info.weights_.Size() == 0; + + ::sycl::buffer preds_buf(preds.HostPointer(), preds.Size()); + ::sycl::buffer labels_buf(info.labels.Data()->HostPointer(), info.labels.Size()); + ::sycl::buffer out_gpair_buf(out_gpair->Data()->HostPointer(), + out_gpair->Size()); + ::sycl::buffer weights_buf(is_null_weight ? NULL : info.weights_.HostPointer(), + is_null_weight ? 1 : info.weights_.Size()); + + auto scale_pos_weight = param_.scale_pos_weight; + if (!is_null_weight) { + CHECK_EQ(info.weights_.Size(), info.labels.Shape(0)) + << "Number of weights should be equal to number of data points."; + } + + int flag = 1; + { + ::sycl::buffer flag_buf(&flag, 1); + qu_.submit([&](::sycl::handler& cgh) { + auto preds_acc = preds_buf.get_access<::sycl::access::mode::read>(cgh); + auto labels_acc = labels_buf.get_access<::sycl::access::mode::read>(cgh); + auto weights_acc = weights_buf.get_access<::sycl::access::mode::read>(cgh); + auto out_gpair_acc = out_gpair_buf.get_access<::sycl::access::mode::write>(cgh); + auto flag_buf_acc = flag_buf.get_access<::sycl::access::mode::write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + bst_float p = Loss::PredTransform(preds_acc[idx]); + bst_float w = is_null_weight ? 1.0f : weights_acc[idx/n_targets]; + bst_float label = labels_acc[idx]; + if (label == 1.0f) { + w *= scale_pos_weight; + } + if (!Loss::CheckLabel(label)) { + // If there is an incorrect label, the host code will know. + flag_buf_acc[0] = 0; + } + out_gpair_acc[idx] = GradientPair(Loss::FirstOrderGradient(p, label) * w, + Loss::SecondOrderGradient(p, label) * w); + }); + }).wait(); + } + // flag_buf is destroyed, content is copyed to the "flag" + + if (flag == 0) { + LOG(FATAL) << Loss::LabelErrorMsg(); + } + } + + public: + const char* DefaultEvalMetric() const override { + return Loss::DefaultEvalMetric(); + } + + void PredTransform(HostDeviceVector *io_preds) const override { + size_t const ndata = io_preds->Size(); + if (ndata == 0) return; + ::sycl::buffer io_preds_buf(io_preds->HostPointer(), io_preds->Size()); + + qu_.submit([&](::sycl::handler& cgh) { + auto io_preds_acc = io_preds_buf.get_access<::sycl::access::mode::read_write>(cgh); + cgh.parallel_for<>(::sycl::range<1>(ndata), [=](::sycl::id<1> pid) { + int idx = pid[0]; + io_preds_acc[idx] = Loss::PredTransform(io_preds_acc[idx]); + }); + }).wait(); + } + + float ProbToMargin(float base_score) const override { + return Loss::ProbToMargin(base_score); + } + + struct ObjInfo Task() const override { + return Loss::Info(); + }; + + uint32_t Targets(MetaInfo const& info) const override { + // Multi-target regression. + return std::max(static_cast(1), info.labels.Shape(1)); + } + + void SaveConfig(Json* p_out) const override { + auto& out = *p_out; + out["name"] = String(Loss::Name()); + out["reg_loss_param"] = ToJson(param_); + } + + void LoadConfig(Json const& in) override { + FromJson(in["reg_loss_param"], ¶m_); + } + + protected: + xgboost::obj::RegLossParam param_; + sycl::DeviceManager device_manager; + + mutable ::sycl::queue qu_; +}; + +XGBOOST_REGISTER_OBJECTIVE(SquaredLossRegression, + std::string(xgboost::obj::LinearSquareLoss::Name()) + "_sycl") +.describe("Regression with squared error with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(SquareLogError, + std::string(xgboost::obj::SquaredLogError::Name()) + "_sycl") +.describe("Regression with root mean squared logarithmic error with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(LogisticRegression, + std::string(xgboost::obj::LogisticRegression::Name()) + "_sycl") +.describe("Logistic regression for probability regression task with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(LogisticClassification, + std::string(xgboost::obj::LogisticClassification::Name()) + "_sycl") +.describe("Logistic regression for binary classification task with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +XGBOOST_REGISTER_OBJECTIVE(LogisticRaw, + std::string(xgboost::obj::LogisticRaw::Name()) + "_sycl") +.describe("Logistic regression for classification, output score " + "before logistic transformation with SYCL backend.") +.set_body([]() { return new RegLossObj(); }); + +} // namespace obj +} // namespace sycl +} // namespace xgboost diff --git a/src/objective/multiclass_obj.cu b/src/objective/multiclass_obj.cu index 38880f911..1a3df3884 100644 --- a/src/objective/multiclass_obj.cu +++ b/src/objective/multiclass_obj.cu @@ -21,6 +21,8 @@ #include "../common/math.h" #include "../common/transform.h" +#include "multiclass_param.h" + namespace xgboost { namespace obj { @@ -28,15 +30,6 @@ namespace obj { DMLC_REGISTRY_FILE_TAG(multiclass_obj_gpu); #endif // defined(XGBOOST_USE_CUDA) -struct SoftmaxMultiClassParam : public XGBoostParameter { - int num_class; - // declare parameters - DMLC_DECLARE_PARAMETER(SoftmaxMultiClassParam) { - DMLC_DECLARE_FIELD(num_class).set_lower_bound(1) - .describe("Number of output class in the multi-class classification."); - } -}; - class SoftmaxMultiClassObj : public ObjFunction { public: explicit SoftmaxMultiClassObj(bool output_prob) diff --git a/src/objective/multiclass_param.h b/src/objective/multiclass_param.h new file mode 100644 index 000000000..d1dea15fd --- /dev/null +++ b/src/objective/multiclass_param.h @@ -0,0 +1,25 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file multiclass_param.h + * \brief Definition of multi-class classification parameters. + */ +#ifndef XGBOOST_OBJECTIVE_MULTICLASS_PARAM_H_ +#define XGBOOST_OBJECTIVE_MULTICLASS_PARAM_H_ + +#include "xgboost/parameter.h" + +namespace xgboost { +namespace obj { + +struct SoftmaxMultiClassParam : public XGBoostParameter { + int num_class; + // declare parameters + DMLC_DECLARE_PARAMETER(SoftmaxMultiClassParam) { + DMLC_DECLARE_FIELD(num_class).set_lower_bound(1) + .describe("Number of output class in the multi-class classification."); + } +}; + +} // namespace obj +} // namespace xgboost +#endif // XGBOOST_OBJECTIVE_MULTICLASS_PARAM_H_ diff --git a/src/objective/objective.cc b/src/objective/objective.cc index 85cd9803d..1ccf53264 100644 --- a/src/objective/objective.cc +++ b/src/objective/objective.cc @@ -18,7 +18,11 @@ DMLC_REGISTRY_ENABLE(::xgboost::ObjFunctionReg); namespace xgboost { // implement factory functions ObjFunction* ObjFunction::Create(const std::string& name, Context const* ctx) { - auto *e = ::dmlc::Registry< ::xgboost::ObjFunctionReg>::Get()->Find(name); + std::string obj_name = name; + if (ctx->IsSycl()) { + obj_name = GetSyclImplementationName(obj_name); + } + auto *e = ::dmlc::Registry< ::xgboost::ObjFunctionReg>::Get()->Find(obj_name); if (e == nullptr) { std::stringstream ss; for (const auto& entry : ::dmlc::Registry< ::xgboost::ObjFunctionReg>::List()) { @@ -32,6 +36,22 @@ ObjFunction* ObjFunction::Create(const std::string& name, Context const* ctx) { return pobj; } +/* If the objective function has sycl-specific implementation, + * returns the specific implementation name. + * Otherwise return the orginal name without modifications. + */ +std::string ObjFunction::GetSyclImplementationName(const std::string& name) { + const std::string sycl_postfix = "_sycl"; + auto *e = ::dmlc::Registry< ::xgboost::ObjFunctionReg>::Get()->Find(name + sycl_postfix); + if (e != nullptr) { + // Function has specific sycl implementation + return name + sycl_postfix; + } else { + // Function hasn't specific sycl implementation + return name; + } +} + void ObjFunction::InitEstimation(MetaInfo const&, linalg::Tensor* base_score) const { CHECK(base_score); base_score->Reshape(1); diff --git a/src/objective/regression_obj.cu b/src/objective/regression_obj.cu index 5627600fc..df30b354b 100644 --- a/src/objective/regression_obj.cu +++ b/src/objective/regression_obj.cu @@ -35,6 +35,8 @@ #include "xgboost/span.h" #include "xgboost/tree_model.h" // RegTree +#include "regression_param.h" + #if defined(XGBOOST_USE_CUDA) #include "../common/cuda_context.cuh" // for CUDAContext #include "../common/device_helpers.cuh" @@ -53,14 +55,7 @@ void CheckRegInputs(MetaInfo const& info, HostDeviceVector const& pre DMLC_REGISTRY_FILE_TAG(regression_obj_gpu); #endif // defined(XGBOOST_USE_CUDA) -struct RegLossParam : public XGBoostParameter { - float scale_pos_weight; - // declare parameters - DMLC_DECLARE_PARAMETER(RegLossParam) { - DMLC_DECLARE_FIELD(scale_pos_weight).set_default(1.0f).set_lower_bound(0.0f) - .describe("Scale the weight of positive examples by this factor"); - } -}; + template class RegLossObj : public FitIntercept { diff --git a/src/objective/regression_param.h b/src/objective/regression_param.h new file mode 100644 index 000000000..8f5cd7112 --- /dev/null +++ b/src/objective/regression_param.h @@ -0,0 +1,25 @@ +/*! + * Copyright 2015-2023 by Contributors + * \file multiclass_param.h + * \brief Definition of single-value regression and classification parameters. + */ +#ifndef XGBOOST_OBJECTIVE_REGRESSION_PARAM_H_ +#define XGBOOST_OBJECTIVE_REGRESSION_PARAM_H_ + +#include "xgboost/parameter.h" + +namespace xgboost { +namespace obj { + +struct RegLossParam : public XGBoostParameter { + float scale_pos_weight; + // declare parameters + DMLC_DECLARE_PARAMETER(RegLossParam) { + DMLC_DECLARE_FIELD(scale_pos_weight).set_default(1.0f).set_lower_bound(0.0f) + .describe("Scale the weight of positive examples by this factor"); + } +}; + +} // namespace obj +} // namespace xgboost +#endif // XGBOOST_OBJECTIVE_REGRESSION_PARAM_H_ diff --git a/tests/cpp/objective/test_multiclass_obj.cc b/tests/cpp/objective/test_multiclass_obj.cc index d028ef9cf..734e097b8 100644 --- a/tests/cpp/objective/test_multiclass_obj.cc +++ b/tests/cpp/objective/test_multiclass_obj.cc @@ -1,18 +1,18 @@ /*! - * Copyright 2018-2019 XGBoost contributors + * Copyright 2018-2023 XGBoost contributors */ #include #include #include "../../src/common/common.h" #include "../helpers.h" +#include "test_multiclass_obj.h" namespace xgboost { -TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassObjGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestSoftmaxMultiClassObjGPair(const Context* ctx) { std::vector> args {{"num_class", "3"}}; std::unique_ptr obj { - ObjFunction::Create("multi:softmax", &ctx) + ObjFunction::Create("multi:softmax", ctx) }; obj->Configure(args); @@ -35,12 +35,11 @@ TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassObjGPair)) { ASSERT_NO_THROW(obj->DefaultEvalMetric()); } -TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassBasic)) { - auto ctx = MakeCUDACtx(GPUIDX); +void TestSoftmaxMultiClassBasic(const Context* ctx) { std::vector> args{ std::pair("num_class", "3")}; - std::unique_ptr obj{ObjFunction::Create("multi:softmax", &ctx)}; + std::unique_ptr obj{ObjFunction::Create("multi:softmax", ctx)}; obj->Configure(args); CheckConfigReload(obj, "multi:softmax"); @@ -56,13 +55,12 @@ TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassBasic)) { } } -TEST(Objective, DeclareUnifiedTest(SoftprobMultiClassBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestSoftprobMultiClassBasic(const Context* ctx) { std::vector> args { std::pair("num_class", "3")}; std::unique_ptr obj { - ObjFunction::Create("multi:softprob", &ctx) + ObjFunction::Create("multi:softprob", ctx) }; obj->Configure(args); CheckConfigReload(obj, "multi:softprob"); @@ -77,4 +75,5 @@ TEST(Objective, DeclareUnifiedTest(SoftprobMultiClassBasic)) { EXPECT_NEAR(preds[i], out_preds[i], 0.01f); } } + } // namespace xgboost diff --git a/tests/cpp/objective/test_multiclass_obj.h b/tests/cpp/objective/test_multiclass_obj.h new file mode 100644 index 000000000..bf6f9258c --- /dev/null +++ b/tests/cpp/objective/test_multiclass_obj.h @@ -0,0 +1,19 @@ +/** + * Copyright 2020-2023 by XGBoost Contributors + */ +#ifndef XGBOOST_TEST_MULTICLASS_OBJ_H_ +#define XGBOOST_TEST_MULTICLASS_OBJ_H_ + +#include // for Context + +namespace xgboost { + +void TestSoftmaxMultiClassObjGPair(const Context* ctx); + +void TestSoftmaxMultiClassBasic(const Context* ctx); + +void TestSoftprobMultiClassBasic(const Context* ctx); + +} // namespace xgboost + +#endif // XGBOOST_TEST_MULTICLASS_OBJ_H_ diff --git a/tests/cpp/objective/test_multiclass_obj_cpu.cc b/tests/cpp/objective/test_multiclass_obj_cpu.cc new file mode 100644 index 000000000..d3cb8aa1f --- /dev/null +++ b/tests/cpp/objective/test_multiclass_obj_cpu.cc @@ -0,0 +1,25 @@ +/*! + * Copyright 2018-2023 XGBoost contributors + */ +#include +#include + +#include "../helpers.h" +#include "test_multiclass_obj.h" + +namespace xgboost { +TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassObjGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestSoftmaxMultiClassObjGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(SoftmaxMultiClassBasic)) { + auto ctx = MakeCUDACtx(GPUIDX); + TestSoftmaxMultiClassBasic(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(SoftprobMultiClassBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestSoftprobMultiClassBasic(&ctx); +} +} // namespace xgboost diff --git a/tests/cpp/objective/test_multiclass_obj_gpu.cu b/tests/cpp/objective/test_multiclass_obj_gpu.cu index 7567d3242..f80f07ce8 100644 --- a/tests/cpp/objective/test_multiclass_obj_gpu.cu +++ b/tests/cpp/objective/test_multiclass_obj_gpu.cu @@ -1 +1 @@ -#include "test_multiclass_obj.cc" +#include "test_multiclass_obj_cpu.cc" diff --git a/tests/cpp/objective/test_regression_obj.cc b/tests/cpp/objective/test_regression_obj.cc index 35e8287b6..4bd693936 100644 --- a/tests/cpp/objective/test_regression_obj.cc +++ b/tests/cpp/objective/test_regression_obj.cc @@ -14,13 +14,15 @@ #include "xgboost/data.h" #include "xgboost/linalg.h" +#include "test_regression_obj.h" + namespace xgboost { -TEST(Objective, DeclareUnifiedTest(LinearRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; +void TestLinearRegressionGPair(const Context* ctx) { + std::string obj_name = "reg:squarederror"; - std::unique_ptr obj{ObjFunction::Create("reg:squarederror", &ctx)}; + std::vector> args; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); CheckObjFunction(obj, @@ -38,13 +40,13 @@ TEST(Objective, DeclareUnifiedTest(LinearRegressionGPair)) { ASSERT_NO_THROW(obj->DefaultEvalMetric()); } -TEST(Objective, DeclareUnifiedTest(SquaredLog)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestSquaredLog(const Context* ctx) { + std::string obj_name = "reg:squaredlogerror"; std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:squaredlogerror", &ctx)}; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); - CheckConfigReload(obj, "reg:squaredlogerror"); + CheckConfigReload(obj, obj_name); CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred @@ -61,42 +63,13 @@ TEST(Objective, DeclareUnifiedTest(SquaredLog)) { ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"rmsle"}); } -TEST(Objective, DeclareUnifiedTest(PseudoHuber)) { - Context ctx = MakeCUDACtx(GPUIDX); - Args args; - - std::unique_ptr obj{ObjFunction::Create("reg:pseudohubererror", &ctx)}; - obj->Configure(args); - CheckConfigReload(obj, "reg:pseudohubererror"); - - CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights - {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad - {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess - CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {}, // empty weights - {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad - {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess - ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"mphe"}); - - obj->Configure({{"huber_slope", "0.1"}}); - CheckConfigReload(obj, "reg:pseudohubererror"); - CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels - {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights - {-0.099388f, -0.099228f, -0.098639f, -0.089443f, 0.098639f}, // out_grad - {0.0013467f, 0.001908f, 0.004443f, 0.089443f, 0.004443f}); // out_hess -} - -TEST(Objective, DeclareUnifiedTest(LogisticRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestLogisticRegressionGPair(const Context* ctx) { + std::string obj_name = "reg:logistic"; std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:logistic", &ctx)}; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); - CheckConfigReload(obj, "reg:logistic"); + CheckConfigReload(obj, obj_name); CheckObjFunction(obj, { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, // preds @@ -106,13 +79,13 @@ TEST(Objective, DeclareUnifiedTest(LogisticRegressionGPair)) { {0.25f, 0.24f, 0.20f, 0.19f, 0.25f, 0.24f, 0.20f, 0.19f}); // out_hess } -TEST(Objective, DeclareUnifiedTest(LogisticRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestLogisticRegressionBasic(const Context* ctx) { + std::string obj_name = "reg:logistic"; std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:logistic", &ctx)}; + std::unique_ptr obj{ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); - CheckConfigReload(obj, "reg:logistic"); + CheckConfigReload(obj, obj_name); // test label validation EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {10}, {1}, {0}, {0})) @@ -135,12 +108,10 @@ TEST(Objective, DeclareUnifiedTest(LogisticRegressionBasic)) { } } -TEST(Objective, DeclareUnifiedTest(LogisticRawGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); +void TestsLogisticRawGPair(const Context* ctx) { + std::string obj_name = "binary:logitraw"; std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("binary:logitraw", &ctx) - }; + std::unique_ptr obj {ObjFunction::Create(obj_name, ctx)}; obj->Configure(args); CheckObjFunction(obj, @@ -151,347 +122,4 @@ TEST(Objective, DeclareUnifiedTest(LogisticRawGPair)) { {0.25f, 0.24f, 0.20f, 0.19f, 0.25f, 0.24f, 0.20f, 0.19f}); } -TEST(Objective, DeclareUnifiedTest(PoissonRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("count:poisson", &ctx) - }; - - args.emplace_back("max_delta_step", "0.1f"); - obj->Configure(args); - - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - { 1, 1, 1, 1, 1, 1, 1, 1}, - { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, - {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - {}, // Empty weight - { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, - {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); -} - -TEST(Objective, DeclareUnifiedTest(PoissonRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("count:poisson", &ctx) - }; - - obj->Configure(args); - CheckConfigReload(obj, "count:poisson"); - - // test label validation - EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) - << "Expected error when label < 0 for PoissonRegression"; - - // test ProbToMargin - EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); - - // test PredTransform - HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; - std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; - obj->PredTransform(&io_preds); - auto& preds = io_preds.HostVector(); - for (int i = 0; i < static_cast(io_preds.Size()); ++i) { - EXPECT_NEAR(preds[i], out_preds[i], 0.01f); - } -} - -TEST(Objective, DeclareUnifiedTest(GammaRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj { - ObjFunction::Create("reg:gamma", &ctx) - }; - - obj->Configure(args); - CheckObjFunction(obj, - {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - {2, 2, 2, 2, 1, 1, 1, 1}, - {1, 1, 1, 1, 1, 1, 1, 1}, - {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, - {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); - CheckObjFunction(obj, - {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - {2, 2, 2, 2, 1, 1, 1, 1}, - {}, // Empty weight - {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, - {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); -} - -TEST(Objective, DeclareUnifiedTest(GammaRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:gamma", &ctx)}; - - obj->Configure(args); - CheckConfigReload(obj, "reg:gamma"); - - // test label validation - EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {0}, {1}, {0}, {0})) - << "Expected error when label = 0 for GammaRegression"; - EXPECT_ANY_THROW(CheckObjFunction(obj, {-1}, {-1}, {1}, {-1}, {-3})) - << "Expected error when label < 0 for GammaRegression"; - - // test ProbToMargin - EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); - - // test PredTransform - HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; - std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; - obj->PredTransform(&io_preds); - auto& preds = io_preds.HostVector(); - for (int i = 0; i < static_cast(io_preds.Size()); ++i) { - EXPECT_NEAR(preds[i], out_preds[i], 0.01f); - } -} - -TEST(Objective, DeclareUnifiedTest(TweedieRegressionGPair)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; - - args.emplace_back("tweedie_variance_power", "1.1f"); - obj->Configure(args); - - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - { 1, 1, 1, 1, 1, 1, 1, 1}, - { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, - {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, 0, 0, 0, 1, 1, 1, 1}, - {}, // Empty weight. - { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, - {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); - ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"tweedie-nloglik@1.1"}); -} - -#if defined(__CUDACC__) -TEST(Objective, CPU_vs_CUDA) { - Context ctx = MakeCUDACtx(GPUIDX); - - std::unique_ptr obj{ObjFunction::Create("reg:squarederror", &ctx)}; - linalg::Matrix cpu_out_preds; - linalg::Matrix cuda_out_preds; - - constexpr size_t kRows = 400; - constexpr size_t kCols = 100; - auto pdmat = RandomDataGenerator(kRows, kCols, 0).Seed(0).GenerateDMatrix(); - HostDeviceVector preds; - preds.Resize(kRows); - auto& h_preds = preds.HostVector(); - for (size_t i = 0; i < h_preds.size(); ++i) { - h_preds[i] = static_cast(i); - } - auto& info = pdmat->Info(); - - info.labels.Reshape(kRows); - auto& h_labels = info.labels.Data()->HostVector(); - for (size_t i = 0; i < h_labels.size(); ++i) { - h_labels[i] = 1 / static_cast(i+1); - } - - { - // CPU - ctx = ctx.MakeCPU(); - obj->GetGradient(preds, info, 0, &cpu_out_preds); - } - { - // CUDA - ctx = ctx.MakeCUDA(0); - obj->GetGradient(preds, info, 0, &cuda_out_preds); - } - - auto h_cpu_out = cpu_out_preds.HostView(); - auto h_cuda_out = cuda_out_preds.HostView(); - - float sgrad = 0; - float shess = 0; - for (size_t i = 0; i < kRows; ++i) { - sgrad += std::pow(h_cpu_out(i).GetGrad() - h_cuda_out(i).GetGrad(), 2); - shess += std::pow(h_cpu_out(i).GetHess() - h_cuda_out(i).GetHess(), 2); - } - ASSERT_NEAR(sgrad, 0.0f, kRtEps); - ASSERT_NEAR(shess, 0.0f, kRtEps); -} -#endif - -TEST(Objective, DeclareUnifiedTest(TweedieRegressionBasic)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; - - obj->Configure(args); - CheckConfigReload(obj, "reg:tweedie"); - - // test label validation - EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) - << "Expected error when label < 0 for TweedieRegression"; - - // test ProbToMargin - EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); - EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); - - // test PredTransform - HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; - std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; - obj->PredTransform(&io_preds); - auto& preds = io_preds.HostVector(); - for (int i = 0; i < static_cast(io_preds.Size()); ++i) { - EXPECT_NEAR(preds[i], out_preds[i], 0.01f); - } -} - -// CoxRegression not implemented in GPU code, no need for testing. -#if !defined(__CUDACC__) -TEST(Objective, CoxRegressionGPair) { - Context ctx = MakeCUDACtx(GPUIDX); - std::vector> args; - std::unique_ptr obj{ObjFunction::Create("survival:cox", &ctx)}; - - obj->Configure(args); - CheckObjFunction(obj, - { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, - { 0, -2, -2, 2, 3, 5, -10, 100}, - { 1, 1, 1, 1, 1, 1, 1, 1}, - { 0, 0, 0, -0.799f, -0.788f, -0.590f, 0.910f, 1.006f}, - { 0, 0, 0, 0.160f, 0.186f, 0.348f, 0.610f, 0.639f}); -} -#endif - -TEST(Objective, DeclareUnifiedTest(AbsoluteError)) { - Context ctx = MakeCUDACtx(GPUIDX); - std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; - obj->Configure({}); - CheckConfigReload(obj, "reg:absoluteerror"); - - MetaInfo info; - std::vector labels{0.f, 3.f, 2.f, 5.f, 4.f, 7.f}; - info.labels.Reshape(6, 1); - info.labels.Data()->HostVector() = labels; - info.num_row_ = labels.size(); - HostDeviceVector predt{1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; - info.weights_.HostVector() = {1.f, 1.f, 1.f, 1.f, 1.f, 1.f}; - - CheckObjFunction(obj, predt.HostVector(), labels, info.weights_.HostVector(), - {1.f, -1.f, 1.f, -1.f, 1.f, -1.f}, info.weights_.HostVector()); - - RegTree tree; - tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - - HostDeviceVector position(labels.size(), 0); - auto& h_position = position.HostVector(); - for (size_t i = 0; i < labels.size(); ++i) { - if (i < labels.size() / 2) { - h_position[i] = 1; // left - } else { - h_position[i] = 2; // right - } - } - - auto& h_predt = predt.HostVector(); - for (size_t i = 0; i < h_predt.size(); ++i) { - h_predt[i] = labels[i] + i; - } - - tree::TrainParam param; - param.Init(Args{}); - auto lr = param.learning_rate; - - obj->UpdateTreeLeaf(position, info, param.learning_rate, predt, 0, &tree); - ASSERT_EQ(tree[1].LeafValue(), -1.0f * lr); - ASSERT_EQ(tree[2].LeafValue(), -4.0f * lr); -} - -TEST(Objective, DeclareUnifiedTest(AbsoluteErrorLeaf)) { - Context ctx = MakeCUDACtx(GPUIDX); - bst_target_t constexpr kTargets = 3, kRows = 16; - std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; - obj->Configure({}); - - MetaInfo info; - info.num_row_ = kRows; - info.labels.Reshape(16, kTargets); - HostDeviceVector predt(info.labels.Size()); - - for (bst_target_t t{0}; t < kTargets; ++t) { - auto h_labels = info.labels.HostView().Slice(linalg::All(), t); - std::iota(linalg::begin(h_labels), linalg::end(h_labels), 0); - - auto h_predt = - linalg::MakeTensorView(&ctx, predt.HostSpan(), kRows, kTargets).Slice(linalg::All(), t); - for (size_t i = 0; i < h_predt.Size(); ++i) { - h_predt(i) = h_labels(i) + i; - } - - HostDeviceVector position(h_labels.Size(), 0); - auto& h_position = position.HostVector(); - for (int32_t i = 0; i < 3; ++i) { - h_position[i] = ~i; // negation for sampled nodes. - } - for (size_t i = 3; i < 8; ++i) { - h_position[i] = 3; - } - // empty leaf for node 4 - for (size_t i = 8; i < 13; ++i) { - h_position[i] = 5; - } - for (size_t i = 13; i < h_labels.Size(); ++i) { - h_position[i] = 6; - } - - RegTree tree; - tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - tree.ExpandNode(1, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - tree.ExpandNode(2, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); - ASSERT_EQ(tree.GetNumLeaves(), 4); - - auto empty_leaf = tree[4].LeafValue(); - - tree::TrainParam param; - param.Init(Args{}); - auto lr = param.learning_rate; - - obj->UpdateTreeLeaf(position, info, lr, predt, t, &tree); - ASSERT_EQ(tree[3].LeafValue(), -5.0f * lr); - ASSERT_EQ(tree[4].LeafValue(), empty_leaf * lr); - ASSERT_EQ(tree[5].LeafValue(), -10.0f * lr); - ASSERT_EQ(tree[6].LeafValue(), -14.0f * lr); - } -} - -TEST(Adaptive, DeclareUnifiedTest(MissingLeaf)) { - std::vector missing{1, 3}; - - std::vector h_nidx = {2, 4, 5}; - std::vector h_nptr = {0, 4, 8, 16}; - - obj::detail::FillMissingLeaf(missing, &h_nidx, &h_nptr); - - ASSERT_EQ(h_nidx[0], missing[0]); - ASSERT_EQ(h_nidx[2], missing[1]); - ASSERT_EQ(h_nidx[1], 2); - ASSERT_EQ(h_nidx[3], 4); - ASSERT_EQ(h_nidx[4], 5); - - ASSERT_EQ(h_nptr[0], 0); - ASSERT_EQ(h_nptr[1], 0); // empty - ASSERT_EQ(h_nptr[2], 4); - ASSERT_EQ(h_nptr[3], 4); // empty - ASSERT_EQ(h_nptr[4], 8); - ASSERT_EQ(h_nptr[5], 16); -} } // namespace xgboost diff --git a/tests/cpp/objective/test_regression_obj.h b/tests/cpp/objective/test_regression_obj.h new file mode 100644 index 000000000..41f7c370e --- /dev/null +++ b/tests/cpp/objective/test_regression_obj.h @@ -0,0 +1,23 @@ +/** + * Copyright 2020-2023 by XGBoost Contributors + */ +#ifndef XGBOOST_TEST_REGRESSION_OBJ_H_ +#define XGBOOST_TEST_REGRESSION_OBJ_H_ + +#include // for Context + +namespace xgboost { + +void TestLinearRegressionGPair(const Context* ctx); + +void TestSquaredLog(const Context* ctx); + +void TestLogisticRegressionGPair(const Context* ctx); + +void TestLogisticRegressionBasic(const Context* ctx); + +void TestsLogisticRawGPair(const Context* ctx); + +} // namespace xgboost + +#endif // XGBOOST_TEST_REGRESSION_OBJ_H_ diff --git a/tests/cpp/objective/test_regression_obj_cpu.cc b/tests/cpp/objective/test_regression_obj_cpu.cc new file mode 100644 index 000000000..3613d0d90 --- /dev/null +++ b/tests/cpp/objective/test_regression_obj_cpu.cc @@ -0,0 +1,412 @@ +/*! + * Copyright 2018-2023 XGBoost contributors + */ +#include +#include +#include + +#include "../../../src/objective/adaptive.h" +#include "../../../src/tree/param.h" // for TrainParam +#include "../helpers.h" + +#include "test_regression_obj.h" + +namespace xgboost { +TEST(Objective, DeclareUnifiedTest(LinearRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestLinearRegressionGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(SquaredLog)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestSquaredLog(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(PseudoHuber)) { + Context ctx = MakeCUDACtx(GPUIDX); + Args args; + + std::unique_ptr obj{ObjFunction::Create("reg:pseudohubererror", &ctx)}; + obj->Configure(args); + CheckConfigReload(obj, "reg:pseudohubererror"); + + CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights + {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad + {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess + CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels + {}, // empty weights + {-0.668965f, -0.624695f, -0.514496f, -0.196116f, 0.514496f}, // out_grad + {0.410660f, 0.476140f, 0.630510f, 0.9428660f, 0.630510f}); // out_hess + ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"mphe"}); + + obj->Configure({{"huber_slope", "0.1"}}); + CheckConfigReload(obj, "reg:pseudohubererror"); + CheckObjFunction(obj, {0.1f, 0.2f, 0.4f, 0.8f, 1.6f}, // pred + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // labels + {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // weights + {-0.099388f, -0.099228f, -0.098639f, -0.089443f, 0.098639f}, // out_grad + {0.0013467f, 0.001908f, 0.004443f, 0.089443f, 0.004443f}); // out_hess +} + +TEST(Objective, DeclareUnifiedTest(LogisticRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestLogisticRegressionGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(LogisticRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestLogisticRegressionBasic(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(LogisticRawGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + TestsLogisticRawGPair(&ctx); +} + +TEST(Objective, DeclareUnifiedTest(PoissonRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj { + ObjFunction::Create("count:poisson", &ctx) + }; + + args.emplace_back("max_delta_step", "0.1f"); + obj->Configure(args); + + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + { 1, 1, 1, 1, 1, 1, 1, 1}, + { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, + {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + {}, // Empty weight + { 1, 1.10f, 2.45f, 2.71f, 0, 0.10f, 1.45f, 1.71f}, + {1.10f, 1.22f, 2.71f, 3.00f, 1.10f, 1.22f, 2.71f, 3.00f}); +} + +TEST(Objective, DeclareUnifiedTest(PoissonRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj { + ObjFunction::Create("count:poisson", &ctx) + }; + + obj->Configure(args); + CheckConfigReload(obj, "count:poisson"); + + // test label validation + EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) + << "Expected error when label < 0 for PoissonRegression"; + + // test ProbToMargin + EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); + + // test PredTransform + HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; + std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; + obj->PredTransform(&io_preds); + auto& preds = io_preds.HostVector(); + for (int i = 0; i < static_cast(io_preds.Size()); ++i) { + EXPECT_NEAR(preds[i], out_preds[i], 0.01f); + } +} + +TEST(Objective, DeclareUnifiedTest(GammaRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj { + ObjFunction::Create("reg:gamma", &ctx) + }; + + obj->Configure(args); + CheckObjFunction(obj, + {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + {2, 2, 2, 2, 1, 1, 1, 1}, + {1, 1, 1, 1, 1, 1, 1, 1}, + {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, + {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); + CheckObjFunction(obj, + {0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + {2, 2, 2, 2, 1, 1, 1, 1}, + {}, // Empty weight + {-1, -0.809, 0.187, 0.264, 0, 0.09f, 0.59f, 0.63f}, + {2, 1.809, 0.813, 0.735, 1, 0.90f, 0.40f, 0.36f}); +} + +TEST(Objective, DeclareUnifiedTest(GammaRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("reg:gamma", &ctx)}; + + obj->Configure(args); + CheckConfigReload(obj, "reg:gamma"); + + // test label validation + EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {0}, {1}, {0}, {0})) + << "Expected error when label = 0 for GammaRegression"; + EXPECT_ANY_THROW(CheckObjFunction(obj, {-1}, {-1}, {1}, {-1}, {-3})) + << "Expected error when label < 0 for GammaRegression"; + + // test ProbToMargin + EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); + + // test PredTransform + HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; + std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; + obj->PredTransform(&io_preds); + auto& preds = io_preds.HostVector(); + for (int i = 0; i < static_cast(io_preds.Size()); ++i) { + EXPECT_NEAR(preds[i], out_preds[i], 0.01f); + } +} + +TEST(Objective, DeclareUnifiedTest(TweedieRegressionGPair)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; + + args.emplace_back("tweedie_variance_power", "1.1f"); + obj->Configure(args); + + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + { 1, 1, 1, 1, 1, 1, 1, 1}, + { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, + {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, 0, 0, 0, 1, 1, 1, 1}, + {}, // Empty weight. + { 1, 1.09f, 2.24f, 2.45f, 0, 0.10f, 1.33f, 1.55f}, + {0.89f, 0.98f, 2.02f, 2.21f, 1, 1.08f, 2.11f, 2.30f}); + ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"tweedie-nloglik@1.1"}); +} + +#if defined(__CUDACC__) +TEST(Objective, CPU_vs_CUDA) { + Context ctx = MakeCUDACtx(GPUIDX); + + std::unique_ptr obj{ObjFunction::Create("reg:squarederror", &ctx)}; + linalg::Matrix cpu_out_preds; + linalg::Matrix cuda_out_preds; + + constexpr size_t kRows = 400; + constexpr size_t kCols = 100; + auto pdmat = RandomDataGenerator(kRows, kCols, 0).Seed(0).GenerateDMatrix(); + HostDeviceVector preds; + preds.Resize(kRows); + auto& h_preds = preds.HostVector(); + for (size_t i = 0; i < h_preds.size(); ++i) { + h_preds[i] = static_cast(i); + } + auto& info = pdmat->Info(); + + info.labels.Reshape(kRows); + auto& h_labels = info.labels.Data()->HostVector(); + for (size_t i = 0; i < h_labels.size(); ++i) { + h_labels[i] = 1 / static_cast(i+1); + } + + { + // CPU + ctx = ctx.MakeCPU(); + obj->GetGradient(preds, info, 0, &cpu_out_preds); + } + { + // CUDA + ctx = ctx.MakeCUDA(0); + obj->GetGradient(preds, info, 0, &cuda_out_preds); + } + + auto h_cpu_out = cpu_out_preds.HostView(); + auto h_cuda_out = cuda_out_preds.HostView(); + + float sgrad = 0; + float shess = 0; + for (size_t i = 0; i < kRows; ++i) { + sgrad += std::pow(h_cpu_out(i).GetGrad() - h_cuda_out(i).GetGrad(), 2); + shess += std::pow(h_cpu_out(i).GetHess() - h_cuda_out(i).GetHess(), 2); + } + ASSERT_NEAR(sgrad, 0.0f, kRtEps); + ASSERT_NEAR(shess, 0.0f, kRtEps); +} +#endif + +TEST(Objective, DeclareUnifiedTest(TweedieRegressionBasic)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("reg:tweedie", &ctx)}; + + obj->Configure(args); + CheckConfigReload(obj, "reg:tweedie"); + + // test label validation + EXPECT_ANY_THROW(CheckObjFunction(obj, {0}, {-1}, {1}, {0}, {0})) + << "Expected error when label < 0 for TweedieRegression"; + + // test ProbToMargin + EXPECT_NEAR(obj->ProbToMargin(0.1f), -2.30f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.5f), -0.69f, 0.01f); + EXPECT_NEAR(obj->ProbToMargin(0.9f), -0.10f, 0.01f); + + // test PredTransform + HostDeviceVector io_preds = {0, 0.1f, 0.5f, 0.9f, 1}; + std::vector out_preds = {1, 1.10f, 1.64f, 2.45f, 2.71f}; + obj->PredTransform(&io_preds); + auto& preds = io_preds.HostVector(); + for (int i = 0; i < static_cast(io_preds.Size()); ++i) { + EXPECT_NEAR(preds[i], out_preds[i], 0.01f); + } +} + +// CoxRegression not implemented in GPU code, no need for testing. +#if !defined(__CUDACC__) +TEST(Objective, CoxRegressionGPair) { + Context ctx = MakeCUDACtx(GPUIDX); + std::vector> args; + std::unique_ptr obj{ObjFunction::Create("survival:cox", &ctx)}; + + obj->Configure(args); + CheckObjFunction(obj, + { 0, 0.1f, 0.9f, 1, 0, 0.1f, 0.9f, 1}, + { 0, -2, -2, 2, 3, 5, -10, 100}, + { 1, 1, 1, 1, 1, 1, 1, 1}, + { 0, 0, 0, -0.799f, -0.788f, -0.590f, 0.910f, 1.006f}, + { 0, 0, 0, 0.160f, 0.186f, 0.348f, 0.610f, 0.639f}); +} +#endif + +TEST(Objective, DeclareUnifiedTest(AbsoluteError)) { + Context ctx = MakeCUDACtx(GPUIDX); + std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; + obj->Configure({}); + CheckConfigReload(obj, "reg:absoluteerror"); + + MetaInfo info; + std::vector labels{0.f, 3.f, 2.f, 5.f, 4.f, 7.f}; + info.labels.Reshape(6, 1); + info.labels.Data()->HostVector() = labels; + info.num_row_ = labels.size(); + HostDeviceVector predt{1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; + info.weights_.HostVector() = {1.f, 1.f, 1.f, 1.f, 1.f, 1.f}; + + CheckObjFunction(obj, predt.HostVector(), labels, info.weights_.HostVector(), + {1.f, -1.f, 1.f, -1.f, 1.f, -1.f}, info.weights_.HostVector()); + + RegTree tree; + tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + + HostDeviceVector position(labels.size(), 0); + auto& h_position = position.HostVector(); + for (size_t i = 0; i < labels.size(); ++i) { + if (i < labels.size() / 2) { + h_position[i] = 1; // left + } else { + h_position[i] = 2; // right + } + } + + auto& h_predt = predt.HostVector(); + for (size_t i = 0; i < h_predt.size(); ++i) { + h_predt[i] = labels[i] + i; + } + + tree::TrainParam param; + param.Init(Args{}); + auto lr = param.learning_rate; + + obj->UpdateTreeLeaf(position, info, param.learning_rate, predt, 0, &tree); + ASSERT_EQ(tree[1].LeafValue(), -1.0f * lr); + ASSERT_EQ(tree[2].LeafValue(), -4.0f * lr); +} + +TEST(Objective, DeclareUnifiedTest(AbsoluteErrorLeaf)) { + Context ctx = MakeCUDACtx(GPUIDX); + bst_target_t constexpr kTargets = 3, kRows = 16; + std::unique_ptr obj{ObjFunction::Create("reg:absoluteerror", &ctx)}; + obj->Configure({}); + + MetaInfo info; + info.num_row_ = kRows; + info.labels.Reshape(16, kTargets); + HostDeviceVector predt(info.labels.Size()); + + for (bst_target_t t{0}; t < kTargets; ++t) { + auto h_labels = info.labels.HostView().Slice(linalg::All(), t); + std::iota(linalg::begin(h_labels), linalg::end(h_labels), 0); + + auto h_predt = + linalg::MakeTensorView(&ctx, predt.HostSpan(), kRows, kTargets).Slice(linalg::All(), t); + for (size_t i = 0; i < h_predt.Size(); ++i) { + h_predt(i) = h_labels(i) + i; + } + + HostDeviceVector position(h_labels.Size(), 0); + auto& h_position = position.HostVector(); + for (int32_t i = 0; i < 3; ++i) { + h_position[i] = ~i; // negation for sampled nodes. + } + for (size_t i = 3; i < 8; ++i) { + h_position[i] = 3; + } + // empty leaf for node 4 + for (size_t i = 8; i < 13; ++i) { + h_position[i] = 5; + } + for (size_t i = 13; i < h_labels.Size(); ++i) { + h_position[i] = 6; + } + + RegTree tree; + tree.ExpandNode(0, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + tree.ExpandNode(1, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + tree.ExpandNode(2, /*split_index=*/1, 2, true, 0.0f, 2.f, 3.f, 4.f, 2.f, 1.f, 1.f); + ASSERT_EQ(tree.GetNumLeaves(), 4); + + auto empty_leaf = tree[4].LeafValue(); + + tree::TrainParam param; + param.Init(Args{}); + auto lr = param.learning_rate; + + obj->UpdateTreeLeaf(position, info, lr, predt, t, &tree); + ASSERT_EQ(tree[3].LeafValue(), -5.0f * lr); + ASSERT_EQ(tree[4].LeafValue(), empty_leaf * lr); + ASSERT_EQ(tree[5].LeafValue(), -10.0f * lr); + ASSERT_EQ(tree[6].LeafValue(), -14.0f * lr); + } +} + +TEST(Adaptive, DeclareUnifiedTest(MissingLeaf)) { + std::vector missing{1, 3}; + + std::vector h_nidx = {2, 4, 5}; + std::vector h_nptr = {0, 4, 8, 16}; + + obj::detail::FillMissingLeaf(missing, &h_nidx, &h_nptr); + + ASSERT_EQ(h_nidx[0], missing[0]); + ASSERT_EQ(h_nidx[2], missing[1]); + ASSERT_EQ(h_nidx[1], 2); + ASSERT_EQ(h_nidx[3], 4); + ASSERT_EQ(h_nidx[4], 5); + + ASSERT_EQ(h_nptr[0], 0); + ASSERT_EQ(h_nptr[1], 0); // empty + ASSERT_EQ(h_nptr[2], 4); + ASSERT_EQ(h_nptr[3], 4); // empty + ASSERT_EQ(h_nptr[4], 8); + ASSERT_EQ(h_nptr[5], 16); +} +} // namespace xgboost diff --git a/tests/cpp/objective/test_regression_obj_gpu.cu b/tests/cpp/objective/test_regression_obj_gpu.cu index 38f29b8a8..746468f71 100644 --- a/tests/cpp/objective/test_regression_obj_gpu.cu +++ b/tests/cpp/objective/test_regression_obj_gpu.cu @@ -3,4 +3,4 @@ */ // Dummy file to keep the CUDA tests. -#include "test_regression_obj.cc" +#include "test_regression_obj_cpu.cc" diff --git a/tests/cpp/plugin/test_sycl_multiclass_obj.cc b/tests/cpp/plugin/test_sycl_multiclass_obj.cc new file mode 100644 index 000000000..d809ecad3 --- /dev/null +++ b/tests/cpp/plugin/test_sycl_multiclass_obj.cc @@ -0,0 +1,28 @@ +/*! + * Copyright 2018-2023 XGBoost contributors + */ +#include +#include + +#include "../objective/test_multiclass_obj.h" + +namespace xgboost { + +TEST(SyclObjective, SoftmaxMultiClassObjGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSoftmaxMultiClassObjGPair(&ctx); +} + +TEST(SyclObjective, SoftmaxMultiClassBasic) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSoftmaxMultiClassObjGPair(&ctx); +} + +TEST(SyclObjective, SoftprobMultiClassBasic) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSoftprobMultiClassBasic(&ctx); +} +} // namespace xgboost diff --git a/tests/cpp/plugin/test_sycl_regression_obj.cc b/tests/cpp/plugin/test_sycl_regression_obj.cc new file mode 100644 index 000000000..66b4ea508 --- /dev/null +++ b/tests/cpp/plugin/test_sycl_regression_obj.cc @@ -0,0 +1,99 @@ +/*! + * Copyright 2017-2019 XGBoost contributors + */ +#include +#include +#include + +#include "../helpers.h" +#include "../objective/test_regression_obj.h" + +namespace xgboost { + +TEST(SyclObjective, LinearRegressionGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestLinearRegressionGPair(&ctx); +} + +TEST(SyclObjective, SquaredLog) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestSquaredLog(&ctx); +} + +TEST(SyclObjective, LogisticRegressionGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestLogisticRegressionGPair(&ctx); +} + +TEST(SyclObjective, LogisticRegressionBasic) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + + TestLogisticRegressionBasic(&ctx); +} + +TEST(SyclObjective, LogisticRawGPair) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + TestsLogisticRawGPair(&ctx); +} + +TEST(SyclObjective, CPUvsSycl) { + Context ctx; + ctx.UpdateAllowUnknown(Args{{"device", "sycl"}}); + ObjFunction * obj_sycl = + ObjFunction::Create("reg:squarederror_sycl", &ctx); + + ctx = ctx.MakeCPU(); + ObjFunction * obj_cpu = + ObjFunction::Create("reg:squarederror", &ctx); + + linalg::Matrix cpu_out_preds; + linalg::Matrix sycl_out_preds; + + constexpr size_t kRows = 400; + constexpr size_t kCols = 100; + auto pdmat = RandomDataGenerator(kRows, kCols, 0).Seed(0).GenerateDMatrix(); + HostDeviceVector preds; + preds.Resize(kRows); + auto& h_preds = preds.HostVector(); + for (size_t i = 0; i < h_preds.size(); ++i) { + h_preds[i] = static_cast(i); + } + auto& info = pdmat->Info(); + + info.labels.Reshape(kRows, 1); + auto& h_labels = info.labels.Data()->HostVector(); + for (size_t i = 0; i < h_labels.size(); ++i) { + h_labels[i] = 1 / static_cast(i+1); + } + + { + // CPU + obj_cpu->GetGradient(preds, info, 0, &cpu_out_preds); + } + { + // sycl + obj_sycl->GetGradient(preds, info, 0, &sycl_out_preds); + } + + auto h_cpu_out = cpu_out_preds.HostView(); + auto h_sycl_out = sycl_out_preds.HostView(); + + float sgrad = 0; + float shess = 0; + for (size_t i = 0; i < kRows; ++i) { + sgrad += std::pow(h_cpu_out(i).GetGrad() - h_sycl_out(i).GetGrad(), 2); + shess += std::pow(h_cpu_out(i).GetHess() - h_sycl_out(i).GetHess(), 2); + } + ASSERT_NEAR(sgrad, 0.0f, kRtEps); + ASSERT_NEAR(shess, 0.0f, kRtEps); + + delete obj_cpu; + delete obj_sycl; +} + +} // namespace xgboost From d530d37707743954e06f9e825e2f9084c771cb07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 22:58:03 +0900 Subject: [PATCH 047/109] [CI] Update RAPIDS to latest stable (#9857) --- tests/buildkite/conftest.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/buildkite/conftest.sh b/tests/buildkite/conftest.sh index 881f98672..3df79b58d 100755 --- a/tests/buildkite/conftest.sh +++ b/tests/buildkite/conftest.sh @@ -24,7 +24,7 @@ set -x CUDA_VERSION=11.8.0 NCCL_VERSION=2.16.5-1 -RAPIDS_VERSION=23.10 +RAPIDS_VERSION=23.12 SPARK_VERSION=3.4.0 JDK_VERSION=8 R_VERSION=4.3.2 From 42173d7bc34e5ea791d0afaee1e08f82bcfc8b3b Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 13 Dec 2023 01:39:41 +0100 Subject: [PATCH 048/109] [doc] Clarify the effect of `enable_categorical` (#9877) --- R-package/R/xgb.DMatrix.R | 14 ++++++++++++-- R-package/man/xgb.DMatrix.Rd | 14 ++++++++++++-- python-package/xgboost/core.py | 14 ++++++++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 602164afe..fead30413 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -27,14 +27,24 @@ #' @param label_lower_bound Lower bound for survival training. #' @param label_upper_bound Upper bound for survival training. #' @param feature_weights Set feature weights for column sampling. +#' @param enable_categorical Experimental support of specializing for categorical features. +#' +#' If passing 'TRUE' and 'data' is a data frame, +#' columns of categorical types will automatically +#' be set to be of categorical type (feature_type='c') in the resulting DMatrix. +#' +#' If passing 'FALSE' and 'data' is a data frame with categorical columns, +#' it will result in an error being thrown. +#' +#' If 'data' is not a data frame, this argument is ignored. +#' +#' JSON/UBJSON serialization format is required for this. #' #' @details #' Note that DMatrix objects are not serializable through R functions such as \code{saveRDS} or \code{save}. #' If a DMatrix gets serialized and then de-serialized (for example, when saving data in an R session or caching #' chunks in an Rmd file), the resulting object will not be usable anymore and will need to be reconstructed #' from the original source of data. -#' @param enable_categorical Experimental support of specializing for -#' categorical features. JSON/UBJSON serialization format is required. #' #' @examples #' data(agaricus.train, package='xgboost') diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 619f5d730..95cc8d3cd 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -58,8 +58,18 @@ frame and matrix.} \item{feature_weights}{Set feature weights for column sampling.} -\item{enable_categorical}{Experimental support of specializing for -categorical features. JSON/UBJSON serialization format is required.} +\item{enable_categorical}{Experimental support of specializing for categorical features. + + If passing 'TRUE' and 'data' is a data frame, + columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting DMatrix. + + If passing 'FALSE' and 'data' is a data frame with categorical columns, + it will result in an error being thrown. + + If 'data' is not a data frame, this argument is ignored. + + JSON/UBJSON serialization format is required for this.} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 3c864a1c8..1bddadbbe 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -822,8 +822,18 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m .. note:: This parameter is experimental - Experimental support of specializing for categorical features. JSON/UBJSON - serialization format is required. + Experimental support of specializing for categorical features. + + If passing 'True' and 'data' is a data frame (from supported libraries + such as Pandas or Modin), columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting DMatrix. + + If passing 'False' and 'data' is a data frame with categorical columns, + it will result in an error being thrown. + + If 'data' is not a data frame, this argument is ignored. + + JSON/UBJSON serialization format is required for this. """ if group is not None and qid is not None: From 936b22fdf30b628dc307add837b29b932d4122be Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Wed, 13 Dec 2023 15:25:51 -0800 Subject: [PATCH 049/109] [CI] Upload libxgboost4j.dylib (M1) to S3 bucket (#9886) * [CI] Upload libxgboost4j.dylib (M1) to S3 bucket * Fix typo --- tests/buildkite/test-macos-m1-clang11.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/buildkite/test-macos-m1-clang11.sh b/tests/buildkite/test-macos-m1-clang11.sh index 97b4fef93..401701b42 100755 --- a/tests/buildkite/test-macos-m1-clang11.sh +++ b/tests/buildkite/test-macos-m1-clang11.sh @@ -24,6 +24,20 @@ popd rm -rf build set +x +echo "--- Upload Python wheel" +set -x +pushd lib +mv -v libxgboost4j.dylib libxgboost4j_m1_${BUILDKITE_COMMIT}.dylib +buildkite-agent artifact upload libxgboost4j_m1_${BUILDKITE_COMMIT}.dylib +if [[ ($is_pull_request == 0) && ($is_release_branch == 1) ]] +then + aws s3 cp libxgboost4j_m1_${BUILDKITE_COMMIT}.dylib \ + s3://xgboost-nightly-builds/${BRANCH_NAME}/libxgboost4j/ \ + --acl public-read --no-progress +fi +popd +set +x + # Ensure that XGBoost can be built with Clang 11 echo "--- Build and Test XGBoost with MacOS M1, Clang 11" set -x From cd473c9da39e56cf3a59a488511256a0f076d734 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Thu, 14 Dec 2023 02:16:53 +0100 Subject: [PATCH 050/109] [R] enable multi-dimensional base_margin (#9885) --- R-package/R/xgb.DMatrix.R | 6 +++--- R-package/man/xgb.DMatrix.Rd | 4 +++- R-package/tests/testthat/test_dmatrix.R | 26 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index fead30413..4b2bb0d2a 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -16,6 +16,8 @@ #' only care about the relative ordering of data points within each group, #' so it doesn't make sense to assign weights to individual data points. #' @param base_margin Base margin used for boosting from existing model. +#' +#' In the case of multi-output models, one can also pass multi-dimensional base_margin. #' @param missing a float value to represents missing values in data (used only when input is a dense matrix). #' It is useful when a 0 or some other extreme value represents missing values in data. #' @param silent whether to suppress printing an informational message after loading from a file. @@ -439,9 +441,7 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { return(TRUE) } if (name == "base_margin") { - # if (length(info)!=nrow(object)) - # stop("The length of base margin must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "group") { diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 95cc8d3cd..a1ef39f0b 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -36,7 +36,9 @@ is assigned to each group (not each data point). This is because we only care about the relative ordering of data points within each group, so it doesn't make sense to assign weights to individual data points.} -\item{base_margin}{Base margin used for boosting from existing model.} +\item{base_margin}{Base margin used for boosting from existing model. + + In the case of multi-output models, one can also pass multi-dimensional base_margin.} \item{missing}{a float value to represents missing values in data (used only when input is a dense matrix). It is useful when a 0 or some other extreme value represents missing values in data.} diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 87a73d84b..55a699687 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -349,3 +349,29 @@ test_that("xgb.DMatrix: data.frame", { m <- xgb.DMatrix(df, enable_categorical = TRUE) expect_equal(getinfo(m, "feature_type"), c("c", "c")) }) + +test_that("xgb.DMatrix: can take multi-dimensional 'base_margin'", { + set.seed(123) + x <- matrix(rnorm(100 * 10), nrow = 100) + y <- matrix(rnorm(100 * 2), nrow = 100) + b <- matrix(rnorm(100 * 2), nrow = 100) + model <- xgb.train( + data = xgb.DMatrix(data = x, label = y, nthread = n_threads), + params = list( + objective = "reg:squarederror", + tree_method = "hist", + multi_strategy = "multi_output_tree", + base_score = 0, + nthread = n_threads + ), + nround = 1 + ) + pred_only_x <- predict(model, x, nthread = n_threads, reshape = TRUE) + pred_w_base <- predict( + model, + xgb.DMatrix(data = x, base_margin = b, nthread = n_threads), + nthread = n_threads, + reshape = TRUE + ) + expect_equal(pred_only_x, pred_w_base - b, tolerance = 1e-5) +}) From 1aa8c8d9be17d985c77a403e0b3c9b794f363843 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 14 Dec 2023 18:28:37 +0800 Subject: [PATCH 051/109] Support more scipy types. (#9881) --- python-package/xgboost/data.py | 73 ++++++++++++++++++++------- tests/python/test_dmatrix.py | 33 ------------- tests/python/test_with_scipy.py | 87 +++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 50 deletions(-) create mode 100644 tests/python/test_with_scipy.py diff --git a/python-package/xgboost/data.py b/python-package/xgboost/data.py index 74b2966fe..05337e788 100644 --- a/python-package/xgboost/data.py +++ b/python-package/xgboost/data.py @@ -57,12 +57,23 @@ def _check_data_shape(data: DataType) -> None: raise ValueError("Please reshape the input data into 2-dimensional matrix.") -def _is_scipy_csr(data: DataType) -> bool: +def is_scipy_csr(data: DataType) -> bool: + """Predicate for scipy CSR input.""" + is_array = False + is_matrix = False try: - import scipy.sparse + from scipy.sparse import csr_array + + is_array = isinstance(data, csr_array) except ImportError: - return False - return isinstance(data, scipy.sparse.csr_matrix) + pass + try: + from scipy.sparse import csr_matrix + + is_matrix = isinstance(data, csr_matrix) + except ImportError: + pass + return is_array or is_matrix def _array_interface_dict(data: np.ndarray) -> dict: @@ -135,12 +146,23 @@ def _from_scipy_csr( return handle, feature_names, feature_types -def _is_scipy_csc(data: DataType) -> bool: +def is_scipy_csc(data: DataType) -> bool: + """Predicate for scipy CSC input.""" + is_array = False + is_matrix = False try: - import scipy.sparse + from scipy.sparse import csc_array + + is_array = isinstance(data, csc_array) except ImportError: - return False - return isinstance(data, scipy.sparse.csc_matrix) + pass + try: + from scipy.sparse import csc_matrix + + is_matrix = isinstance(data, csc_matrix) + except ImportError: + pass + return is_array or is_matrix def _from_scipy_csc( @@ -171,12 +193,23 @@ def _from_scipy_csc( return handle, feature_names, feature_types -def _is_scipy_coo(data: DataType) -> bool: +def is_scipy_coo(data: DataType) -> bool: + """Predicate for scipy COO input.""" + is_array = False + is_matrix = False try: - import scipy.sparse + from scipy.sparse import coo_array + + is_array = isinstance(data, coo_array) except ImportError: - return False - return isinstance(data, scipy.sparse.coo_matrix) + pass + try: + from scipy.sparse import coo_matrix + + is_matrix = isinstance(data, coo_matrix) + except ImportError: + pass + return is_array or is_matrix def _is_np_array_like(data: DataType) -> bool: @@ -1138,15 +1171,15 @@ def dispatch_data_backend( """Dispatch data for DMatrix.""" if not _is_cudf_ser(data) and not _is_pandas_series(data): _check_data_shape(data) - if _is_scipy_csr(data): + if is_scipy_csr(data): return _from_scipy_csr( data, missing, threads, feature_names, feature_types, data_split_mode ) - if _is_scipy_csc(data): + if is_scipy_csc(data): return _from_scipy_csc( data, missing, threads, feature_names, feature_types, data_split_mode ) - if _is_scipy_coo(data): + if is_scipy_coo(data): return _from_scipy_csr( data.tocsr(), missing, @@ -1396,9 +1429,15 @@ def _proxy_transform( if _is_np_array_like(data): data, _ = _ensure_np_dtype(data, data.dtype) return data, None, feature_names, feature_types - if _is_scipy_csr(data): + if is_scipy_csr(data): data = transform_scipy_sparse(data, True) return data, None, feature_names, feature_types + if is_scipy_csc(data): + data = transform_scipy_sparse(data.tocsr(), True) + return data, None, feature_names, feature_types + if is_scipy_coo(data): + data = transform_scipy_sparse(data.tocsr(), True) + return data, None, feature_names, feature_types if _is_pandas_series(data): import pandas as pd @@ -1451,7 +1490,7 @@ def dispatch_proxy_set_data( _check_data_shape(data) proxy._set_data_from_array(data) # pylint: disable=W0212 return - if _is_scipy_csr(data): + if is_scipy_csr(data): proxy._set_data_from_csr(data) # pylint: disable=W0212 return raise err diff --git a/tests/python/test_dmatrix.py b/tests/python/test_dmatrix.py index 05a9af3b0..2e8a1a2a6 100644 --- a/tests/python/test_dmatrix.py +++ b/tests/python/test_dmatrix.py @@ -112,39 +112,6 @@ class TestDMatrix: with pytest.raises(ValueError): xgb.DMatrix(data) - def test_csr(self): - indptr = np.array([0, 2, 3, 6]) - indices = np.array([0, 2, 2, 0, 1, 2]) - data = np.array([1, 2, 3, 4, 5, 6]) - X = scipy.sparse.csr_matrix((data, indices, indptr), shape=(3, 3)) - dtrain = xgb.DMatrix(X) - assert dtrain.num_row() == 3 - assert dtrain.num_col() == 3 - - def test_csc(self): - row = np.array([0, 2, 2, 0, 1, 2]) - col = np.array([0, 0, 1, 2, 2, 2]) - data = np.array([1, 2, 3, 4, 5, 6]) - X = scipy.sparse.csc_matrix((data, (row, col)), shape=(3, 3)) - dtrain = xgb.DMatrix(X) - assert dtrain.num_row() == 3 - assert dtrain.num_col() == 3 - - indptr = np.array([0, 3, 5]) - data = np.array([0, 1, 2, 3, 4]) - row_idx = np.array([0, 1, 2, 0, 2]) - X = scipy.sparse.csc_matrix((data, row_idx, indptr), shape=(3, 2)) - assert tm.predictor_equal(xgb.DMatrix(X.tocsr()), xgb.DMatrix(X)) - - def test_coo(self): - row = np.array([0, 2, 2, 0, 1, 2]) - col = np.array([0, 0, 1, 2, 2, 2]) - data = np.array([1, 2, 3, 4, 5, 6]) - X = scipy.sparse.coo_matrix((data, (row, col)), shape=(3, 3)) - dtrain = xgb.DMatrix(X) - assert dtrain.num_row() == 3 - assert dtrain.num_col() == 3 - def test_np_view(self): # Sliced Float32 array y = np.array([12, 34, 56], np.float32)[::2] diff --git a/tests/python/test_with_scipy.py b/tests/python/test_with_scipy.py new file mode 100644 index 000000000..ab54d2a43 --- /dev/null +++ b/tests/python/test_with_scipy.py @@ -0,0 +1,87 @@ +import itertools +import warnings +from typing import Type + +import numpy as np +import pytest +import scipy.sparse + +import xgboost as xgb +from xgboost import testing as tm + + +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize( + "DMatrixT,CSR", + [ + (m, n) + for m, n in itertools.product( + (xgb.DMatrix, xgb.QuantileDMatrix), + (scipy.sparse.csr_matrix, scipy.sparse.csr_array), + ) + ], +) +def test_csr(DMatrixT: Type[xgb.DMatrix], CSR: Type) -> None: + with warnings.catch_warnings(): + indptr = np.array([0, 2, 3, 6]) + indices = np.array([0, 2, 2, 0, 1, 2]) + data = np.array([1, 2, 3, 4, 5, 6]) + X = CSR((data, indices, indptr), shape=(3, 3)) + dtrain = DMatrixT(X) + assert dtrain.num_row() == 3 + assert dtrain.num_col() == 3 + assert dtrain.num_nonmissing() == data.size + + +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize( + "DMatrixT,CSC", + [ + (m, n) + for m, n in itertools.product( + (xgb.DMatrix, xgb.QuantileDMatrix), + (scipy.sparse.csc_matrix, scipy.sparse.csc_array), + ) + ], +) +def test_csc(DMatrixT: Type[xgb.DMatrix], CSC: Type) -> None: + with warnings.catch_warnings(): + row = np.array([0, 2, 2, 0, 1, 2]) + col = np.array([0, 0, 1, 2, 2, 2]) + data = np.array([1, 2, 3, 4, 5, 6]) + X = CSC((data, (row, col)), shape=(3, 3)) + dtrain = DMatrixT(X) + assert dtrain.num_row() == 3 + assert dtrain.num_col() == 3 + assert dtrain.num_nonmissing() == data.size + + indptr = np.array([0, 3, 5]) + data = np.array([0, 1, 2, 3, 4]) + row_idx = np.array([0, 1, 2, 0, 2]) + X = CSC((data, row_idx, indptr), shape=(3, 2)) + assert tm.predictor_equal(DMatrixT(X.tocsr()), DMatrixT(X)) + + +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize( + "DMatrixT,COO", + [ + (m, n) + for m, n in itertools.product( + (xgb.DMatrix, xgb.QuantileDMatrix), + (scipy.sparse.coo_matrix, scipy.sparse.coo_array), + ) + ], +) +def test_coo(DMatrixT: Type[xgb.DMatrix], COO: Type) -> None: + with warnings.catch_warnings(): + row = np.array([0, 2, 2, 0, 1, 2]) + col = np.array([0, 0, 1, 2, 2, 2]) + data = np.array([1, 2, 3, 4, 5, 6]) + X = COO((data, (row, col)), shape=(3, 3)) + dtrain = DMatrixT(X) + assert dtrain.num_row() == 3 + assert dtrain.num_col() == 3 + assert dtrain.num_nonmissing() == data.size + + assert tm.predictor_equal(DMatrixT(X.tocsr()), DMatrixT(X)) From 125bc812f81ec908ed365d63cc8db97ff7699975 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 14 Dec 2023 23:29:19 +0800 Subject: [PATCH 052/109] [doc] Reference `enable_categorical` doc in sklearn. (#9884) --- python-package/xgboost/core.py | 7 ++++--- python-package/xgboost/sklearn.py | 8 +------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 1bddadbbe..2e72dbbd2 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -824,9 +824,10 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m Experimental support of specializing for categorical features. - If passing 'True' and 'data' is a data frame (from supported libraries - such as Pandas or Modin), columns of categorical types will automatically - be set to be of categorical type (feature_type='c') in the resulting DMatrix. + If passing 'True' and 'data' is a data frame (from supported libraries such + as Pandas, Modin or cuDF), columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting + DMatrix. If passing 'False' and 'data' is a data frame with categorical columns, it will result in an error being thrown. diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index 748b26cf6..ded37881f 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -276,13 +276,7 @@ __model_doc = f""" enable_categorical : bool - .. versionadded:: 1.5.0 - - .. note:: This parameter is experimental - - Experimental support for categorical data. When enabled, cudf/pandas.DataFrame - should be used to specify categorical data type. Also, JSON/UBJSON - serialization format is required. + See the same parameter of :py:class:`DMatrix` for details. feature_types : Optional[FeatureTypes] From 1c6e031c7558163e807e515c783b3dbcbbbe80d5 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 15 Dec 2023 01:30:43 +0800 Subject: [PATCH 053/109] [R] Fix clang warning. (#9874) --- R-package/src/xgboost_R.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index c27dc5e86..c675e873d 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -154,12 +154,12 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { } \ PutRNGstate(); -/*! - * \brief macro to check the call. +/** + * @brief Macro for checking XGBoost return code. */ -#define CHECK_CALL(x) \ - if ((x) != 0) { \ - error(XGBGetLastError()); \ +#define CHECK_CALL(__rc) \ + if ((__rc) != 0) { \ + Rf_error("%s", XGBGetLastError()); \ } using dmlc::BeginPtr; From 2a6ab2547d1af8126e82114c4b1ed475059efffc Mon Sep 17 00:00:00 2001 From: Dmitry Razdoburdin Date: Fri, 15 Dec 2023 04:04:39 +0100 Subject: [PATCH 054/109] SYCL inference optimization (#9876) --------- Co-authored-by: Dmitry Razdoburdin <> --- plugin/sycl/data.h | 22 ++- plugin/sycl/predictor/predictor.cc | 240 +++++++++++++++-------------- 2 files changed, 136 insertions(+), 126 deletions(-) diff --git a/plugin/sycl/data.h b/plugin/sycl/data.h index 179c7cd1f..489fde989 100644 --- a/plugin/sycl/data.h +++ b/plugin/sycl/data.h @@ -66,13 +66,13 @@ class USMVector { public: USMVector() : size_(0), capacity_(0), data_(nullptr) {} - USMVector(::sycl::queue& qu, size_t size) : size_(size), capacity_(size) { + USMVector(::sycl::queue* qu, size_t size) : size_(size), capacity_(size) { data_ = allocate_memory_(qu, size_); } - USMVector(::sycl::queue& qu, size_t size, T v) : size_(size), capacity_(size) { + USMVector(::sycl::queue* qu, size_t size, T v) : size_(size), capacity_(size) { data_ = allocate_memory_(qu, size_); - qu.fill(data_.get(), v, size_).wait(); + qu->fill(data_.get(), v, size_).wait(); } USMVector(::sycl::queue* qu, const std::vector &vec) { @@ -147,25 +147,22 @@ class USMVector { } } - ::sycl::event ResizeAsync(::sycl::queue* qu, size_t size_new, T v) { + void Resize(::sycl::queue* qu, size_t size_new, T v, ::sycl::event* event) { if (size_new <= size_) { size_ = size_new; - return ::sycl::event(); } else if (size_new <= capacity_) { auto event = qu->fill(data_.get() + size_, v, size_new - size_); size_ = size_new; - return event; } else { size_t size_old = size_; auto data_old = data_; size_ = size_new; capacity_ = size_new; data_ = allocate_memory_(qu, size_); - ::sycl::event event; if (size_old > 0) { - event = qu->memcpy(data_.get(), data_old.get(), sizeof(T) * size_old); + *event = qu->memcpy(data_.get(), data_old.get(), sizeof(T) * size_old, *event); } - return qu->fill(data_.get() + size_old, v, size_new - size_old, event); + *event = qu->fill(data_.get() + size_old, v, size_new - size_old, *event); } } @@ -210,7 +207,7 @@ struct DeviceMatrix { DMatrix* p_mat; // Pointer to the original matrix on the host ::sycl::queue qu_; USMVector row_ptr; - USMVector data; + USMVector data; size_t total_offset; DeviceMatrix(::sycl::queue qu, DMatrix* dmat) : p_mat(dmat), qu_(qu) { @@ -238,8 +235,9 @@ struct DeviceMatrix { for (size_t i = 0; i < batch_size; i++) row_ptr[i + batch.base_rowid] += batch.base_rowid; } - std::copy(data_vec.data(), data_vec.data() + offset_vec[batch_size], - data.Data() + data_offset); + qu.memcpy(data.Data() + data_offset, + data_vec.data(), + offset_vec[batch_size] * sizeof(Entry)).wait(); data_offset += offset_vec[batch_size]; } } diff --git a/plugin/sycl/predictor/predictor.cc b/plugin/sycl/predictor/predictor.cc index 3ceb99f1e..dd56dd3bd 100755 --- a/plugin/sycl/predictor/predictor.cc +++ b/plugin/sycl/predictor/predictor.cc @@ -20,6 +20,7 @@ #include "xgboost/tree_model.h" #include "xgboost/predictor.h" #include "xgboost/tree_updater.h" +#include "../../../src/common/timer.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wtautological-constant-compare" @@ -36,36 +37,37 @@ namespace predictor { DMLC_REGISTRY_FILE_TAG(predictor_sycl); -/* Wrapper for descriptor of a tree node */ -struct DeviceNode { - DeviceNode() - : fidx(-1), left_child_idx(-1), right_child_idx(-1) {} - - union NodeValue { - float leaf_weight; - float fvalue; - }; +union NodeValue { + float leaf_weight; + float fvalue; +}; +class Node { int fidx; int left_child_idx; int right_child_idx; NodeValue val; - explicit DeviceNode(const RegTree::Node& n) { - this->left_child_idx = n.LeftChild(); - this->right_child_idx = n.RightChild(); - this->fidx = n.SplitIndex(); + public: + explicit Node(const RegTree::Node& n) { + left_child_idx = n.LeftChild(); + right_child_idx = n.RightChild(); + fidx = n.SplitIndex(); if (n.DefaultLeft()) { fidx |= (1U << 31); } if (n.IsLeaf()) { - this->val.leaf_weight = n.LeafValue(); + val.leaf_weight = n.LeafValue(); } else { - this->val.fvalue = n.SplitCond(); + val.fvalue = n.SplitCond(); } } + int LeftChildIdx() const {return left_child_idx; } + + int RightChildIdx() const {return right_child_idx; } + bool IsLeaf() const { return left_child_idx == -1; } int GetFidx() const { return fidx & ((1U << 31) - 1U); } @@ -74,9 +76,9 @@ struct DeviceNode { int MissingIdx() const { if (MissingLeft()) { - return this->left_child_idx; + return left_child_idx; } else { - return this->right_child_idx; + return right_child_idx; } } @@ -85,105 +87,79 @@ struct DeviceNode { float GetWeight() const { return val.leaf_weight; } }; -/* SYCL implementation of a device model, - * storing tree structure in USM buffers to provide access from device kernels - */ class DeviceModel { public: - ::sycl::queue qu_; - USMVector nodes_; - USMVector tree_segments_; - USMVector tree_group_; - size_t tree_beg_; - size_t tree_end_; - int num_group_; + USMVector nodes; + USMVector first_node_position; + USMVector tree_group; + size_t tree_beg; + size_t tree_end; + int num_group; - DeviceModel() {} - - ~DeviceModel() {} - - void Init(::sycl::queue qu, const gbm::GBTreeModel& model, size_t tree_begin, size_t tree_end) { - qu_ = qu; - - tree_segments_.Resize(&qu_, (tree_end - tree_begin) + 1); - int sum = 0; - tree_segments_[0] = sum; + void Init(::sycl::queue* qu, const gbm::GBTreeModel& model, size_t tree_begin, size_t tree_end) { + int n_nodes = 0; + first_node_position.Resize(qu, (tree_end - tree_begin) + 1); + first_node_position[0] = n_nodes; for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { if (model.trees[tree_idx]->HasCategoricalSplit()) { LOG(FATAL) << "Categorical features are not yet supported by sycl"; } - sum += model.trees[tree_idx]->GetNodes().size(); - tree_segments_[tree_idx - tree_begin + 1] = sum; + n_nodes += model.trees[tree_idx]->GetNodes().size(); + first_node_position[tree_idx - tree_begin + 1] = n_nodes; } - nodes_.Resize(&qu_, sum); + nodes.Resize(qu, n_nodes); for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { auto& src_nodes = model.trees[tree_idx]->GetNodes(); - for (size_t node_idx = 0; node_idx < src_nodes.size(); node_idx++) - nodes_[node_idx + tree_segments_[tree_idx - tree_begin]] = - static_cast(src_nodes[node_idx]); + size_t n_nodes_shift = first_node_position[tree_idx - tree_begin]; + for (size_t node_idx = 0; node_idx < src_nodes.size(); node_idx++) { + nodes[node_idx + n_nodes_shift] = static_cast(src_nodes[node_idx]); + } } - tree_group_.Resize(&qu_, model.tree_info.size()); + tree_group.Resize(qu, model.tree_info.size()); for (size_t tree_idx = 0; tree_idx < model.tree_info.size(); tree_idx++) - tree_group_[tree_idx] = model.tree_info[tree_idx]; + tree_group[tree_idx] = model.tree_info[tree_idx]; - tree_beg_ = tree_begin; - tree_end_ = tree_end; - num_group_ = model.learner_model_param->num_output_group; + tree_beg = tree_begin; + tree_end = tree_end; + num_group = model.learner_model_param->num_output_group; } }; -float GetFvalue(int ridx, int fidx, Entry* data, size_t* row_ptr, bool* is_missing) { - // Binary search - auto begin_ptr = data + row_ptr[ridx]; - auto end_ptr = data + row_ptr[ridx + 1]; - Entry* previous_middle = nullptr; - while (end_ptr != begin_ptr) { - auto middle = begin_ptr + (end_ptr - begin_ptr) / 2; - if (middle == previous_middle) { - break; +float GetLeafWeight(const Node* nodes, const float* fval_buff, const uint8_t* miss_buff) { + const Node* node = nodes; + while (!node->IsLeaf()) { + if (miss_buff[node->GetFidx()] == 1) { + node = nodes + node->MissingIdx(); } else { - previous_middle = middle; - } - - if (middle->index == fidx) { - *is_missing = false; - return middle->fvalue; - } else if (middle->index < fidx) { - begin_ptr = middle; - } else { - end_ptr = middle; - } - } - *is_missing = true; - return 0.0; -} - -float GetLeafWeight(int ridx, const DeviceNode* tree, Entry* data, size_t* row_ptr) { - DeviceNode n = tree[0]; - int node_id = 0; - bool is_missing; - while (!n.IsLeaf()) { - float fvalue = GetFvalue(ridx, n.GetFidx(), data, row_ptr, &is_missing); - // Missing value - if (is_missing) { - n = tree[n.MissingIdx()]; - } else { - if (fvalue < n.GetFvalue()) { - node_id = n.left_child_idx; - n = tree[n.left_child_idx]; + const float fvalue = fval_buff[node->GetFidx()]; + if (fvalue < node->GetFvalue()) { + node = nodes + node->LeftChildIdx(); } else { - node_id = n.right_child_idx; - n = tree[n.right_child_idx]; + node = nodes + node->RightChildIdx(); } } } - return n.GetWeight(); + return node->GetWeight(); } -void DevicePredictInternal(::sycl::queue qu, - sycl::DeviceMatrix* dmat, +float GetLeafWeight(const Node* nodes, const float* fval_buff) { + const Node* node = nodes; + while (!node->IsLeaf()) { + const float fvalue = fval_buff[node->GetFidx()]; + if (fvalue < node->GetFvalue()) { + node = nodes + node->LeftChildIdx(); + } else { + node = nodes + node->RightChildIdx(); + } + } + return node->GetWeight(); +} + +template +void DevicePredictInternal(::sycl::queue* qu, + const sycl::DeviceMatrix& dmat, HostDeviceVector* out_preds, const gbm::GBTreeModel& model, size_t tree_begin, @@ -194,43 +170,75 @@ void DevicePredictInternal(::sycl::queue qu, DeviceModel device_model; device_model.Init(qu, model, tree_begin, tree_end); - auto& out_preds_vec = out_preds->HostVector(); - - DeviceNode* nodes = device_model.nodes_.Data(); - ::sycl::buffer out_preds_buf(out_preds_vec.data(), out_preds_vec.size()); - size_t* tree_segments = device_model.tree_segments_.Data(); - int* tree_group = device_model.tree_group_.Data(); - size_t* row_ptr = dmat->row_ptr.Data(); - Entry* data = dmat->data.Data(); - int num_features = dmat->p_mat->Info().num_col_; - int num_rows = dmat->row_ptr.Size() - 1; + const Node* nodes = device_model.nodes.DataConst(); + const size_t* first_node_position = device_model.first_node_position.DataConst(); + const int* tree_group = device_model.tree_group.DataConst(); + const size_t* row_ptr = dmat.row_ptr.DataConst(); + const Entry* data = dmat.data.DataConst(); + int num_features = dmat.p_mat->Info().num_col_; + int num_rows = dmat.row_ptr.Size() - 1; int num_group = model.learner_model_param->num_output_group; - qu.submit([&](::sycl::handler& cgh) { + USMVector fval_buff(qu, num_features * num_rows); + USMVector miss_buff; + auto* fval_buff_ptr = fval_buff.Data(); + + std::vector<::sycl::event> events(1); + if constexpr (any_missing) { + miss_buff.Resize(qu, num_features * num_rows, 1, &events[0]); + } + auto* miss_buff_ptr = miss_buff.Data(); + + auto& out_preds_vec = out_preds->HostVector(); + ::sycl::buffer out_preds_buf(out_preds_vec.data(), out_preds_vec.size()); + events[0] = qu->submit([&](::sycl::handler& cgh) { + cgh.depends_on(events[0]); auto out_predictions = out_preds_buf.template get_access<::sycl::access::mode::read_write>(cgh); cgh.parallel_for<>(::sycl::range<1>(num_rows), [=](::sycl::id<1> pid) { - int global_idx = pid[0]; - if (global_idx >= num_rows) return; + int row_idx = pid[0]; + auto* fval_buff_row_ptr = fval_buff_ptr + num_features * row_idx; + auto* miss_buff_row_ptr = miss_buff_ptr + num_features * row_idx; + + const Entry* first_entry = data + row_ptr[row_idx]; + const Entry* last_entry = data + row_ptr[row_idx + 1]; + for (const Entry* entry = first_entry; entry < last_entry; entry += 1) { + fval_buff_row_ptr[entry->index] = entry->fvalue; + if constexpr (any_missing) { + miss_buff_row_ptr[entry->index] = 0; + } + } + if (num_group == 1) { float sum = 0.0; for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - const DeviceNode* tree = nodes + tree_segments[tree_idx - tree_begin]; - sum += GetLeafWeight(global_idx, tree, data, row_ptr); + const Node* first_node = nodes + first_node_position[tree_idx - tree_begin]; + if constexpr (any_missing) { + sum += GetLeafWeight(first_node, fval_buff_row_ptr, miss_buff_row_ptr); + } else { + sum += GetLeafWeight(first_node, fval_buff_row_ptr); + } } - out_predictions[global_idx] += sum; + out_predictions[row_idx] += sum; } else { for (int tree_idx = tree_begin; tree_idx < tree_end; tree_idx++) { - const DeviceNode* tree = nodes + tree_segments[tree_idx - tree_begin]; - int out_prediction_idx = global_idx * num_group + tree_group[tree_idx]; - out_predictions[out_prediction_idx] += GetLeafWeight(global_idx, tree, data, row_ptr); + const Node* first_node = nodes + first_node_position[tree_idx - tree_begin]; + int out_prediction_idx = row_idx * num_group + tree_group[tree_idx]; + if constexpr (any_missing) { + out_predictions[out_prediction_idx] += + GetLeafWeight(first_node, fval_buff_row_ptr, miss_buff_row_ptr); + } else { + out_predictions[out_prediction_idx] += + GetLeafWeight(first_node, fval_buff_row_ptr); + } } } }); - }).wait(); + }); + qu->wait(); } class Predictor : public xgboost::Predictor { - protected: + public: void InitOutPredictions(const MetaInfo& info, HostDeviceVector* out_preds, const gbm::GBTreeModel& model) const override { @@ -263,7 +271,6 @@ class Predictor : public xgboost::Predictor { } } - public: explicit Predictor(Context const* context) : xgboost::Predictor::Predictor{context}, cpu_predictor(xgboost::Predictor::Create("cpu_predictor", context)) {} @@ -281,7 +288,12 @@ class Predictor : public xgboost::Predictor { } if (tree_begin < tree_end) { - DevicePredictInternal(qu, &device_matrix, out_preds, model, tree_begin, tree_end); + const bool any_missing = !(dmat->IsDense()); + if (any_missing) { + DevicePredictInternal(&qu, device_matrix, out_preds, model, tree_begin, tree_end); + } else { + DevicePredictInternal(&qu, device_matrix, out_preds, model, tree_begin, tree_end); + } } } From db7f952ed62940208668facef02e513b4ff3da9d Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 16 Dec 2023 05:19:22 +0100 Subject: [PATCH 055/109] update docs for parameters (#9900) --- doc/parameter.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/parameter.rst b/doc/parameter.rst index d2471dfd9..0ce60916e 100644 --- a/doc/parameter.rst +++ b/doc/parameter.rst @@ -78,7 +78,7 @@ Parameters for Tree Booster * ``gamma`` [default=0, alias: ``min_split_loss``] - - Minimum loss reduction required to make a further partition on a leaf node of the tree. The larger ``gamma`` is, the more conservative the algorithm will be. + - Minimum loss reduction required to make a further partition on a leaf node of the tree. The larger ``gamma`` is, the more conservative the algorithm will be. Note that a tree where no splits were made might still contain a single terminal node with a non-zero score. - range: [0,∞] * ``max_depth`` [default=6] @@ -388,6 +388,7 @@ Specify the learning task and the corresponding learning objective. The objectiv - The initial prediction score of all instances, global bias - The parameter is automatically estimated for selected objectives before training. To disable the estimation, specify a real number argument. + - If ``base_margin`` is supplied, ``base_score`` will not be added. - For sufficient number of iterations, changing this value will not have too much effect. * ``eval_metric`` [default according to objective] From 0edd600f3d10059a13d1db41b9c5704a7e0a660c Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sun, 17 Dec 2023 13:34:34 +0800 Subject: [PATCH 056/109] [doc] Brief introduction to `base_score`. (#9882) --- doc/parameter.rst | 2 + doc/tutorials/custom_metric_obj.rst | 3 +- doc/tutorials/index.rst | 1 + doc/tutorials/intercept.rst | 104 ++++++++++++++++++++++++++++ python-package/xgboost/core.py | 2 +- python-package/xgboost/sklearn.py | 8 +-- 6 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 doc/tutorials/intercept.rst diff --git a/doc/parameter.rst b/doc/parameter.rst index 0ce60916e..a7d8203b0 100644 --- a/doc/parameter.rst +++ b/doc/parameter.rst @@ -391,6 +391,8 @@ Specify the learning task and the corresponding learning objective. The objectiv - If ``base_margin`` is supplied, ``base_score`` will not be added. - For sufficient number of iterations, changing this value will not have too much effect. + See :doc:`/tutorials/intercept` for more info. + * ``eval_metric`` [default according to objective] - Evaluation metrics for validation data, a default metric will be assigned according to objective (rmse for regression, and logloss for classification, `mean average precision` for ``rank:map``, etc.) diff --git a/doc/tutorials/custom_metric_obj.rst b/doc/tutorials/custom_metric_obj.rst index f5c08bf59..76ee1b3de 100644 --- a/doc/tutorials/custom_metric_obj.rst +++ b/doc/tutorials/custom_metric_obj.rst @@ -271,7 +271,8 @@ available in XGBoost: We use ``multi:softmax`` to illustrate the differences of transformed prediction. With ``softprob`` the output prediction array has shape ``(n_samples, n_classes)`` while for ``softmax`` it's ``(n_samples, )``. A demo for multi-class objective function is also -available at :ref:`sphx_glr_python_examples_custom_softmax.py`. +available at :ref:`sphx_glr_python_examples_custom_softmax.py`. Also, see +:doc:`/tutorials/intercept` for some more explanation. ********************** diff --git a/doc/tutorials/index.rst b/doc/tutorials/index.rst index 5d090ce65..c82abf43f 100644 --- a/doc/tutorials/index.rst +++ b/doc/tutorials/index.rst @@ -30,4 +30,5 @@ See `Awesome XGBoost `_ for mo input_format param_tuning custom_metric_obj + intercept privacy_preserving \ No newline at end of file diff --git a/doc/tutorials/intercept.rst b/doc/tutorials/intercept.rst new file mode 100644 index 000000000..8452918e1 --- /dev/null +++ b/doc/tutorials/intercept.rst @@ -0,0 +1,104 @@ +######### +Intercept +######### + +.. versionadded:: 2.0.0 + +Since 2.0.0, XGBoost supports estimating the model intercept (named ``base_score``) +automatically based on targets upon training. The behavior can be controlled by setting +``base_score`` to a constant value. The following snippet disables the automatic +estimation: + +.. code-block:: python + + import xgboost as xgb + + reg = xgb.XGBRegressor() + reg.set_params(base_score=0.5) + +In addition, here 0.5 represents the value after applying the inverse link function. See +the end of the document for a description. + +Other than the ``base_score``, users can also provide global bias via the data field +``base_margin``, which is a vector or a matrix depending on the task. With multi-output +and multi-class, the ``base_margin`` is a matrix with size ``(n_samples, n_targets)`` or +``(n_samples, n_classes)``. + +.. code-block:: python + + import xgboost as xgb + from sklearn.datasets import make_regression + + X, y = make_regression() + + reg = xgb.XGBRegressor() + reg.fit(X, y) + # Request for raw prediction + m = reg.predict(X, output_margin=True) + + reg_1 = xgb.XGBRegressor() + # Feed the prediction into the next model + reg.fit(X, y, base_margin=m) + reg.predict(X, base_margin=m) + + +It specifies the bias for each sample and can be used for stacking an XGBoost model on top +of other models, see :ref:`sphx_glr_python_examples_boost_from_prediction.py` for a worked +example. When ``base_margin`` is specified, it automatically overrides the ``base_score`` +parameter. If you are stacking XGBoost models, then the usage should be relatively +straightforward, with the previous model providing raw prediction and a new model using +the prediction as bias. For more customized inputs, users need to take extra care of the +link function. Let :math:`F` be the model and :math:`g` be the link function, since +``base_score`` is overridden when sample-specific ``base_margin`` is available, we will +omit it here: + +.. math:: + + g(E[y_i]) = F(x_i) + + +When base margin :math:`b` is provided, it's added to the raw model output :math:`F`: + +.. math:: + + g(E[y_i]) = F(x_i) + b_i + +and the output of the final model is: + + +.. math:: + + g^{-1}(F(x_i) + b_i) + +Using the gamma deviance objective ``reg:gamma`` as an example, which has a log link +function, hence: + +.. math:: + + \ln{(E[y_i])} = F(x_i) + b_i \\ + E[y_i] = \exp{(F(x_i) + b_i)} + +As a result, if you are feeding outputs from models like GLM with a corresponding +objective function, make sure the outputs are not yet transformed by the inverse link. + +In the case of ``base_score`` (intercept), it can be accessed through +:py:meth:`~xgboost.Booster.save_config` after estimation. Unlike the ``base_margin``, the +returned value represents a value after applying inverse link. With logistic regression +and the logit link function as an example, given the ``base_score`` as 0.5, +:math:`g(intercept) = logit(0.5) = 0` is added to the raw model output: + +.. math:: + + E[y_i] = g^{-1}{(F(x_i) + g(intercept))} + +and 0.5 is the same as :math:`base_score = g^{-1}(0) = 0.5`. This is more intuitive if you +remove the model and consider only the intercept, which is estimated before the model is +fitted: + +.. math:: + + E[y] = g^{-1}{g(intercept))} \\ + E[y] = intercept + +For some objectives like MAE, there are close solutions, while for others it's estimated +with one step Newton method. \ No newline at end of file diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 2e72dbbd2..097fb0935 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -785,7 +785,7 @@ class DMatrix: # pylint: disable=too-many-instance-attributes,too-many-public-m so it doesn't make sense to assign weights to individual data points. base_margin : - Base margin used for boosting from existing model. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. missing : Value in the input data which needs to be present as a missing value. If None, defaults to np.nan. diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index ded37881f..ea8d8d041 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -1006,7 +1006,7 @@ class XGBModel(XGBModelBase): sample_weight : instance weights base_margin : - global bias for each instance. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. eval_set : A list of (X, y) tuple pairs to use as validation sets, for which metrics will be computed. @@ -1146,7 +1146,7 @@ class XGBModel(XGBModelBase): When this is True, validate that the Booster's and data's feature_names are identical. Otherwise, it is assumed that the feature_names are the same. base_margin : - Margin added to prediction. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. iteration_range : Specifies which layer of trees are used in prediction. For example, if a random forest is trained with 100 rounds. Specifying ``iteration_range=(10, @@ -1599,7 +1599,7 @@ class XGBClassifier(XGBModel, XGBClassifierBase): When this is True, validate that the Booster's and data's feature_names are identical. Otherwise, it is assumed that the feature_names are the same. base_margin : - Margin added to prediction. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. iteration_range : Specifies which layer of trees are used in prediction. For example, if a random forest is trained with 100 rounds. Specifying `iteration_range=(10, @@ -1942,7 +1942,7 @@ class XGBRanker(XGBModel, XGBRankerMixIn): weights to individual data points. base_margin : - Global bias for each instance. + Global bias for each instance. See :doc:`/tutorials/intercept` for details. eval_set : A list of (X, y) tuple pairs to use as validation sets, for which metrics will be computed. From ff3d82c006083c01f7e1d9796564d32844ea952c Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 18 Dec 2023 13:31:01 +0100 Subject: [PATCH 057/109] [R] Refactor field logic for dmatrix (#9901) --- R-package/NAMESPACE | 1 + R-package/R/xgb.DMatrix.R | 70 ++++++++++++++++++++++------ R-package/R/xgb.cv.R | 5 +- R-package/man/setinfo.Rd | 2 +- R-package/man/xgb.DMatrix.hasinfo.Rd | 32 +++++++++++++ 5 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 R-package/man/xgb.DMatrix.hasinfo.Rd diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index bbb5ee225..40ede23a5 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -28,6 +28,7 @@ export(setinfo) export(slice) export(xgb.Booster.complete) export(xgb.DMatrix) +export(xgb.DMatrix.hasinfo) export(xgb.DMatrix.save) export(xgb.attr) export(xgb.attributes) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 4b2bb0d2a..11d1105e6 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -163,7 +163,10 @@ xgb.DMatrix <- function( } dmat <- handle - attributes(dmat) <- list(class = "xgb.DMatrix") + attributes(dmat) <- list( + class = "xgb.DMatrix", + fields = new.env() + ) if (!is.null(label)) { setinfo(dmat, "label", label) @@ -199,6 +202,35 @@ xgb.DMatrix <- function( return(dmat) } +#' @title Check whether DMatrix object has a field +#' @description Checks whether an xgb.DMatrix object has a given field assigned to +#' it, such as weights, labels, etc. +#' @param object The DMatrix object to check for the given \code{info} field. +#' @param info The field to check for presence or absence in \code{object}. +#' @seealso \link{xgb.DMatrix}, \link{getinfo.xgb.DMatrix}, \link{setinfo.xgb.DMatrix} +#' @examples +#' library(xgboost) +#' x <- matrix(1:10, nrow = 5) +#' dm <- xgb.DMatrix(x, nthread = 1) +#' +#' # 'dm' so far doesn't have any fields set +#' xgb.DMatrix.hasinfo(dm, "label") +#' +#' # Fields can be added after construction +#' setinfo(dm, "label", 1:5) +#' xgb.DMatrix.hasinfo(dm, "label") +#' @export +xgb.DMatrix.hasinfo <- function(object, info) { + if (!inherits(object, "xgb.DMatrix")) { + stop("Object is not an 'xgb.DMatrix'.") + } + if (.Call(XGCheckNullPtr_R, object)) { + warning("xgb.DMatrix object is invalid. Must be constructed again.") + return(FALSE) + } + return(NVL(attr(object, "fields")[[info]], FALSE)) +} + # get dmatrix from data, label # internal helper method @@ -389,7 +421,7 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' @param object Object of class "xgb.DMatrix" #' @param name the name of the field to get #' @param info the specific field of information to set -#' @param ... other parameters +#' @param ... Not used. #' #' @details #' See the documentation for \link{xgb.DMatrix} for possible fields that can be set @@ -418,6 +450,12 @@ setinfo <- function(object, ...) UseMethod("setinfo") #' @rdname setinfo #' @export setinfo.xgb.DMatrix <- function(object, name, info, ...) { + .internal.setinfo.xgb.DMatrix(object, name, info, ...) + attr(object, "fields")[[name]] <- TRUE + return(TRUE) +} + +.internal.setinfo.xgb.DMatrix <- function(object, name, info, ...) { if (name == "label") { if (NROW(info) != nrow(object)) stop("The length of labels must equal to the number of rows in the input data") @@ -425,19 +463,19 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { return(TRUE) } if (name == "label_lower_bound") { - if (length(info) != nrow(object)) + if (NROW(info) != nrow(object)) stop("The length of lower-bound labels must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "label_upper_bound") { - if (length(info) != nrow(object)) + if (NROW(info) != nrow(object)) stop("The length of upper-bound labels must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "weight") { - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "base_margin") { @@ -447,20 +485,20 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { if (name == "group") { if (sum(info) != nrow(object)) stop("The sum of groups must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "qid") { if (NROW(info) != nrow(object)) stop("The length of qid assignments must equal to the number of rows in the input data") - .Call(XGDMatrixSetInfo_R, object, name, as.integer(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } if (name == "feature_weights") { - if (length(info) != ncol(object)) { + if (NROW(info) != ncol(object)) { stop("The number of feature weights must equal to the number of columns in the input data") } - .Call(XGDMatrixSetInfo_R, object, name, as.numeric(info)) + .Call(XGDMatrixSetInfo_R, object, name, info) return(TRUE) } @@ -568,11 +606,15 @@ slice.xgb.DMatrix <- function(object, idxset, ...) { #' @method print xgb.DMatrix #' @export print.xgb.DMatrix <- function(x, verbose = FALSE, ...) { + if (.Call(XGCheckNullPtr_R, x)) { + cat("INVALID xgb.DMatrix object. Must be constructed anew.\n") + return(invisible(x)) + } cat('xgb.DMatrix dim:', nrow(x), 'x', ncol(x), ' info: ') infos <- character(0) - if (length(getinfo(x, 'label')) > 0) infos <- 'label' - if (length(getinfo(x, 'weight')) > 0) infos <- c(infos, 'weight') - if (length(getinfo(x, 'base_margin')) > 0) infos <- c(infos, 'base_margin') + if (xgb.DMatrix.hasinfo(x, 'label')) infos <- 'label' + if (xgb.DMatrix.hasinfo(x, 'weight')) infos <- c(infos, 'weight') + if (xgb.DMatrix.hasinfo(x, 'base_margin')) infos <- c(infos, 'base_margin') if (length(infos) == 0) infos <- 'NA' cat(infos) cnames <- colnames(x) diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index 9e1ffeddc..1c17d86f0 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -126,6 +126,9 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing early_stopping_rounds = NULL, maximize = NULL, callbacks = list(), ...) { check.deprecation(...) + if (inherits(data, "xgb.DMatrix") && .Call(XGCheckNullPtr_R, data)) { + stop("'data' is an invalid 'xgb.DMatrix' object. Must be constructed again.") + } params <- check.booster.params(params, ...) # TODO: should we deprecate the redundant 'metrics' parameter? @@ -136,7 +139,7 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing check.custom.eval() # Check the labels - if ((inherits(data, 'xgb.DMatrix') && is.null(getinfo(data, 'label'))) || + if ((inherits(data, 'xgb.DMatrix') && !xgb.DMatrix.hasinfo(data, 'label')) || (!inherits(data, 'xgb.DMatrix') && is.null(label))) { stop("Labels must be provided for CV either through xgb.DMatrix, or through 'label=' when 'data' is matrix") } else if (inherits(data, 'xgb.DMatrix')) { diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd index a8bc56b02..299e72675 100644 --- a/R-package/man/setinfo.Rd +++ b/R-package/man/setinfo.Rd @@ -12,7 +12,7 @@ setinfo(object, ...) \arguments{ \item{object}{Object of class "xgb.DMatrix"} -\item{...}{other parameters} +\item{...}{Not used.} \item{name}{the name of the field to get} diff --git a/R-package/man/xgb.DMatrix.hasinfo.Rd b/R-package/man/xgb.DMatrix.hasinfo.Rd new file mode 100644 index 000000000..308d9b42e --- /dev/null +++ b/R-package/man/xgb.DMatrix.hasinfo.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.DMatrix.hasinfo} +\alias{xgb.DMatrix.hasinfo} +\title{Check whether DMatrix object has a field} +\usage{ +xgb.DMatrix.hasinfo(object, info) +} +\arguments{ +\item{object}{The DMatrix object to check for the given \code{info} field.} + +\item{info}{The field to check for presence or absence in \code{object}.} +} +\description{ +Checks whether an xgb.DMatrix object has a given field assigned to +it, such as weights, labels, etc. +} +\examples{ +library(xgboost) +x <- matrix(1:10, nrow = 5) +dm <- xgb.DMatrix(x, nthread = 1) + +# 'dm' so far doesn't have any fields set +xgb.DMatrix.hasinfo(dm, "label") + +# Fields can be added after construction +setinfo(dm, "label", 1:5) +xgb.DMatrix.hasinfo(dm, "label") +} +\seealso{ +\link{xgb.DMatrix}, \link{getinfo.xgb.DMatrix}, \link{setinfo.xgb.DMatrix} +} From ae32936ba242e3fe083f79e8592b337220c1cb73 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 19 Dec 2023 03:45:03 +0100 Subject: [PATCH 058/109] [R] Catch C++ exceptions (#9903) --- R-package/src/xgboost_R.cc | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index c675e873d..79b9e6037 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -138,21 +138,44 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { } } // namespace +struct RRNGStateController { + RRNGStateController() { + GetRNGstate(); + } + + ~RRNGStateController() { + PutRNGstate(); + } +}; + /*! * \brief macro to annotate begin of api */ #define R_API_BEGIN() \ - GetRNGstate(); \ - try { + try { \ + RRNGStateController rng_controller{}; + +/* Note: an R error triggers a long jump, hence all C++ objects that +allocated memory through non-R allocators, including the exception +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[256]; /*! * \brief macro to annotate end of api */ #define R_API_END() \ } catch(dmlc::Error& e) { \ - PutRNGstate(); \ - error(e.what()); \ + Rf_error("%s", e.what()); \ + } catch(std::exception &e) { \ + std::strncpy(cpp_ex_msg, e.what(), 256); \ + goto throw_cpp_ex_as_R_err; \ } \ - PutRNGstate(); + if (false) { \ + throw_cpp_ex_as_R_err: \ + Rf_error("%s", cpp_ex_msg); \ + } /** * @brief Macro for checking XGBoost return code. From 9d122293bcbeb519415f97e6c318688f718e4519 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 20 Dec 2023 09:17:00 +0800 Subject: [PATCH 059/109] [doc] Fix typo. [skip ci] (#9904) --- doc/tutorials/intercept.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/tutorials/intercept.rst b/doc/tutorials/intercept.rst index 8452918e1..3f6775611 100644 --- a/doc/tutorials/intercept.rst +++ b/doc/tutorials/intercept.rst @@ -38,8 +38,8 @@ and multi-class, the ``base_margin`` is a matrix with size ``(n_samples, n_targe reg_1 = xgb.XGBRegressor() # Feed the prediction into the next model - reg.fit(X, y, base_margin=m) - reg.predict(X, base_margin=m) + reg_1.fit(X, y, base_margin=m) + reg_1.predict(X, base_margin=m) It specifies the bias for each sample and can be used for stacking an XGBoost model on top @@ -79,7 +79,8 @@ function, hence: E[y_i] = \exp{(F(x_i) + b_i)} As a result, if you are feeding outputs from models like GLM with a corresponding -objective function, make sure the outputs are not yet transformed by the inverse link. +objective function, make sure the outputs are not yet transformed by the inverse link +(activation). In the case of ``base_score`` (intercept), it can be accessed through :py:meth:`~xgboost.Booster.save_config` after estimation. Unlike the ``base_margin``, the @@ -91,13 +92,13 @@ and the logit link function as an example, given the ``base_score`` as 0.5, E[y_i] = g^{-1}{(F(x_i) + g(intercept))} -and 0.5 is the same as :math:`base_score = g^{-1}(0) = 0.5`. This is more intuitive if you -remove the model and consider only the intercept, which is estimated before the model is -fitted: +and 0.5 is the same as :math:`base\_score = g^{-1}(0) = 0.5`. This is more intuitive if +you remove the model and consider only the intercept, which is estimated before the model +is fitted: .. math:: - E[y] = g^{-1}{g(intercept))} \\ + E[y] = g^{-1}{(g(intercept))} \\ E[y] = intercept For some objectives like MAE, there are close solutions, while for others it's estimated From 252e018275195b5e6a7fc49f069dccb2ec2d8c46 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 20 Dec 2023 03:52:00 +0100 Subject: [PATCH 060/109] correct name of function in header (#9905) --- include/xgboost/c_api.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xgboost/c_api.h b/include/xgboost/c_api.h index a56ead278..795c78946 100644 --- a/include/xgboost/c_api.h +++ b/include/xgboost/c_api.h @@ -532,7 +532,7 @@ XGProxyDMatrixSetDataCudaArrayInterface(DMatrixHandle handle, * * @return 0 when success, -1 when failure happens */ -XGB_DLL int XGProxyDMatrixSetDataCudaColumnar(DMatrixHandle handle, char const *c_interface_str); +XGB_DLL int XGProxyDMatrixSetDataColumnar(DMatrixHandle handle, char const *c_interface_str); /*! * \brief Set data on a DMatrix proxy. From b807f3e30c8caaeda010afd87ddd2797b36b029f Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Thu, 21 Dec 2023 03:01:30 +0100 Subject: [PATCH 061/109] [R] improve docstrings for "xgb.Booster.R" (#9906) --- R-package/DESCRIPTION | 3 +- R-package/R/xgb.Booster.R | 362 ++++++++++++++----------- R-package/R/xgb.create.features.R | 2 +- R-package/R/xgb.plot.shap.R | 4 +- R-package/R/xgboost.R | 8 +- R-package/man/agaricus.test.Rd | 8 +- R-package/man/agaricus.train.Rd | 8 +- R-package/man/cb.save.model.Rd | 2 +- R-package/man/getinfo.Rd | 18 +- R-package/man/predict.xgb.Booster.Rd | 184 +++++++------ R-package/man/print.xgb.Booster.Rd | 27 +- R-package/man/xgb.Booster.complete.Rd | 25 +- R-package/man/xgb.DMatrix.Rd | 18 +- R-package/man/xgb.attr.Rd | 48 ++-- R-package/man/xgb.config.Rd | 19 +- R-package/man/xgb.create.features.Rd | 4 +- R-package/man/xgb.cv.Rd | 86 +++--- R-package/man/xgb.importance.Rd | 20 +- R-package/man/xgb.model.dt.tree.Rd | 24 +- R-package/man/xgb.parameters.Rd | 17 +- R-package/man/xgb.plot.deepness.Rd | 6 +- R-package/man/xgb.plot.shap.Rd | 6 +- R-package/man/xgb.plot.shap.summary.Rd | 6 +- R-package/man/xgb.plot.tree.Rd | 16 +- R-package/man/xgb.save.raw.Rd | 6 +- R-package/man/xgb.shap.data.Rd | 6 +- R-package/man/xgb.train.Rd | 259 +++++++++--------- R-package/man/xgb.unserialize.Rd | 2 +- 28 files changed, 661 insertions(+), 533 deletions(-) diff --git a/R-package/DESCRIPTION b/R-package/DESCRIPTION index 92b53a660..7c01d50c6 100644 --- a/R-package/DESCRIPTION +++ b/R-package/DESCRIPTION @@ -63,7 +63,8 @@ Imports: Matrix (>= 1.1-0), methods, data.table (>= 1.9.6), - jsonlite (>= 1.0), + jsonlite (>= 1.0) +Roxygen: list(markdown = TRUE) RoxygenNote: 7.2.3 Encoding: UTF-8 SystemRequirements: GNU make, C++17 diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index aa5e65e40..4e980641a 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -79,36 +79,45 @@ xgb.get.handle <- function(object) { handle } -#' Restore missing parts of an incomplete xgb.Booster object. +#' Restore missing parts of an incomplete xgb.Booster object #' -#' It attempts to complete an \code{xgb.Booster} object by restoring either its missing -#' raw model memory dump (when it has no \code{raw} data but its \code{xgb.Booster.handle} is valid) -#' or its missing internal handle (when its \code{xgb.Booster.handle} is not valid +#' It attempts to complete an `xgb.Booster` object by restoring either its missing +#' raw model memory dump (when it has no `raw` data but its `xgb.Booster.handle` is valid) +#' or its missing internal handle (when its `xgb.Booster.handle` is not valid #' but it has a raw Booster memory dump). #' -#' @param object object of class \code{xgb.Booster} -#' @param saveraw a flag indicating whether to append \code{raw} Booster memory dump data +#' @param object Object of class `xgb.Booster`. +#' @param saveraw A flag indicating whether to append `raw` Booster memory dump data #' when it doesn't already exist. #' #' @details #' #' While this method is primarily for internal use, it might be useful in some practical situations. #' -#' E.g., when an \code{xgb.Booster} model is saved as an R object and then is loaded as an R object, +#' E.g., when an `xgb.Booster` model is saved as an R object and then is loaded as an R object, #' its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods #' should still work for such a model object since those methods would be using -#' \code{xgb.Booster.complete} internally. However, one might find it to be more efficient to call the -#' \code{xgb.Booster.complete} function explicitly once after loading a model as an R-object. +#' `xgb.Booster.complete()` internally. However, one might find it to be more efficient to call the +#' `xgb.Booster.complete()` function explicitly once after loading a model as an R-object. #' That would prevent further repeated implicit reconstruction of an internal booster model. #' #' @return -#' An object of \code{xgb.Booster} class. +#' An object of `xgb.Booster` class. #' #' @examples #' -#' data(agaricus.train, package='xgboost') -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' data(agaricus.train, package = "xgboost") +#' +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) +#' #' saveRDS(bst, "xgb.model.rds") #' #' # Warning: The resulting RDS file is only compatible with the current XGBoost version. @@ -161,112 +170,100 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { return(object) } -#' Predict method for eXtreme Gradient Boosting model +#' Predict method for XGBoost model #' #' Predicted values based on either xgboost model or model handle object. #' -#' @param object Object of class \code{xgb.Booster} or \code{xgb.Booster.handle} -#' @param newdata takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, -#' local data file or \code{xgb.DMatrix}. -#' -#' For single-row predictions on sparse data, it's recommended to use CSR format. If passing -#' a sparse vector, it will take it as a row vector. -#' @param missing Missing is only used when input is dense matrix. Pick a float value that represents -#' missing values in data (e.g., sometimes 0 or some other extreme value is used). -#' @param outputmargin whether the prediction should be returned in the for of original untransformed -#' sum of predictions from boosting iterations' results. E.g., setting \code{outputmargin=TRUE} for -#' logistic regression would result in predictions for log-odds instead of probabilities. -#' @param ntreelimit Deprecated, use \code{iterationrange} instead. -#' @param predleaf whether predict leaf index. -#' @param predcontrib whether to return feature contributions to individual predictions (see Details). -#' @param approxcontrib whether to use a fast approximation for feature contributions (see Details). -#' @param predinteraction whether to return contributions of feature interactions to individual predictions (see Details). -#' @param reshape whether to reshape the vector of predictions to a matrix form when there are several -#' prediction outputs per case. This option has no effect when either of predleaf, predcontrib, -#' or predinteraction flags is TRUE. -#' @param training whether is the prediction result used for training. For dart booster, +#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param newdata Takes `matrix`, `dgCMatrix`, `dgRMatrix`, `dsparseVector`, +#' local data file, or `xgb.DMatrix`. +#' For single-row predictions on sparse data, it is recommended to use the CSR format. +#' If passing a sparse vector, it will take it as a row vector. +#' @param missing Only used when input is a dense matrix. Pick a float value that represents +#' missing values in data (e.g., 0 or some other extreme value). +#' @param outputmargin Whether the prediction should be returned in the form of original untransformed +#' sum of predictions from boosting iterations' results. E.g., setting `outputmargin=TRUE` for +#' logistic regression would return log-odds instead of probabilities. +#' @param ntreelimit Deprecated, use `iterationrange` instead. +#' @param predleaf Whether to predict pre-tree leaf indices. +#' @param predcontrib Whether to return feature contributions to individual predictions (see Details). +#' @param approxcontrib Whether to use a fast approximation for feature contributions (see Details). +#' @param predinteraction Whether to return contributions of feature interactions to individual predictions (see Details). +#' @param reshape Whether to reshape the vector of predictions to matrix form when there are several +#' prediction outputs per case. No effect if `predleaf`, `predcontrib`, +#' or `predinteraction` is `TRUE`. +#' @param training Whether the predictions are used for training. For dart booster, #' training predicting will perform dropout. -#' @param iterationrange Specifies which layer of trees are used in prediction. For -#' example, if a random forest is trained with 100 rounds. Specifying -#' `iterationrange=(1, 21)`, then only the forests built during [1, 21) (half open set) -#' rounds are used in this prediction. It's 1-based index just like R vector. When set -#' to \code{c(1, 1)} XGBoost will use all trees. -#' @param strict_shape Default is \code{FALSE}. When it's set to \code{TRUE}, output -#' type and shape of prediction are invariant to model type. -#' +#' @param iterationrange Specifies which trees are used in prediction. For +#' example, take a random forest with 100 rounds. +#' With `iterationrange=c(1, 21)`, only the trees built during `[1, 21)` (half open set) +#' rounds are used in this prediction. The index is 1-based just like an R vector. When set +#' to `c(1, 1)`, XGBoost will use all trees. +#' @param strict_shape Default is `FALSE`. When set to `TRUE`, the output +#' type and shape of predictions are invariant to the model type. #' @param ... Not used. #' #' @details #' -#' Note that \code{iterationrange} would currently do nothing for predictions from gblinear, -#' since gblinear doesn't keep its boosting history. +#' Note that `iterationrange` would currently do nothing for predictions from "gblinear", +#' since "gblinear" doesn't keep its boosting history. #' -#' One possible practical applications of the \code{predleaf} option is to use the model +#' One possible practical applications of the `predleaf` option is to use the model #' as a generator of new features which capture non-linearity and interactions, -#' e.g., as implemented in \code{\link{xgb.create.features}}. +#' e.g., as implemented in [xgb.create.features()]. #' -#' Setting \code{predcontrib = TRUE} allows to calculate contributions of each feature to +#' Setting `predcontrib = TRUE` allows to calculate contributions of each feature to #' individual predictions. For "gblinear" booster, feature contributions are simply linear terms #' (feature_beta * feature_value). For "gbtree" booster, feature contributions are SHAP #' values (Lundberg 2017) that sum to the difference between the expected output #' of the model and the current prediction (where the hessian weights are used to compute the expectations). -#' Setting \code{approxcontrib = TRUE} approximates these values following the idea explained +#' Setting `approxcontrib = TRUE` approximates these values following the idea explained #' in \url{http://blog.datadive.net/interpreting-random-forests/}. #' -#' With \code{predinteraction = TRUE}, SHAP values of contributions of interaction of each pair of features +#' With `predinteraction = TRUE`, SHAP values of contributions of interaction of each pair of features #' are computed. Note that this operation might be rather expensive in terms of compute and memory. #' Since it quadratically depends on the number of features, it is recommended to perform selection #' of the most important features first. See below about the format of the returned results. #' -#' The \code{predict()} method uses as many threads as defined in \code{xgb.Booster} object (all by default). -#' If you want to change their number, then assign a new number to \code{nthread} using \code{\link{xgb.parameters<-}}. -#' Note also that converting a matrix to \code{\link{xgb.DMatrix}} uses multiple threads too. +#' The `predict()` method uses as many threads as defined in `xgb.Booster` object (all by default). +#' If you want to change their number, assign a new number to `nthread` using [xgb.parameters<-()]. +#' Note that converting a matrix to [xgb.DMatrix()] uses multiple threads too. #' #' @return -#' The return type is different depending whether \code{strict_shape} is set to \code{TRUE}. By default, -#' for regression or binary classification, it returns a vector of length \code{nrows(newdata)}. -#' For multiclass classification, either a \code{num_class * nrows(newdata)} vector or -#' a \code{(nrows(newdata), num_class)} dimension matrix is returned, depending on -#' the \code{reshape} value. -#' -#' When \code{predleaf = TRUE}, the output is a matrix object with the -#' number of columns corresponding to the number of trees. -#' -#' When \code{predcontrib = TRUE} and it is not a multiclass setting, the output is a matrix object with -#' \code{num_features + 1} columns. The last "+ 1" column in a matrix corresponds to bias. -#' For a multiclass case, a list of \code{num_class} elements is returned, where each element is -#' such a matrix. The contribution values are on the scale of untransformed margin -#' (e.g., for binary classification would mean that the contributions are log-odds deviations from bias). -#' -#' When \code{predinteraction = TRUE} and it is not a multiclass setting, the output is a 3d array with -#' dimensions \code{c(nrow, num_features + 1, num_features + 1)}. The off-diagonal (in the last two dimensions) -#' elements represent different features interaction contributions. The array is symmetric WRT the last -#' two dimensions. The "+ 1" columns corresponds to bias. Summing this array along the last dimension should -#' produce practically the same result as predict with \code{predcontrib = TRUE}. -#' For a multiclass case, a list of \code{num_class} elements is returned, where each element is -#' such an array. -#' -#' When \code{strict_shape} is set to \code{TRUE}, the output is always an array. For -#' normal prediction, the output is a 2-dimension array \code{(num_class, nrow(newdata))}. -#' -#' For \code{predcontrib = TRUE}, output is \code{(ncol(newdata) + 1, num_class, nrow(newdata))} -#' For \code{predinteraction = TRUE}, output is \code{(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))} -#' For \code{predleaf = TRUE}, output is \code{(n_trees_in_forest, num_class, n_iterations, nrow(newdata))} -#' -#' @seealso -#' \code{\link{xgb.train}}. +#' The return type depends on `strict_shape`. If `FALSE` (default): +#' - For regression or binary classification: A vector of length `nrows(newdata)`. +#' - For multiclass classification: A vector of length `num_class * nrows(newdata)` or +#' a `(nrows(newdata), num_class)` matrix, depending on the `reshape` value. +#' - When `predleaf = TRUE`: A matrix with one column per tree. +#' - When `predcontrib = TRUE`: When not multiclass, a matrix with +#' ` num_features + 1` columns. The last "+ 1" column corresponds to the baseline value. +#' In the multiclass case, a list of `num_class` such matrices. +#' The contribution values are on the scale of untransformed margin +#' (e.g., for binary classification, the values are log-odds deviations from the baseline). +#' - When `predinteraction = TRUE`: When not multiclass, the output is a 3d array of +#' dimension `c(nrow, num_features + 1, num_features + 1)`. The off-diagonal (in the last two dimensions) +#' elements represent different feature interaction contributions. The array is symmetric WRT the last +#' two dimensions. The "+ 1" columns corresponds to the baselines. Summing this array along the last dimension should +#' produce practically the same result as `predcontrib = TRUE`. +#' In the multiclass case, a list of `num_class` such arrays. #' +#' When `strict_shape = TRUE`, the output is always an array: +#' - For normal predictions, the output has dimension `(num_class, nrow(newdata))`. +#' - For `predcontrib = TRUE`, the dimension is `(ncol(newdata) + 1, num_class, nrow(newdata))`. +#' - For `predinteraction = TRUE`, the dimension is `(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))`. +#' - For `predleaf = TRUE`, the dimension is `(n_trees_in_forest, num_class, n_iterations, nrow(newdata))`. +#' @seealso [xgb.train()] #' @references -#' -#' Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} -#' -#' Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +#' 1. Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +#' NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} +#' 2. Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +#' \url{https://arxiv.org/abs/1706.06060} #' #' @examples #' ## binary classification: #' -#' data(agaricus.train, package='xgboost') -#' data(agaricus.test, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' data(agaricus.test, package = "xgboost") #' #' ## Keep the number of threads to 2 for examples #' nthread <- 2 @@ -275,8 +272,16 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' train <- agaricus.train #' test <- agaricus.test #' -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 0.5, nthread = nthread, nrounds = 5, objective = "binary:logistic") +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 0.5, +#' nthread = nthread, +#' nrounds = 5, +#' objective = "binary:logistic" +#' ) +#' #' # use all trees by default #' pred <- predict(bst, test$data) #' # use only the 1st tree @@ -308,32 +313,53 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' #' lb <- as.numeric(iris$Species) - 1 #' num_class <- 3 +#' #' set.seed(11) -#' bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, -#' max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, -#' objective = "multi:softprob", num_class = num_class) +#' +#' bst <- xgboost( +#' data = as.matrix(iris[, -5]), +#' label = lb, +#' max_depth = 4, +#' eta = 0.5, +#' nthread = 2, +#' nrounds = 10, +#' subsample = 0.5, +#' objective = "multi:softprob", +#' num_class = num_class +#' ) +#' #' # predict for softmax returns num_class probability numbers per case: #' pred <- predict(bst, as.matrix(iris[, -5])) #' str(pred) #' # reshape it to a num_class-columns matrix -#' pred <- matrix(pred, ncol=num_class, byrow=TRUE) +#' pred <- matrix(pred, ncol = num_class, byrow = TRUE) #' # convert the probabilities to softmax labels #' pred_labels <- max.col(pred) - 1 #' # the following should result in the same error as seen in the last iteration -#' sum(pred_labels != lb)/length(lb) +#' sum(pred_labels != lb) / length(lb) #' -#' # compare that to the predictions from softmax: +#' # compare with predictions from softmax: #' set.seed(11) -#' bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, -#' max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, -#' objective = "multi:softmax", num_class = num_class) +#' +#' bst <- xgboost( +#' data = as.matrix(iris[, -5]), +#' label = lb, +#' max_depth = 4, +#' eta = 0.5, +#' nthread = 2, +#' nrounds = 10, +#' subsample = 0.5, +#' objective = "multi:softmax", +#' num_class = num_class +#' ) +#' #' pred <- predict(bst, as.matrix(iris[, -5])) #' str(pred) #' all.equal(pred, pred_labels) #' # prediction from using only 5 iterations should result #' # in the same error as seen in iteration 5: -#' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange=c(1, 6)) -#' sum(pred5 != lb)/length(lb) +#' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) +#' sum(pred5 != lb) / length(lb) #' #' @rdname predict.xgb.Booster #' @export @@ -497,63 +523,69 @@ predict.xgb.Booster.handle <- function(object, ...) { } -#' Accessors for serializable attributes of a model. +#' Accessors for serializable attributes of a model #' #' These methods allow to manipulate the key-value attribute strings of an xgboost model. #' -#' @param object Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}. -#' @param name a non-empty character string specifying which attribute is to be accessed. -#' @param value a value of an attribute for \code{xgb.attr<-}; for \code{xgb.attributes<-} -#' it's a list (or an object coercible to a list) with the names of attributes to set +#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param name A non-empty character string specifying which attribute is to be accessed. +#' @param value For `xgb.attr<-`, a value of an attribute; for `xgb.attributes<-`, +#' it is a list (or an object coercible to a list) with the names of attributes to set #' and the elements corresponding to attribute values. #' Non-character values are converted to character. -#' When attribute value is not a scalar, only the first index is used. -#' Use \code{NULL} to remove an attribute. +#' When an attribute value is not a scalar, only the first index is used. +#' Use `NULL` to remove an attribute. #' #' @details -#' The primary purpose of xgboost model attributes is to store some meta-data about the model. +#' The primary purpose of xgboost model attributes is to store some meta data about the model. #' Note that they are a separate concept from the object attributes in R. #' Specifically, they refer to key-value strings that can be attached to an xgboost model, #' stored together with the model's binary representation, and accessed later #' (from R or any other interface). -#' In contrast, any R-attribute assigned to an R-object of \code{xgb.Booster} class -#' would not be saved by \code{xgb.save} because an xgboost model is an external memory object +#' In contrast, any R attribute assigned to an R object of `xgb.Booster` class +#' would not be saved by [xgb.save()] because an xgboost model is an external memory object #' and its serialization is handled externally. #' Also, setting an attribute that has the same name as one of xgboost's parameters wouldn't #' change the value of that parameter for a model. -#' Use \code{\link{xgb.parameters<-}} to set or change model parameters. +#' Use [xgb.parameters<-()] to set or change model parameters. #' -#' The attribute setters would usually work more efficiently for \code{xgb.Booster.handle} -#' than for \code{xgb.Booster}, since only just a handle (pointer) would need to be copied. +#' The attribute setters would usually work more efficiently for `xgb.Booster.handle` +#' than for `xgb.Booster`, since only just a handle (pointer) would need to be copied. #' That would only matter if attributes need to be set many times. -#' Note, however, that when feeding a handle of an \code{xgb.Booster} object to the attribute setters, -#' the raw model cache of an \code{xgb.Booster} object would not be automatically updated, -#' and it would be user's responsibility to call \code{xgb.serialize} to update it. +#' Note, however, that when feeding a handle of an `xgb.Booster` object to the attribute setters, +#' the raw model cache of an `xgb.Booster` object would not be automatically updated, +#' and it would be the user's responsibility to call [xgb.serialize()] to update it. #' -#' The \code{xgb.attributes<-} setter either updates the existing or adds one or several attributes, +#' The `xgb.attributes<-` setter either updates the existing or adds one or several attributes, #' but it doesn't delete the other existing attributes. #' #' @return -#' \code{xgb.attr} returns either a string value of an attribute -#' or \code{NULL} if an attribute wasn't stored in a model. -#' -#' \code{xgb.attributes} returns a list of all attribute stored in a model -#' or \code{NULL} if a model has no stored attributes. +#' - `xgb.attr()` returns either a string value of an attribute +#' or `NULL` if an attribute wasn't stored in a model. +#' - `xgb.attributes()` returns a list of all attributes stored in a model +#' or `NULL` if a model has no stored attributes. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' train <- agaricus.train #' -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' #' xgb.attr(bst, "my_attribute") <- "my attribute value" #' print(xgb.attr(bst, "my_attribute")) #' xgb.attributes(bst) <- list(a = 123, b = "abc") #' -#' xgb.save(bst, 'xgb.model') -#' bst1 <- xgb.load('xgb.model') -#' if (file.exists('xgb.model')) file.remove('xgb.model') +#' xgb.save(bst, "xgb.model") +#' bst1 <- xgb.load("xgb.model") +#' if (file.exists("xgb.model")) file.remove("xgb.model") #' print(xgb.attr(bst1, "my_attribute")) #' print(xgb.attributes(bst1)) #' @@ -632,22 +664,29 @@ xgb.attributes <- function(object) { object } -#' Accessors for model parameters as JSON string. +#' Accessors for model parameters as JSON string #' -#' @param object Object of class \code{xgb.Booster} +#' @param object Object of class `xgb.Booster`. #' @param value A JSON string. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' #' ## Keep the number of threads to 1 for examples #' nthread <- 1 #' data.table::setDTthreads(nthread) #' train <- agaricus.train #' #' bst <- xgboost( -#' data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 2, +#' objective = "binary:logistic" #' ) +#' #' config <- xgb.config(bst) #' #' @rdname xgb.config @@ -667,24 +706,31 @@ xgb.config <- function(object) { object } -#' Accessors for model parameters. +#' Accessors for model parameters #' #' Only the setter for xgboost parameters is currently implemented. #' -#' @param object Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}. -#' @param value a list (or an object coercible to a list) with the names of parameters to set +#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param value A list (or an object coercible to a list) with the names of parameters to set #' and the elements corresponding to parameter values. #' #' @details -#' Note that the setter would usually work more efficiently for \code{xgb.Booster.handle} -#' than for \code{xgb.Booster}, since only just a handle would need to be copied. +#' Note that the setter would usually work more efficiently for `xgb.Booster.handle` +#' than for `xgb.Booster`, since only just a handle would need to be copied. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' train <- agaricus.train #' -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' #' xgb.parameters(bst) <- list(eta = 0.1) #' @@ -724,23 +770,31 @@ xgb.ntree <- function(bst) { #' Print xgb.Booster #' -#' Print information about xgb.Booster. +#' Print information about `xgb.Booster`. #' -#' @param x an xgb.Booster object -#' @param verbose whether to print detailed data (e.g., attribute values) -#' @param ... not currently used +#' @param x An `xgb.Booster` object. +#' @param verbose Whether to print detailed data (e.g., attribute values). +#' @param ... Not currently used. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' train <- agaricus.train -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") -#' attr(bst, 'myattr') <- 'memo' +#' +#' bst <- xgboost( +#' data = train$data, +#' label = train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) +#' +#' attr(bst, "myattr") <- "memo" #' #' print(bst) -#' print(bst, verbose=TRUE) +#' print(bst, verbose = TRUE) #' -#' @method print xgb.Booster #' @export print.xgb.Booster <- function(x, verbose = FALSE, ...) { cat('##### xgb.Booster\n') diff --git a/R-package/R/xgb.create.features.R b/R-package/R/xgb.create.features.R index 98b03ea8a..baef3bb03 100644 --- a/R-package/R/xgb.create.features.R +++ b/R-package/R/xgb.create.features.R @@ -51,7 +51,7 @@ #' dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) #' dtest <- with(agaricus.test, xgb.DMatrix(data, label = label, nthread = 2)) #' -#' param <- list(max_depth=2, eta=1, silent=1, objective='binary:logistic') +#' param <- list(max_depth=2, eta=1, objective='binary:logistic') #' nrounds = 4 #' #' bst = xgb.train(params = param, data = dtrain, nrounds = nrounds, nthread = 2) diff --git a/R-package/R/xgb.plot.shap.R b/R-package/R/xgb.plot.shap.R index d9afd5546..d61bd23d4 100644 --- a/R-package/R/xgb.plot.shap.R +++ b/R-package/R/xgb.plot.shap.R @@ -7,7 +7,7 @@ #' \code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}. #' @param features a vector of either column indices or of feature names to plot. When it is NULL, #' feature importance is calculated, and \code{top_n} high ranked features are taken. -#' @param top_n when \code{features} is NULL, top_n [1, 100] most important features in a model are taken. +#' @param top_n when \code{features} is NULL, top_n `[1, 100]` most important features in a model are taken. #' @param model an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} #' or \code{features} is missing. #' @param trees passed to \code{\link{xgb.importance}} when \code{features = NULL}. @@ -197,7 +197,7 @@ xgb.plot.shap <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, #' hence allows us to see which features have a negative / positive contribution #' on the model prediction, and whether the contribution is different for larger #' or smaller values of the feature. We effectively try to replicate the -#' \code{summary_plot} function from https://github.com/shap/shap. +#' \code{summary_plot} function from . #' #' @inheritParams xgb.plot.shap #' diff --git a/R-package/R/xgboost.R b/R-package/R/xgboost.R index e60ea2de8..f61c535e2 100644 --- a/R-package/R/xgboost.R +++ b/R-package/R/xgboost.R @@ -40,10 +40,10 @@ xgboost <- function(data = NULL, label = NULL, missing = NA, weight = NULL, #' } #' #' @references -#' https://archive.ics.uci.edu/ml/datasets/Mushroom +#' #' #' Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -#' [http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +#' . Irvine, CA: University of California, #' School of Information and Computer Science. #' #' @docType data @@ -67,10 +67,10 @@ NULL #' } #' #' @references -#' https://archive.ics.uci.edu/ml/datasets/Mushroom +#' #' #' Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -#' [http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +#' . Irvine, CA: University of California, #' School of Information and Computer Science. #' #' @docType data diff --git a/R-package/man/agaricus.test.Rd b/R-package/man/agaricus.test.Rd index e3694ae0d..f90a5a0d2 100644 --- a/R-package/man/agaricus.test.Rd +++ b/R-package/man/agaricus.test.Rd @@ -19,15 +19,15 @@ UCI Machine Learning Repository. This data set includes the following fields: \itemize{ - \item \code{label} the label for each record - \item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. +\item \code{label} the label for each record +\item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. } } \references{ -https://archive.ics.uci.edu/ml/datasets/Mushroom +\url{https://archive.ics.uci.edu/ml/datasets/Mushroom} Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -[http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +\url{http://archive.ics.uci.edu/ml}. Irvine, CA: University of California, School of Information and Computer Science. } \keyword{datasets} diff --git a/R-package/man/agaricus.train.Rd b/R-package/man/agaricus.train.Rd index 92692c965..dd05410ba 100644 --- a/R-package/man/agaricus.train.Rd +++ b/R-package/man/agaricus.train.Rd @@ -19,15 +19,15 @@ UCI Machine Learning Repository. This data set includes the following fields: \itemize{ - \item \code{label} the label for each record - \item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. +\item \code{label} the label for each record +\item \code{data} a sparse Matrix of \code{dgCMatrix} class, with 126 columns. } } \references{ -https://archive.ics.uci.edu/ml/datasets/Mushroom +\url{https://archive.ics.uci.edu/ml/datasets/Mushroom} Bache, K. & Lichman, M. (2013). UCI Machine Learning Repository -[http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, +\url{http://archive.ics.uci.edu/ml}. Irvine, CA: University of California, School of Information and Computer Science. } \keyword{datasets} diff --git a/R-package/man/cb.save.model.Rd b/R-package/man/cb.save.model.Rd index fd564b3e8..584fd69b7 100644 --- a/R-package/man/cb.save.model.Rd +++ b/R-package/man/cb.save.model.Rd @@ -13,7 +13,7 @@ cb.save.model(save_period = 0, save_name = "xgboost.model") \item{save_name}{the name or path for the saved model file. It can contain a \code{\link[base]{sprintf}} formatting specifier to include the integer iteration number in the file name. -E.g., with \code{save_name} = 'xgboost_%04d.model', +E.g., with \code{save_name} = 'xgboost_\%04d.model', the file saved at iteration 50 would be named "xgboost_0050.model".} } \description{ diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index 9503c2154..71f855d8a 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -23,15 +23,15 @@ Get information of an xgb.DMatrix object The \code{name} field can be one of the following: \itemize{ - \item \code{label} - \item \code{weight} - \item \code{base_margin} - \item \code{label_lower_bound} - \item \code{label_upper_bound} - \item \code{group} - \item \code{feature_type} - \item \code{feature_name} - \item \code{nrow} +\item \code{label} +\item \code{weight} +\item \code{base_margin} +\item \code{label_lower_bound} +\item \code{label_upper_bound} +\item \code{group} +\item \code{feature_type} +\item \code{feature_name} +\item \code{nrow} } See the documentation for \link{xgb.DMatrix} for more information about these fields. diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index c1e58f63b..135177dda 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -3,7 +3,7 @@ \name{predict.xgb.Booster} \alias{predict.xgb.Booster} \alias{predict.xgb.Booster.handle} -\title{Predict method for eXtreme Gradient Boosting model} +\title{Predict method for XGBoost model} \usage{ \method{predict}{xgb.Booster}( object, @@ -25,90 +25,86 @@ \method{predict}{xgb.Booster.handle}(object, ...) } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}} +\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} -\item{newdata}{takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, - local data file or \code{xgb.DMatrix}. +\item{newdata}{Takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, +local data file, or \code{xgb.DMatrix}. +For single-row predictions on sparse data, it is recommended to use the CSR format. +If passing a sparse vector, it will take it as a row vector.} - For single-row predictions on sparse data, it's recommended to use CSR format. If passing - a sparse vector, it will take it as a row vector.} +\item{missing}{Only used when input is a dense matrix. Pick a float value that represents +missing values in data (e.g., 0 or some other extreme value).} -\item{missing}{Missing is only used when input is dense matrix. Pick a float value that represents -missing values in data (e.g., sometimes 0 or some other extreme value is used).} - -\item{outputmargin}{whether the prediction should be returned in the for of original untransformed +\item{outputmargin}{Whether the prediction should be returned in the form of original untransformed sum of predictions from boosting iterations' results. E.g., setting \code{outputmargin=TRUE} for -logistic regression would result in predictions for log-odds instead of probabilities.} +logistic regression would return log-odds instead of probabilities.} \item{ntreelimit}{Deprecated, use \code{iterationrange} instead.} -\item{predleaf}{whether predict leaf index.} +\item{predleaf}{Whether to predict pre-tree leaf indices.} -\item{predcontrib}{whether to return feature contributions to individual predictions (see Details).} +\item{predcontrib}{Whether to return feature contributions to individual predictions (see Details).} -\item{approxcontrib}{whether to use a fast approximation for feature contributions (see Details).} +\item{approxcontrib}{Whether to use a fast approximation for feature contributions (see Details).} -\item{predinteraction}{whether to return contributions of feature interactions to individual predictions (see Details).} +\item{predinteraction}{Whether to return contributions of feature interactions to individual predictions (see Details).} -\item{reshape}{whether to reshape the vector of predictions to a matrix form when there are several -prediction outputs per case. This option has no effect when either of predleaf, predcontrib, -or predinteraction flags is TRUE.} +\item{reshape}{Whether to reshape the vector of predictions to matrix form when there are several +prediction outputs per case. No effect if \code{predleaf}, \code{predcontrib}, +or \code{predinteraction} is \code{TRUE}.} -\item{training}{whether is the prediction result used for training. For dart booster, +\item{training}{Whether the predictions are used for training. For dart booster, training predicting will perform dropout.} -\item{iterationrange}{Specifies which layer of trees are used in prediction. For -example, if a random forest is trained with 100 rounds. Specifying -`iterationrange=(1, 21)`, then only the forests built during [1, 21) (half open set) -rounds are used in this prediction. It's 1-based index just like R vector. When set -to \code{c(1, 1)} XGBoost will use all trees.} +\item{iterationrange}{Specifies which trees are used in prediction. For +example, take a random forest with 100 rounds. +With \code{iterationrange=c(1, 21)}, only the trees built during \verb{[1, 21)} (half open set) +rounds are used in this prediction. The index is 1-based just like an R vector. When set +to \code{c(1, 1)}, XGBoost will use all trees.} -\item{strict_shape}{Default is \code{FALSE}. When it's set to \code{TRUE}, output -type and shape of prediction are invariant to model type.} +\item{strict_shape}{Default is \code{FALSE}. When set to \code{TRUE}, the output +type and shape of predictions are invariant to the model type.} \item{...}{Not used.} } \value{ -The return type is different depending whether \code{strict_shape} is set to \code{TRUE}. By default, -for regression or binary classification, it returns a vector of length \code{nrows(newdata)}. -For multiclass classification, either a \code{num_class * nrows(newdata)} vector or -a \code{(nrows(newdata), num_class)} dimension matrix is returned, depending on -the \code{reshape} value. +The return type depends on \code{strict_shape}. If \code{FALSE} (default): +\itemize{ +\item For regression or binary classification: A vector of length \code{nrows(newdata)}. +\item For multiclass classification: A vector of length \code{num_class * nrows(newdata)} or +a \verb{(nrows(newdata), num_class)} matrix, depending on the \code{reshape} value. +\item When \code{predleaf = TRUE}: A matrix with one column per tree. +\item When \code{predcontrib = TRUE}: When not multiclass, a matrix with +\code{ num_features + 1} columns. The last "+ 1" column corresponds to the baseline value. +In the multiclass case, a list of \code{num_class} such matrices. +The contribution values are on the scale of untransformed margin +(e.g., for binary classification, the values are log-odds deviations from the baseline). +\item When \code{predinteraction = TRUE}: When not multiclass, the output is a 3d array of +dimension \code{c(nrow, num_features + 1, num_features + 1)}. The off-diagonal (in the last two dimensions) +elements represent different feature interaction contributions. The array is symmetric WRT the last +two dimensions. The "+ 1" columns corresponds to the baselines. Summing this array along the last dimension should +produce practically the same result as \code{predcontrib = TRUE}. +In the multiclass case, a list of \code{num_class} such arrays. +} -When \code{predleaf = TRUE}, the output is a matrix object with the -number of columns corresponding to the number of trees. - -When \code{predcontrib = TRUE} and it is not a multiclass setting, the output is a matrix object with -\code{num_features + 1} columns. The last "+ 1" column in a matrix corresponds to bias. -For a multiclass case, a list of \code{num_class} elements is returned, where each element is -such a matrix. The contribution values are on the scale of untransformed margin -(e.g., for binary classification would mean that the contributions are log-odds deviations from bias). - -When \code{predinteraction = TRUE} and it is not a multiclass setting, the output is a 3d array with -dimensions \code{c(nrow, num_features + 1, num_features + 1)}. The off-diagonal (in the last two dimensions) -elements represent different features interaction contributions. The array is symmetric WRT the last -two dimensions. The "+ 1" columns corresponds to bias. Summing this array along the last dimension should -produce practically the same result as predict with \code{predcontrib = TRUE}. -For a multiclass case, a list of \code{num_class} elements is returned, where each element is -such an array. - -When \code{strict_shape} is set to \code{TRUE}, the output is always an array. For -normal prediction, the output is a 2-dimension array \code{(num_class, nrow(newdata))}. - -For \code{predcontrib = TRUE}, output is \code{(ncol(newdata) + 1, num_class, nrow(newdata))} -For \code{predinteraction = TRUE}, output is \code{(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))} -For \code{predleaf = TRUE}, output is \code{(n_trees_in_forest, num_class, n_iterations, nrow(newdata))} +When \code{strict_shape = TRUE}, the output is always an array: +\itemize{ +\item For normal predictions, the output has dimension \verb{(num_class, nrow(newdata))}. +\item For \code{predcontrib = TRUE}, the dimension is \verb{(ncol(newdata) + 1, num_class, nrow(newdata))}. +\item For \code{predinteraction = TRUE}, the dimension is \verb{(ncol(newdata) + 1, ncol(newdata) + 1, num_class, nrow(newdata))}. +\item For \code{predleaf = TRUE}, the dimension is \verb{(n_trees_in_forest, num_class, n_iterations, nrow(newdata))}. +} } \description{ Predicted values based on either xgboost model or model handle object. } \details{ -Note that \code{iterationrange} would currently do nothing for predictions from gblinear, -since gblinear doesn't keep its boosting history. +Note that \code{iterationrange} would currently do nothing for predictions from "gblinear", +since "gblinear" doesn't keep its boosting history. One possible practical applications of the \code{predleaf} option is to use the model as a generator of new features which capture non-linearity and interactions, -e.g., as implemented in \code{\link{xgb.create.features}}. +e.g., as implemented in \code{\link[=xgb.create.features]{xgb.create.features()}}. Setting \code{predcontrib = TRUE} allows to calculate contributions of each feature to individual predictions. For "gblinear" booster, feature contributions are simply linear terms @@ -124,14 +120,14 @@ Since it quadratically depends on the number of features, it is recommended to p of the most important features first. See below about the format of the returned results. The \code{predict()} method uses as many threads as defined in \code{xgb.Booster} object (all by default). -If you want to change their number, then assign a new number to \code{nthread} using \code{\link{xgb.parameters<-}}. -Note also that converting a matrix to \code{\link{xgb.DMatrix}} uses multiple threads too. +If you want to change their number, assign a new number to \code{nthread} using \code{\link[=xgb.parameters<-]{xgb.parameters<-()}}. +Note that converting a matrix to \code{\link[=xgb.DMatrix]{xgb.DMatrix()}} uses multiple threads too. } \examples{ ## binary classification: -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') +data(agaricus.train, package = "xgboost") +data(agaricus.test, package = "xgboost") ## Keep the number of threads to 2 for examples nthread <- 2 @@ -140,8 +136,16 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 0.5, nthread = nthread, nrounds = 5, objective = "binary:logistic") +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 0.5, + nthread = nthread, + nrounds = 5, + objective = "binary:logistic" +) + # use all trees by default pred <- predict(bst, test$data) # use only the 1st tree @@ -173,39 +177,63 @@ par(mar = old_mar) lb <- as.numeric(iris$Species) - 1 num_class <- 3 + set.seed(11) -bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, - max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, - objective = "multi:softprob", num_class = num_class) + +bst <- xgboost( + data = as.matrix(iris[, -5]), + label = lb, + max_depth = 4, + eta = 0.5, + nthread = 2, + nrounds = 10, + subsample = 0.5, + objective = "multi:softprob", + num_class = num_class +) + # predict for softmax returns num_class probability numbers per case: pred <- predict(bst, as.matrix(iris[, -5])) str(pred) # reshape it to a num_class-columns matrix -pred <- matrix(pred, ncol=num_class, byrow=TRUE) +pred <- matrix(pred, ncol = num_class, byrow = TRUE) # convert the probabilities to softmax labels pred_labels <- max.col(pred) - 1 # the following should result in the same error as seen in the last iteration -sum(pred_labels != lb)/length(lb) +sum(pred_labels != lb) / length(lb) -# compare that to the predictions from softmax: +# compare with predictions from softmax: set.seed(11) -bst <- xgboost(data = as.matrix(iris[, -5]), label = lb, - max_depth = 4, eta = 0.5, nthread = 2, nrounds = 10, subsample = 0.5, - objective = "multi:softmax", num_class = num_class) + +bst <- xgboost( + data = as.matrix(iris[, -5]), + label = lb, + max_depth = 4, + eta = 0.5, + nthread = 2, + nrounds = 10, + subsample = 0.5, + objective = "multi:softmax", + num_class = num_class +) + pred <- predict(bst, as.matrix(iris[, -5])) str(pred) all.equal(pred, pred_labels) # prediction from using only 5 iterations should result # in the same error as seen in iteration 5: -pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange=c(1, 6)) -sum(pred5 != lb)/length(lb) +pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) +sum(pred5 != lb) / length(lb) } \references{ -Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} - -Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +\enumerate{ +\item Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} +\item Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +\url{https://arxiv.org/abs/1706.06060} +} } \seealso{ -\code{\link{xgb.train}}. +\code{\link[=xgb.train]{xgb.train()}} } diff --git a/R-package/man/print.xgb.Booster.Rd b/R-package/man/print.xgb.Booster.Rd index d684882f5..4d09bb5ec 100644 --- a/R-package/man/print.xgb.Booster.Rd +++ b/R-package/man/print.xgb.Booster.Rd @@ -7,23 +7,32 @@ \method{print}{xgb.Booster}(x, verbose = FALSE, ...) } \arguments{ -\item{x}{an xgb.Booster object} +\item{x}{An \code{xgb.Booster} object.} -\item{verbose}{whether to print detailed data (e.g., attribute values)} +\item{verbose}{Whether to print detailed data (e.g., attribute values).} -\item{...}{not currently used} +\item{...}{Not currently used.} } \description{ -Print information about xgb.Booster. +Print information about \code{xgb.Booster}. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") train <- agaricus.train -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") -attr(bst, 'myattr') <- 'memo' + +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) + +attr(bst, "myattr") <- "memo" print(bst) -print(bst, verbose=TRUE) +print(bst, verbose = TRUE) } diff --git a/R-package/man/xgb.Booster.complete.Rd b/R-package/man/xgb.Booster.complete.Rd index 214694565..0adb1a69f 100644 --- a/R-package/man/xgb.Booster.complete.Rd +++ b/R-package/man/xgb.Booster.complete.Rd @@ -2,14 +2,14 @@ % Please edit documentation in R/xgb.Booster.R \name{xgb.Booster.complete} \alias{xgb.Booster.complete} -\title{Restore missing parts of an incomplete xgb.Booster object.} +\title{Restore missing parts of an incomplete xgb.Booster object} \usage{ xgb.Booster.complete(object, saveraw = TRUE) } \arguments{ -\item{object}{object of class \code{xgb.Booster}} +\item{object}{Object of class \code{xgb.Booster}.} -\item{saveraw}{a flag indicating whether to append \code{raw} Booster memory dump data +\item{saveraw}{A flag indicating whether to append \code{raw} Booster memory dump data when it doesn't already exist.} } \value{ @@ -27,15 +27,24 @@ While this method is primarily for internal use, it might be useful in some prac E.g., when an \code{xgb.Booster} model is saved as an R object and then is loaded as an R object, its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods should still work for such a model object since those methods would be using -\code{xgb.Booster.complete} internally. However, one might find it to be more efficient to call the -\code{xgb.Booster.complete} function explicitly once after loading a model as an R-object. +\code{xgb.Booster.complete()} internally. However, one might find it to be more efficient to call the +\code{xgb.Booster.complete()} function explicitly once after loading a model as an R-object. That would prevent further repeated implicit reconstruction of an internal booster model. } \examples{ -data(agaricus.train, package='xgboost') -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +data(agaricus.train, package = "xgboost") + +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) + saveRDS(bst, "xgb.model.rds") # Warning: The resulting RDS file is only compatible with the current XGBoost version. diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index a1ef39f0b..01436ba14 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -38,7 +38,8 @@ so it doesn't make sense to assign weights to individual data points.} \item{base_margin}{Base margin used for boosting from existing model. - In the case of multi-output models, one can also pass multi-dimensional base_margin.} +\if{html}{\out{
}}\preformatted{ In the case of multi-output models, one can also pass multi-dimensional base_margin. +}\if{html}{\out{
}}} \item{missing}{a float value to represents missing values in data (used only when input is a dense matrix). It is useful when a 0 or some other extreme value represents missing values in data.} @@ -62,16 +63,17 @@ frame and matrix.} \item{enable_categorical}{Experimental support of specializing for categorical features. - If passing 'TRUE' and 'data' is a data frame, - columns of categorical types will automatically - be set to be of categorical type (feature_type='c') in the resulting DMatrix. +\if{html}{\out{
}}\preformatted{ If passing 'TRUE' and 'data' is a data frame, + columns of categorical types will automatically + be set to be of categorical type (feature_type='c') in the resulting DMatrix. - If passing 'FALSE' and 'data' is a data frame with categorical columns, - it will result in an error being thrown. + If passing 'FALSE' and 'data' is a data frame with categorical columns, + it will result in an error being thrown. - If 'data' is not a data frame, this argument is ignored. + If 'data' is not a data frame, this argument is ignored. - JSON/UBJSON serialization format is required for this.} + JSON/UBJSON serialization format is required for this. +}\if{html}{\out{
}}} } \description{ Construct xgb.DMatrix object from either a dense matrix, a sparse matrix, or a local file. diff --git a/R-package/man/xgb.attr.Rd b/R-package/man/xgb.attr.Rd index 03779e420..9203e6281 100644 --- a/R-package/man/xgb.attr.Rd +++ b/R-package/man/xgb.attr.Rd @@ -5,7 +5,7 @@ \alias{xgb.attr<-} \alias{xgb.attributes} \alias{xgb.attributes<-} -\title{Accessors for serializable attributes of a model.} +\title{Accessors for serializable attributes of a model} \usage{ xgb.attr(object, name) @@ -18,62 +18,70 @@ xgb.attributes(object) <- value \arguments{ \item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} -\item{name}{a non-empty character string specifying which attribute is to be accessed.} +\item{name}{A non-empty character string specifying which attribute is to be accessed.} -\item{value}{a value of an attribute for \code{xgb.attr<-}; for \code{xgb.attributes<-} -it's a list (or an object coercible to a list) with the names of attributes to set +\item{value}{For \verb{xgb.attr<-}, a value of an attribute; for \verb{xgb.attributes<-}, +it is a list (or an object coercible to a list) with the names of attributes to set and the elements corresponding to attribute values. Non-character values are converted to character. -When attribute value is not a scalar, only the first index is used. +When an attribute value is not a scalar, only the first index is used. Use \code{NULL} to remove an attribute.} } \value{ -\code{xgb.attr} returns either a string value of an attribute +\itemize{ +\item \code{xgb.attr()} returns either a string value of an attribute or \code{NULL} if an attribute wasn't stored in a model. - -\code{xgb.attributes} returns a list of all attribute stored in a model +\item \code{xgb.attributes()} returns a list of all attributes stored in a model or \code{NULL} if a model has no stored attributes. } +} \description{ These methods allow to manipulate the key-value attribute strings of an xgboost model. } \details{ -The primary purpose of xgboost model attributes is to store some meta-data about the model. +The primary purpose of xgboost model attributes is to store some meta data about the model. Note that they are a separate concept from the object attributes in R. Specifically, they refer to key-value strings that can be attached to an xgboost model, stored together with the model's binary representation, and accessed later (from R or any other interface). -In contrast, any R-attribute assigned to an R-object of \code{xgb.Booster} class -would not be saved by \code{xgb.save} because an xgboost model is an external memory object +In contrast, any R attribute assigned to an R object of \code{xgb.Booster} class +would not be saved by \code{\link[=xgb.save]{xgb.save()}} because an xgboost model is an external memory object and its serialization is handled externally. Also, setting an attribute that has the same name as one of xgboost's parameters wouldn't change the value of that parameter for a model. -Use \code{\link{xgb.parameters<-}} to set or change model parameters. +Use \code{\link[=xgb.parameters<-]{xgb.parameters<-()}} to set or change model parameters. The attribute setters would usually work more efficiently for \code{xgb.Booster.handle} than for \code{xgb.Booster}, since only just a handle (pointer) would need to be copied. That would only matter if attributes need to be set many times. Note, however, that when feeding a handle of an \code{xgb.Booster} object to the attribute setters, the raw model cache of an \code{xgb.Booster} object would not be automatically updated, -and it would be user's responsibility to call \code{xgb.serialize} to update it. +and it would be the user's responsibility to call \code{\link[=xgb.serialize]{xgb.serialize()}} to update it. -The \code{xgb.attributes<-} setter either updates the existing or adds one or several attributes, +The \verb{xgb.attributes<-} setter either updates the existing or adds one or several attributes, but it doesn't delete the other existing attributes. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") train <- agaricus.train -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) xgb.attr(bst, "my_attribute") <- "my attribute value" print(xgb.attr(bst, "my_attribute")) xgb.attributes(bst) <- list(a = 123, b = "abc") -xgb.save(bst, 'xgb.model') -bst1 <- xgb.load('xgb.model') -if (file.exists('xgb.model')) file.remove('xgb.model') +xgb.save(bst, "xgb.model") +bst1 <- xgb.load("xgb.model") +if (file.exists("xgb.model")) file.remove("xgb.model") print(xgb.attr(bst1, "my_attribute")) print(xgb.attributes(bst1)) diff --git a/R-package/man/xgb.config.Rd b/R-package/man/xgb.config.Rd index 35545cc77..83040b877 100644 --- a/R-package/man/xgb.config.Rd +++ b/R-package/man/xgb.config.Rd @@ -3,31 +3,38 @@ \name{xgb.config} \alias{xgb.config} \alias{xgb.config<-} -\title{Accessors for model parameters as JSON string.} +\title{Accessors for model parameters as JSON string} \usage{ xgb.config(object) xgb.config(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster}} +\item{object}{Object of class \code{xgb.Booster}.} \item{value}{A JSON string.} } \description{ -Accessors for model parameters as JSON string. +Accessors for model parameters as JSON string } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") + ## Keep the number of threads to 1 for examples nthread <- 1 data.table::setDTthreads(nthread) train <- agaricus.train bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = nthread, + nrounds = 2, + objective = "binary:logistic" ) + config <- xgb.config(bst) } diff --git a/R-package/man/xgb.create.features.Rd b/R-package/man/xgb.create.features.Rd index 64d4af158..68b561997 100644 --- a/R-package/man/xgb.create.features.Rd +++ b/R-package/man/xgb.create.features.Rd @@ -48,7 +48,7 @@ be the binary vector \code{[0, 1, 0, 1, 0]}, where the first 3 entries correspond to the leaves of the first subtree and last 2 to those of the second subtree. -[...] +\link{...} We can understand boosted decision tree based transformation as a supervised feature encoding that @@ -62,7 +62,7 @@ data(agaricus.test, package='xgboost') dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) dtest <- with(agaricus.test, xgb.DMatrix(data, label = label, nthread = 2)) -param <- list(max_depth=2, eta=1, silent=1, objective='binary:logistic') +param <- list(max_depth=2, eta=1, objective='binary:logistic') nrounds = 4 bst = xgb.train(params = param, data = dtrain, nrounds = nrounds, nthread = 2) diff --git a/R-package/man/xgb.cv.Rd b/R-package/man/xgb.cv.Rd index 9c69eb97f..2d8508c4d 100644 --- a/R-package/man/xgb.cv.Rd +++ b/R-package/man/xgb.cv.Rd @@ -29,22 +29,22 @@ xgb.cv( } \arguments{ \item{params}{the list of parameters. The complete list of parameters is - available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below - is a shorter summary: +available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below +is a shorter summary: \itemize{ - \item \code{objective} objective function, common ones are - \itemize{ - \item \code{reg:squarederror} Regression with squared loss. - \item \code{binary:logistic} logistic regression for classification. - \item See \code{\link[=xgb.train]{xgb.train}()} for complete list of objectives. - } - \item \code{eta} step size of each boosting step - \item \code{max_depth} maximum depth of the tree - \item \code{nthread} number of thread used in training, if not set, all threads are used +\item \code{objective} objective function, common ones are +\itemize{ +\item \code{reg:squarederror} Regression with squared loss. +\item \code{binary:logistic} logistic regression for classification. +\item See \code{\link[=xgb.train]{xgb.train}()} for complete list of objectives. +} +\item \code{eta} step size of each boosting step +\item \code{max_depth} maximum depth of the tree +\item \code{nthread} number of thread used in training, if not set, all threads are used } - See \code{\link{xgb.train}} for further details. - See also demo/ for walkthrough example in R.} +See \code{\link{xgb.train}} for further details. +See also demo/ for walkthrough example in R.} \item{data}{takes an \code{xgb.DMatrix}, \code{matrix}, or \code{dgCMatrix} as the input.} @@ -64,17 +64,17 @@ from each CV model. This parameter engages the \code{\link{cb.cv.predict}} callb \item{showsd}{\code{boolean}, whether to show standard deviation of cross validation} \item{metrics, }{list of evaluation metrics to be used in cross validation, - when it is not specified, the evaluation metric is chosen according to objective function. - Possible options are: +when it is not specified, the evaluation metric is chosen according to objective function. +Possible options are: \itemize{ - \item \code{error} binary classification error rate - \item \code{rmse} Rooted mean square error - \item \code{logloss} negative log-likelihood function - \item \code{mae} Mean absolute error - \item \code{mape} Mean absolute percentage error - \item \code{auc} Area under curve - \item \code{aucpr} Area under PR curve - \item \code{merror} Exact matching error, used to evaluate multi-class classification +\item \code{error} binary classification error rate +\item \code{rmse} Rooted mean square error +\item \code{logloss} negative log-likelihood function +\item \code{mae} Mean absolute error +\item \code{mape} Mean absolute percentage error +\item \code{auc} Area under curve +\item \code{aucpr} Area under PR curve +\item \code{merror} Exact matching error, used to evaluate multi-class classification }} \item{obj}{customized objective function. Returns gradient and second order @@ -120,26 +120,26 @@ to customize the training process.} \value{ An object of class \code{xgb.cv.synchronous} with the following elements: \itemize{ - \item \code{call} a function call. - \item \code{params} parameters that were passed to the xgboost library. Note that it does not - capture parameters changed by the \code{\link{cb.reset.parameters}} callback. - \item \code{callbacks} callback functions that were either automatically assigned or - explicitly passed. - \item \code{evaluation_log} evaluation history stored as a \code{data.table} with the - first column corresponding to iteration number and the rest corresponding to the - CV-based evaluation means and standard deviations for the training and test CV-sets. - It is created by the \code{\link{cb.evaluation.log}} callback. - \item \code{niter} number of boosting iterations. - \item \code{nfeatures} number of features in training data. - \item \code{folds} the list of CV folds' indices - either those passed through the \code{folds} - parameter or randomly generated. - \item \code{best_iteration} iteration number with the best evaluation metric value - (only available with early stopping). - \item \code{best_ntreelimit} and the \code{ntreelimit} Deprecated attributes, use \code{best_iteration} instead. - \item \code{pred} CV prediction values available when \code{prediction} is set. - It is either vector or matrix (see \code{\link{cb.cv.predict}}). - \item \code{models} a list of the CV folds' models. It is only available with the explicit - setting of the \code{cb.cv.predict(save_models = TRUE)} callback. +\item \code{call} a function call. +\item \code{params} parameters that were passed to the xgboost library. Note that it does not +capture parameters changed by the \code{\link{cb.reset.parameters}} callback. +\item \code{callbacks} callback functions that were either automatically assigned or +explicitly passed. +\item \code{evaluation_log} evaluation history stored as a \code{data.table} with the +first column corresponding to iteration number and the rest corresponding to the +CV-based evaluation means and standard deviations for the training and test CV-sets. +It is created by the \code{\link{cb.evaluation.log}} callback. +\item \code{niter} number of boosting iterations. +\item \code{nfeatures} number of features in training data. +\item \code{folds} the list of CV folds' indices - either those passed through the \code{folds} +parameter or randomly generated. +\item \code{best_iteration} iteration number with the best evaluation metric value +(only available with early stopping). +\item \code{best_ntreelimit} and the \code{ntreelimit} Deprecated attributes, use \code{best_iteration} instead. +\item \code{pred} CV prediction values available when \code{prediction} is set. +It is either vector or matrix (see \code{\link{cb.cv.predict}}). +\item \code{models} a list of the CV folds' models. It is only available with the explicit +setting of the \code{cb.cv.predict(save_models = TRUE)} callback. } } \description{ diff --git a/R-package/man/xgb.importance.Rd b/R-package/man/xgb.importance.Rd index d9367b211..12daca365 100644 --- a/R-package/man/xgb.importance.Rd +++ b/R-package/man/xgb.importance.Rd @@ -35,20 +35,20 @@ is zero-based (e.g., use \code{trees = 0:4} for first 5 trees).} \value{ For a tree model, a \code{data.table} with the following columns: \itemize{ - \item \code{Features} names of the features used in the model; - \item \code{Gain} represents fractional contribution of each feature to the model based on - the total gain of this feature's splits. Higher percentage means a more important - predictive feature. - \item \code{Cover} metric of the number of observation related to this feature; - \item \code{Frequency} percentage representing the relative number of times - a feature have been used in trees. +\item \code{Features} names of the features used in the model; +\item \code{Gain} represents fractional contribution of each feature to the model based on +the total gain of this feature's splits. Higher percentage means a more important +predictive feature. +\item \code{Cover} metric of the number of observation related to this feature; +\item \code{Frequency} percentage representing the relative number of times +a feature have been used in trees. } A linear model's importance \code{data.table} has the following columns: \itemize{ - \item \code{Features} names of the features used in the model; - \item \code{Weight} the linear coefficient of this feature; - \item \code{Class} (only for multiclass models) class label. +\item \code{Features} names of the features used in the model; +\item \code{Weight} the linear coefficient of this feature; +\item \code{Class} (only for multiclass models) class label. } If \code{feature_names} is not provided and \code{model} doesn't have \code{feature_names}, diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 5a17f9d90..131830bde 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -41,18 +41,18 @@ A \code{data.table} with detailed information about model trees' nodes. The columns of the \code{data.table} are: \itemize{ - \item \code{Tree}: integer ID of a tree in a model (zero-based index) - \item \code{Node}: integer ID of a node in a tree (zero-based index) - \item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) - \item \code{Feature}: for a branch node, it's a feature id or name (when available); - for a leaf note, it simply labels it as \code{'Leaf'} - \item \code{Split}: location of the split for a branch node (split condition is always "less than") - \item \code{Yes}: ID of the next node when the split condition is met - \item \code{No}: ID of the next node when the split condition is not met - \item \code{Missing}: ID of the next node when branch value is missing - \item \code{Quality}: either the split gain (change in loss) or the leaf value - \item \code{Cover}: metric related to the number of observation either seen by a split - or collected by a leaf during training. +\item \code{Tree}: integer ID of a tree in a model (zero-based index) +\item \code{Node}: integer ID of a node in a tree (zero-based index) +\item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) +\item \code{Feature}: for a branch node, it's a feature id or name (when available); +for a leaf note, it simply labels it as \code{'Leaf'} +\item \code{Split}: location of the split for a branch node (split condition is always "less than") +\item \code{Yes}: ID of the next node when the split condition is met +\item \code{No}: ID of the next node when the split condition is not met +\item \code{Missing}: ID of the next node when branch value is missing +\item \code{Quality}: either the split gain (change in loss) or the leaf value +\item \code{Cover}: metric related to the number of observation either seen by a split +or collected by a leaf during training. } When \code{use_int_id=FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers diff --git a/R-package/man/xgb.parameters.Rd b/R-package/man/xgb.parameters.Rd index ab2695650..5305afa51 100644 --- a/R-package/man/xgb.parameters.Rd +++ b/R-package/man/xgb.parameters.Rd @@ -2,14 +2,14 @@ % Please edit documentation in R/xgb.Booster.R \name{xgb.parameters<-} \alias{xgb.parameters<-} -\title{Accessors for model parameters.} +\title{Accessors for model parameters} \usage{ xgb.parameters(object) <- value } \arguments{ \item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} -\item{value}{a list (or an object coercible to a list) with the names of parameters to set +\item{value}{A list (or an object coercible to a list) with the names of parameters to set and the elements corresponding to parameter values.} } \description{ @@ -20,11 +20,18 @@ Note that the setter would usually work more efficiently for \code{xgb.Booster.h than for \code{xgb.Booster}, since only just a handle would need to be copied. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") train <- agaricus.train -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +bst <- xgboost( + data = train$data, + label = train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) xgb.parameters(bst) <- list(eta = 0.1) diff --git a/R-package/man/xgb.plot.deepness.Rd b/R-package/man/xgb.plot.deepness.Rd index 9e23ac130..12c5c68e2 100644 --- a/R-package/man/xgb.plot.deepness.Rd +++ b/R-package/man/xgb.plot.deepness.Rd @@ -44,9 +44,9 @@ Visualizes distributions related to depth of tree leafs. When \code{which="2x1"}, two distributions with respect to the leaf depth are plotted on top of each other: \itemize{ - \item the distribution of the number of leafs in a tree model at a certain depth; - \item the distribution of average weighted number of observations ("cover") - ending up in leafs at certain depth. +\item the distribution of the number of leafs in a tree model at a certain depth; +\item the distribution of average weighted number of observations ("cover") +ending up in leafs at certain depth. } Those could be helpful in determining sensible ranges of the \code{max_depth} and \code{min_child_weight} parameters. diff --git a/R-package/man/xgb.plot.shap.Rd b/R-package/man/xgb.plot.shap.Rd index 6f2d0dfa6..75f8d2d0f 100644 --- a/R-package/man/xgb.plot.shap.Rd +++ b/R-package/man/xgb.plot.shap.Rd @@ -41,7 +41,7 @@ xgb.plot.shap( \item{features}{a vector of either column indices or of feature names to plot. When it is NULL, feature importance is calculated, and \code{top_n} high ranked features are taken.} -\item{top_n}{when \code{features} is NULL, top_n [1, 100] most important features in a model are taken.} +\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} \item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} or \code{features} is missing.} @@ -94,8 +94,8 @@ more than 5 distinct values.} \value{ In addition to producing plots (when \code{plot=TRUE}), it silently returns a list of two matrices: \itemize{ - \item \code{data} the values of selected features; - \item \code{shap_contrib} the contributions of selected features. +\item \code{data} the values of selected features; +\item \code{shap_contrib} the contributions of selected features. } } \description{ diff --git a/R-package/man/xgb.plot.shap.summary.Rd b/R-package/man/xgb.plot.shap.summary.Rd index 3ff8af21c..910119e6f 100644 --- a/R-package/man/xgb.plot.shap.summary.Rd +++ b/R-package/man/xgb.plot.shap.summary.Rd @@ -38,7 +38,7 @@ xgb.plot.shap.summary( \item{features}{a vector of either column indices or of feature names to plot. When it is NULL, feature importance is calculated, and \code{top_n} high ranked features are taken.} -\item{top_n}{when \code{features} is NULL, top_n [1, 100] most important features in a model are taken.} +\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} \item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} or \code{features} is missing.} @@ -67,12 +67,12 @@ Each point (observation) is coloured based on its feature value. The plot hence allows us to see which features have a negative / positive contribution on the model prediction, and whether the contribution is different for larger or smaller values of the feature. We effectively try to replicate the -\code{summary_plot} function from https://github.com/shap/shap. +\code{summary_plot} function from \url{https://github.com/shap/shap}. } \examples{ # See \code{\link{xgb.plot.shap}}. } \seealso{ \code{\link{xgb.plot.shap}}, \code{\link{xgb.ggplot.shap.summary}}, - \url{https://github.com/shap/shap} +\url{https://github.com/shap/shap} } diff --git a/R-package/man/xgb.plot.tree.Rd b/R-package/man/xgb.plot.tree.Rd index d419eb76a..224e393ce 100644 --- a/R-package/man/xgb.plot.tree.Rd +++ b/R-package/man/xgb.plot.tree.Rd @@ -52,14 +52,14 @@ Read a tree model text dump and plot the model. The content of each node is organised that way: \itemize{ - \item Feature name. - \item \code{Cover}: The sum of second order gradient of training data classified to the leaf. - If it is square loss, this simply corresponds to the number of instances seen by a split - or collected by a leaf during training. - The deeper in the tree a node is, the lower this metric will be. - \item \code{Gain} (for split nodes): the information gain metric of a split - (corresponds to the importance of the node in the model). - \item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. +\item Feature name. +\item \code{Cover}: The sum of second order gradient of training data classified to the leaf. +If it is square loss, this simply corresponds to the number of instances seen by a split +or collected by a leaf during training. +The deeper in the tree a node is, the lower this metric will be. +\item \code{Gain} (for split nodes): the information gain metric of a split +(corresponds to the importance of the node in the model). +\item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. } The tree root nodes also indicate the Tree index (0-based). diff --git a/R-package/man/xgb.save.raw.Rd b/R-package/man/xgb.save.raw.Rd index c7c93a734..083551933 100644 --- a/R-package/man/xgb.save.raw.Rd +++ b/R-package/man/xgb.save.raw.Rd @@ -12,9 +12,9 @@ xgb.save.raw(model, raw_format = "deprecated") \item{raw_format}{The format for encoding the booster. Available options are \itemize{ - \item \code{json}: Encode the booster into JSON text document. - \item \code{ubj}: Encode the booster into Universal Binary JSON. - \item \code{deprecated}: Encode the booster into old customized binary format. +\item \code{json}: Encode the booster into JSON text document. +\item \code{ubj}: Encode the booster into Universal Binary JSON. +\item \code{deprecated}: Encode the booster into old customized binary format. } Right now the default is \code{deprecated} but will be changed to \code{ubj} in upcoming release.} diff --git a/R-package/man/xgb.shap.data.Rd b/R-package/man/xgb.shap.data.Rd index 2f0e4adea..6c4336cde 100644 --- a/R-package/man/xgb.shap.data.Rd +++ b/R-package/man/xgb.shap.data.Rd @@ -27,7 +27,7 @@ xgb.shap.data( \item{features}{a vector of either column indices or of feature names to plot. When it is NULL, feature importance is calculated, and \code{top_n} high ranked features are taken.} -\item{top_n}{when \code{features} is NULL, top_n [1, 100] most important features in a model are taken.} +\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} \item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} or \code{features} is missing.} @@ -45,8 +45,8 @@ it is set so that up to 100K data points are used.} } \value{ A list containing: 'data', a matrix containing sample observations - and their feature values; 'shap_contrib', a matrix containing the SHAP contribution - values for these observations. +and their feature values; 'shap_contrib', a matrix containing the SHAP contribution +values for these observations. } \description{ Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. diff --git a/R-package/man/xgb.train.Rd b/R-package/man/xgb.train.Rd index 105009cf8..0ef2e2216 100644 --- a/R-package/man/xgb.train.Rd +++ b/R-package/man/xgb.train.Rd @@ -43,111 +43,114 @@ xgboost( } \arguments{ \item{params}{the list of parameters. The complete list of parameters is - available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below - is a shorter summary: - -1. General Parameters - -\itemize{ - \item \code{booster} which booster to use, can be \code{gbtree} or \code{gblinear}. Default: \code{gbtree}. +available in the \href{http://xgboost.readthedocs.io/en/latest/parameter.html}{online documentation}. Below +is a shorter summary: +\enumerate{ +\item General Parameters } -2. Booster Parameters +\itemize{ +\item \code{booster} which booster to use, can be \code{gbtree} or \code{gblinear}. Default: \code{gbtree}. +} +\enumerate{ +\item Booster Parameters +} 2.1. Parameters for Tree Booster \itemize{ - \item{ \code{eta} control the learning rate: scale the contribution of each tree by a factor of \code{0 < eta < 1} - when it is added to the current approximation. - Used to prevent overfitting by making the boosting process more conservative. - Lower value for \code{eta} implies larger value for \code{nrounds}: low \code{eta} value means model - more robust to overfitting but slower to compute. Default: 0.3} - \item{ \code{gamma} minimum loss reduction required to make a further partition on a leaf node of the tree. - the larger, the more conservative the algorithm will be.} - \item \code{max_depth} maximum depth of a tree. Default: 6 - \item{\code{min_child_weight} minimum sum of instance weight (hessian) needed in a child. - If the tree partition step results in a leaf node with the sum of instance weight less than min_child_weight, - then the building process will give up further partitioning. - In linear regression mode, this simply corresponds to minimum number of instances needed to be in each node. - The larger, the more conservative the algorithm will be. Default: 1} - \item{ \code{subsample} subsample ratio of the training instance. - Setting it to 0.5 means that xgboost randomly collected half of the data instances to grow trees - and this will prevent overfitting. It makes computation shorter (because less data to analyse). - It is advised to use this parameter with \code{eta} and increase \code{nrounds}. Default: 1} - \item \code{colsample_bytree} subsample ratio of columns when constructing each tree. Default: 1 - \item \code{lambda} L2 regularization term on weights. Default: 1 - \item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 - \item{ \code{num_parallel_tree} Experimental parameter. number of trees to grow per round. - Useful to test Random Forest through XGBoost - (set \code{colsample_bytree < 1}, \code{subsample < 1} and \code{round = 1}) accordingly. - Default: 1} - \item{ \code{monotone_constraints} A numerical vector consists of \code{1}, \code{0} and \code{-1} with its length - equals to the number of features in the training data. - \code{1} is increasing, \code{-1} is decreasing and \code{0} is no constraint.} - \item{ \code{interaction_constraints} A list of vectors specifying feature indices of permitted interactions. - Each item of the list represents one permitted interaction where specified features are allowed to interact with each other. - Feature index values should start from \code{0} (\code{0} references the first column). - Leave argument unspecified for no interaction constraints.} +\item{ \code{eta} control the learning rate: scale the contribution of each tree by a factor of \code{0 < eta < 1} +when it is added to the current approximation. +Used to prevent overfitting by making the boosting process more conservative. +Lower value for \code{eta} implies larger value for \code{nrounds}: low \code{eta} value means model +more robust to overfitting but slower to compute. Default: 0.3} +\item{ \code{gamma} minimum loss reduction required to make a further partition on a leaf node of the tree. +the larger, the more conservative the algorithm will be.} +\item \code{max_depth} maximum depth of a tree. Default: 6 +\item{\code{min_child_weight} minimum sum of instance weight (hessian) needed in a child. +If the tree partition step results in a leaf node with the sum of instance weight less than min_child_weight, +then the building process will give up further partitioning. +In linear regression mode, this simply corresponds to minimum number of instances needed to be in each node. +The larger, the more conservative the algorithm will be. Default: 1} +\item{ \code{subsample} subsample ratio of the training instance. +Setting it to 0.5 means that xgboost randomly collected half of the data instances to grow trees +and this will prevent overfitting. It makes computation shorter (because less data to analyse). +It is advised to use this parameter with \code{eta} and increase \code{nrounds}. Default: 1} +\item \code{colsample_bytree} subsample ratio of columns when constructing each tree. Default: 1 +\item \code{lambda} L2 regularization term on weights. Default: 1 +\item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 +\item{ \code{num_parallel_tree} Experimental parameter. number of trees to grow per round. +Useful to test Random Forest through XGBoost +(set \code{colsample_bytree < 1}, \code{subsample < 1} and \code{round = 1}) accordingly. +Default: 1} +\item{ \code{monotone_constraints} A numerical vector consists of \code{1}, \code{0} and \code{-1} with its length +equals to the number of features in the training data. +\code{1} is increasing, \code{-1} is decreasing and \code{0} is no constraint.} +\item{ \code{interaction_constraints} A list of vectors specifying feature indices of permitted interactions. +Each item of the list represents one permitted interaction where specified features are allowed to interact with each other. +Feature index values should start from \code{0} (\code{0} references the first column). +Leave argument unspecified for no interaction constraints.} } 2.2. Parameters for Linear Booster \itemize{ - \item \code{lambda} L2 regularization term on weights. Default: 0 - \item \code{lambda_bias} L2 regularization term on bias. Default: 0 - \item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 +\item \code{lambda} L2 regularization term on weights. Default: 0 +\item \code{lambda_bias} L2 regularization term on bias. Default: 0 +\item \code{alpha} L1 regularization term on weights. (there is no L1 reg on bias because it is not important). Default: 0 +} +\enumerate{ +\item Task Parameters } - -3. Task Parameters \itemize{ \item{ \code{objective} specify the learning task and the corresponding learning objective, users can pass a self-defined function to it. - The default objective options are below: - \itemize{ - \item \code{reg:squarederror} Regression with squared loss (Default). - \item{ \code{reg:squaredlogerror}: regression with squared log loss \eqn{1/2 * (log(pred + 1) - log(label + 1))^2}. - All inputs are required to be greater than -1. - Also, see metric rmsle for possible issue with this objective.} - \item \code{reg:logistic} logistic regression. - \item \code{reg:pseudohubererror}: regression with Pseudo Huber loss, a twice differentiable alternative to absolute loss. - \item \code{binary:logistic} logistic regression for binary classification. Output probability. - \item \code{binary:logitraw} logistic regression for binary classification, output score before logistic transformation. - \item \code{binary:hinge}: hinge loss for binary classification. This makes predictions of 0 or 1, rather than producing probabilities. - \item{ \code{count:poisson}: Poisson regression for count data, output mean of Poisson distribution. - \code{max_delta_step} is set to 0.7 by default in poisson regression (used to safeguard optimization).} - \item{ \code{survival:cox}: Cox regression for right censored survival time data (negative values are considered right censored). - Note that predictions are returned on the hazard ratio scale (i.e., as HR = exp(marginal_prediction) in the proportional - hazard function \code{h(t) = h0(t) * HR)}.} - \item{ \code{survival:aft}: Accelerated failure time model for censored survival time data. See - \href{https://xgboost.readthedocs.io/en/latest/tutorials/aft_survival_analysis.html}{Survival Analysis with Accelerated Failure Time} - for details.} - \item \code{aft_loss_distribution}: Probability Density Function used by \code{survival:aft} and \code{aft-nloglik} metric. - \item{ \code{multi:softmax} set xgboost to do multiclass classification using the softmax objective. - Class is represented by a number and should be from 0 to \code{num_class - 1}.} - \item{ \code{multi:softprob} same as softmax, but prediction outputs a vector of ndata * nclass elements, which can be - further reshaped to ndata, nclass matrix. The result contains predicted probabilities of each data point belonging - to each class.} - \item \code{rank:pairwise} set xgboost to do ranking task by minimizing the pairwise loss. - \item{ \code{rank:ndcg}: Use LambdaMART to perform list-wise ranking where - \href{https://en.wikipedia.org/wiki/Discounted_cumulative_gain}{Normalized Discounted Cumulative Gain (NDCG)} is maximized.} - \item{ \code{rank:map}: Use LambdaMART to perform list-wise ranking where - \href{https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Mean_average_precision}{Mean Average Precision (MAP)} - is maximized.} - \item{ \code{reg:gamma}: gamma regression with log-link. - Output is a mean of gamma distribution. - It might be useful, e.g., for modeling insurance claims severity, or for any outcome that might be - \href{https://en.wikipedia.org/wiki/Gamma_distribution#Applications}{gamma-distributed}.} - \item{ \code{reg:tweedie}: Tweedie regression with log-link. - It might be useful, e.g., for modeling total loss in insurance, or for any outcome that might be - \href{https://en.wikipedia.org/wiki/Tweedie_distribution#Applications}{Tweedie-distributed}.} - } - } - \item \code{base_score} the initial prediction score of all instances, global bias. Default: 0.5 - \item{ \code{eval_metric} evaluation metrics for validation data. - Users can pass a self-defined function to it. - Default: metric will be assigned according to objective - (rmse for regression, and error for classification, mean average precision for ranking). - List is provided in detail section.} +The default objective options are below: +\itemize{ +\item \code{reg:squarederror} Regression with squared loss (Default). +\item{ \code{reg:squaredlogerror}: regression with squared log loss \eqn{1/2 * (log(pred + 1) - log(label + 1))^2}. +All inputs are required to be greater than -1. +Also, see metric rmsle for possible issue with this objective.} +\item \code{reg:logistic} logistic regression. +\item \code{reg:pseudohubererror}: regression with Pseudo Huber loss, a twice differentiable alternative to absolute loss. +\item \code{binary:logistic} logistic regression for binary classification. Output probability. +\item \code{binary:logitraw} logistic regression for binary classification, output score before logistic transformation. +\item \code{binary:hinge}: hinge loss for binary classification. This makes predictions of 0 or 1, rather than producing probabilities. +\item{ \code{count:poisson}: Poisson regression for count data, output mean of Poisson distribution. +\code{max_delta_step} is set to 0.7 by default in poisson regression (used to safeguard optimization).} +\item{ \code{survival:cox}: Cox regression for right censored survival time data (negative values are considered right censored). +Note that predictions are returned on the hazard ratio scale (i.e., as HR = exp(marginal_prediction) in the proportional +hazard function \code{h(t) = h0(t) * HR)}.} +\item{ \code{survival:aft}: Accelerated failure time model for censored survival time data. See +\href{https://xgboost.readthedocs.io/en/latest/tutorials/aft_survival_analysis.html}{Survival Analysis with Accelerated Failure Time} +for details.} +\item \code{aft_loss_distribution}: Probability Density Function used by \code{survival:aft} and \code{aft-nloglik} metric. +\item{ \code{multi:softmax} set xgboost to do multiclass classification using the softmax objective. +Class is represented by a number and should be from 0 to \code{num_class - 1}.} +\item{ \code{multi:softprob} same as softmax, but prediction outputs a vector of ndata * nclass elements, which can be +further reshaped to ndata, nclass matrix. The result contains predicted probabilities of each data point belonging +to each class.} +\item \code{rank:pairwise} set xgboost to do ranking task by minimizing the pairwise loss. +\item{ \code{rank:ndcg}: Use LambdaMART to perform list-wise ranking where +\href{https://en.wikipedia.org/wiki/Discounted_cumulative_gain}{Normalized Discounted Cumulative Gain (NDCG)} is maximized.} +\item{ \code{rank:map}: Use LambdaMART to perform list-wise ranking where +\href{https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Mean_average_precision}{Mean Average Precision (MAP)} +is maximized.} +\item{ \code{reg:gamma}: gamma regression with log-link. +Output is a mean of gamma distribution. +It might be useful, e.g., for modeling insurance claims severity, or for any outcome that might be +\href{https://en.wikipedia.org/wiki/Gamma_distribution#Applications}{gamma-distributed}.} +\item{ \code{reg:tweedie}: Tweedie regression with log-link. +It might be useful, e.g., for modeling total loss in insurance, or for any outcome that might be +\href{https://en.wikipedia.org/wiki/Tweedie_distribution#Applications}{Tweedie-distributed}.} +} +} +\item \code{base_score} the initial prediction score of all instances, global bias. Default: 0.5 +\item{ \code{eval_metric} evaluation metrics for validation data. +Users can pass a self-defined function to it. +Default: metric will be assigned according to objective +(rmse for regression, and error for classification, mean average precision for ranking). +List is provided in detail section.} }} \item{data}{training dataset. \code{xgb.train} accepts only an \code{xgb.DMatrix} as the input. @@ -218,24 +221,24 @@ This parameter is only used when input is a dense matrix.} \value{ An object of class \code{xgb.Booster} with the following elements: \itemize{ - \item \code{handle} a handle (pointer) to the xgboost model in memory. - \item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. - \item \code{niter} number of boosting iterations. - \item \code{evaluation_log} evaluation history stored as a \code{data.table} with the - first column corresponding to iteration number and the rest corresponding to evaluation - metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. - \item \code{call} a function call. - \item \code{params} parameters that were passed to the xgboost library. Note that it does not - capture parameters changed by the \code{\link{cb.reset.parameters}} callback. - \item \code{callbacks} callback functions that were either automatically assigned or - explicitly passed. - \item \code{best_iteration} iteration number with the best evaluation metric value - (only available with early stopping). - \item \code{best_score} the best evaluation metric value during early stopping. - (only available with early stopping). - \item \code{feature_names} names of the training dataset features - (only when column names were defined in training data). - \item \code{nfeatures} number of features in training data. +\item \code{handle} a handle (pointer) to the xgboost model in memory. +\item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. +\item \code{niter} number of boosting iterations. +\item \code{evaluation_log} evaluation history stored as a \code{data.table} with the +first column corresponding to iteration number and the rest corresponding to evaluation +metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. +\item \code{call} a function call. +\item \code{params} parameters that were passed to the xgboost library. Note that it does not +capture parameters changed by the \code{\link{cb.reset.parameters}} callback. +\item \code{callbacks} callback functions that were either automatically assigned or +explicitly passed. +\item \code{best_iteration} iteration number with the best evaluation metric value +(only available with early stopping). +\item \code{best_score} the best evaluation metric value during early stopping. +(only available with early stopping). +\item \code{feature_names} names of the training dataset features +(only when column names were defined in training data). +\item \code{nfeatures} number of features in training data. } } \description{ @@ -258,29 +261,29 @@ when the \code{eval_metric} parameter is not provided. User may set one or several \code{eval_metric} parameters. Note that when using a customized metric, only this single metric can be used. The following is the list of built-in metrics for which XGBoost provides optimized implementation: - \itemize{ - \item \code{rmse} root mean square error. \url{https://en.wikipedia.org/wiki/Root_mean_square_error} - \item \code{logloss} negative log-likelihood. \url{https://en.wikipedia.org/wiki/Log-likelihood} - \item \code{mlogloss} multiclass logloss. \url{https://scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html} - \item \code{error} Binary classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. - By default, it uses the 0.5 threshold for predicted values to define negative and positive instances. - Different threshold (e.g., 0.) could be specified as "error@0." - \item \code{merror} Multiclass classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. - \item \code{mae} Mean absolute error - \item \code{mape} Mean absolute percentage error - \item{ \code{auc} Area under the curve. - \url{https://en.wikipedia.org/wiki/Receiver_operating_characteristic#'Area_under_curve} for ranking evaluation.} - \item \code{aucpr} Area under the PR curve. \url{https://en.wikipedia.org/wiki/Precision_and_recall} for ranking evaluation. - \item \code{ndcg} Normalized Discounted Cumulative Gain (for ranking task). \url{https://en.wikipedia.org/wiki/NDCG} - } +\itemize{ +\item \code{rmse} root mean square error. \url{https://en.wikipedia.org/wiki/Root_mean_square_error} +\item \code{logloss} negative log-likelihood. \url{https://en.wikipedia.org/wiki/Log-likelihood} +\item \code{mlogloss} multiclass logloss. \url{https://scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html} +\item \code{error} Binary classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. +By default, it uses the 0.5 threshold for predicted values to define negative and positive instances. +Different threshold (e.g., 0.) could be specified as "error@0." +\item \code{merror} Multiclass classification error rate. It is calculated as \code{(# wrong cases) / (# all cases)}. +\item \code{mae} Mean absolute error +\item \code{mape} Mean absolute percentage error +\item{ \code{auc} Area under the curve. +\url{https://en.wikipedia.org/wiki/Receiver_operating_characteristic#'Area_under_curve} for ranking evaluation.} +\item \code{aucpr} Area under the PR curve. \url{https://en.wikipedia.org/wiki/Precision_and_recall} for ranking evaluation. +\item \code{ndcg} Normalized Discounted Cumulative Gain (for ranking task). \url{https://en.wikipedia.org/wiki/NDCG} +} The following callbacks are automatically created when certain parameters are set: \itemize{ - \item \code{cb.print.evaluation} is turned on when \code{verbose > 0}; - and the \code{print_every_n} parameter is passed to it. - \item \code{cb.evaluation.log} is on when \code{watchlist} is present. - \item \code{cb.early.stop}: when \code{early_stopping_rounds} is set. - \item \code{cb.save.model}: when \code{save_period > 0} is set. +\item \code{cb.print.evaluation} is turned on when \code{verbose > 0}; +and the \code{print_every_n} parameter is passed to it. +\item \code{cb.evaluation.log} is on when \code{watchlist} is present. +\item \code{cb.early.stop}: when \code{early_stopping_rounds} is set. +\item \code{cb.save.model}: when \code{save_period > 0} is set. } } \examples{ diff --git a/R-package/man/xgb.unserialize.Rd b/R-package/man/xgb.unserialize.Rd index d191d77d4..f83ee635d 100644 --- a/R-package/man/xgb.unserialize.Rd +++ b/R-package/man/xgb.unserialize.Rd @@ -11,7 +11,7 @@ xgb.unserialize(buffer, handle = NULL) \item{handle}{An \code{xgb.Booster.handle} object which will be overwritten with the new deserialized object. Must be a null handle (e.g. when loading the model through -`readRDS`). If not provided, a new handle will be created.} +\code{readRDS}). If not provided, a new handle will be created.} } \value{ An \code{xgb.Booster.handle} object. From 6a5f6ba694c980a884ac82407b1098a80cb0c6a0 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Sun, 24 Dec 2023 00:09:05 +0800 Subject: [PATCH 062/109] [CI] Add timeout for distributed GPU tests. (#9917) --- .../test_gpu_with_dask/test_gpu_demos.py | 10 ++++++---- .../test_gpu_with_dask/test_gpu_with_dask.py | 4 +++- .../test_distributed/test_gpu_with_spark/test_data.py | 5 ++++- .../test_gpu_with_spark/test_gpu_spark.py | 5 ++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py b/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py index 92539aaa1..c0ae8b849 100644 --- a/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py +++ b/tests/test_distributed/test_gpu_with_dask/test_gpu_demos.py @@ -5,9 +5,13 @@ import pytest from xgboost import testing as tm +pytestmark = [ + pytest.mark.skipif(**tm.no_dask()), + pytest.mark.skipif(**tm.no_dask_cuda()), + tm.timeout(60), +] + -@pytest.mark.skipif(**tm.no_dask()) -@pytest.mark.skipif(**tm.no_dask_cuda()) @pytest.mark.skipif(**tm.no_cupy()) @pytest.mark.mgpu def test_dask_training(): @@ -16,8 +20,6 @@ def test_dask_training(): subprocess.check_call(cmd) -@pytest.mark.skipif(**tm.no_dask_cuda()) -@pytest.mark.skipif(**tm.no_dask()) @pytest.mark.mgpu def test_dask_sklearn_demo(): script = os.path.join(tm.demo_dir(__file__), "dask", "sklearn_gpu_training.py") diff --git a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py index f25ac9fb0..a15e1903d 100644 --- a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py +++ b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py @@ -1,4 +1,4 @@ -"""Copyright 2019-2022 XGBoost contributors""" +"""Copyright 2019-2023, XGBoost contributors""" import asyncio import json from collections import OrderedDict @@ -18,6 +18,7 @@ from xgboost.testing.params import hist_parameter_strategy pytestmark = [ pytest.mark.skipif(**tm.no_dask()), pytest.mark.skipif(**tm.no_dask_cuda()), + tm.timeout(60), ] from ..test_with_dask.test_with_dask import generate_array @@ -629,6 +630,7 @@ def test_nccl_load(local_cuda_client: Client, tree_method: str) -> None: def run(wid: int) -> None: # FIXME(jiamingy): https://github.com/dmlc/xgboost/issues/9147 from xgboost.core import _LIB, _register_log_callback + _register_log_callback(_LIB) with CommunicatorContext(**args): diff --git a/tests/test_distributed/test_gpu_with_spark/test_data.py b/tests/test_distributed/test_gpu_with_spark/test_data.py index 72d9c190f..c2e068a87 100644 --- a/tests/test_distributed/test_gpu_with_spark/test_data.py +++ b/tests/test_distributed/test_gpu_with_spark/test_data.py @@ -2,7 +2,10 @@ import pytest from xgboost import testing as tm -pytestmark = pytest.mark.skipif(**tm.no_spark()) +pytestmark = [ + pytest.mark.skipif(**tm.no_spark()), + tm.timeout(120), +] from ..test_with_spark.test_data import run_dmatrix_ctor diff --git a/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py b/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py index 3bf94c954..f389d6d26 100644 --- a/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py +++ b/tests/test_distributed/test_gpu_with_spark/test_gpu_spark.py @@ -8,7 +8,10 @@ import sklearn from xgboost import testing as tm -pytestmark = pytest.mark.skipif(**tm.no_spark()) +pytestmark = [ + pytest.mark.skipif(**tm.no_spark()), + tm.timeout(240), +] from pyspark.ml.linalg import Vectors from pyspark.ml.tuning import CrossValidator, ParamGridBuilder From 52620fdb341c91cc7edcd882a40b09305947dc7b Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 26 Dec 2023 10:30:13 +0100 Subject: [PATCH 063/109] [R] Improve more docstrings (#9919) --- R-package/R/xgb.ggplot.R | 31 ++-- R-package/R/xgb.importance.R | 142 +++++++++------ R-package/R/xgb.model.dt.tree.R | 89 +++++----- R-package/R/xgb.plot.deepness.R | 83 +++++---- R-package/R/xgb.plot.importance.R | 81 +++++---- R-package/R/xgb.plot.multi.trees.R | 51 +++--- R-package/R/xgb.plot.shap.R | 236 ++++++++++++++++--------- R-package/R/xgb.plot.tree.R | 89 +++++----- R-package/man/xgb.importance.Rd | 134 +++++++++----- R-package/man/xgb.model.dt.tree.Rd | 83 +++++---- R-package/man/xgb.plot.deepness.Rd | 78 ++++---- R-package/man/xgb.plot.importance.Rd | 78 ++++---- R-package/man/xgb.plot.multi.trees.Rd | 58 +++--- R-package/man/xgb.plot.shap.Rd | 193 +++++++++++++------- R-package/man/xgb.plot.shap.summary.Rd | 55 +++--- R-package/man/xgb.plot.tree.Rd | 87 +++++---- R-package/man/xgb.shap.data.Rd | 55 ------ 17 files changed, 931 insertions(+), 692 deletions(-) delete mode 100644 R-package/man/xgb.shap.data.Rd diff --git a/R-package/R/xgb.ggplot.R b/R-package/R/xgb.ggplot.R index e79644543..1fe30ba2b 100644 --- a/R-package/R/xgb.ggplot.R +++ b/R-package/R/xgb.ggplot.R @@ -127,22 +127,20 @@ xgb.ggplot.shap.summary <- function(data, shap_contrib = NULL, features = NULL, p } -#' Combine and melt feature values and SHAP contributions for sample -#' observations. +#' Combine feature values and SHAP values #' -#' Conforms to data format required for ggplot functions. +#' Internal function used to combine and melt feature values and SHAP contributions +#' as required for ggplot functions related to SHAP. #' -#' Internal utility function. +#' @param data_list The result of `xgb.shap.data()`. +#' @param normalize Whether to standardize feature values to mean 0 and +#' standard deviation 1. This is useful for comparing multiple features on the same +#' plot. Default is \code{FALSE}. #' -#' @param data_list List containing 'data' and 'shap_contrib' returned by -#' \code{xgb.shap.data()}. -#' @param normalize Whether to standardize feature values to have mean 0 and -#' standard deviation 1 (useful for comparing multiple features on the same -#' plot). Default \code{FALSE}. -#' -#' @return A data.table containing the observation ID, the feature name, the +#' @return A `data.table` containing the observation ID, the feature name, the #' feature value (normalized if specified), and the SHAP contribution value. #' @noRd +#' @keywords internal prepare.ggplot.shap.data <- function(data_list, normalize = FALSE) { data <- data_list[["data"]] shap_contrib <- data_list[["shap_contrib"]] @@ -163,15 +161,16 @@ prepare.ggplot.shap.data <- function(data_list, normalize = FALSE) { p_data } -#' Scale feature value to have mean 0, standard deviation 1 +#' Scale feature values #' -#' This is used to compare multiple features on the same plot. -#' Internal utility function +#' Internal function that scales feature values to mean 0 and standard deviation 1. +#' Useful to compare multiple features on the same plot. #' -#' @param x Numeric vector +#' @param x Numeric vector. #' -#' @return Numeric vector with mean 0 and sd 1. +#' @return Numeric vector with mean 0 and standard deviation 1. #' @noRd +#' @keywords internal normalize <- function(x) { loc <- mean(x, na.rm = TRUE) scale <- stats::sd(x, na.rm = TRUE) diff --git a/R-package/R/xgb.importance.R b/R-package/R/xgb.importance.R index 844e36cdf..c94e1babb 100644 --- a/R-package/R/xgb.importance.R +++ b/R-package/R/xgb.importance.R @@ -1,83 +1,115 @@ -#' Importance of features in a model. +#' Feature importance #' -#' Creates a \code{data.table} of feature importances in a model. +#' Creates a `data.table` of feature importances. #' -#' @param feature_names character vector of feature names. If the model already -#' contains feature names, those would be used when \code{feature_names=NULL} (default value). -#' Non-null \code{feature_names} could be provided to override those in the model. -#' @param model object of class \code{xgb.Booster}. -#' @param trees (only for the gbtree booster) an integer vector of tree indices that should be included -#' into the importance calculation. If set to \code{NULL}, all trees of the model are parsed. -#' It could be useful, e.g., in multiclass classification to get feature importances -#' for each class separately. IMPORTANT: the tree index in xgboost models -#' is zero-based (e.g., use \code{trees = 0:4} for first 5 trees). -#' @param data deprecated. -#' @param label deprecated. -#' @param target deprecated. +#' @param feature_names Character vector used to overwrite the feature names +#' of the model. The default is `NULL` (use original feature names). +#' @param model Object of class `xgb.Booster`. +#' @param trees An integer vector of tree indices that should be included +#' into the importance calculation (only for the "gbtree" booster). +#' The default (`NULL`) parses all trees. +#' It could be useful, e.g., in multiclass classification to get feature importances +#' for each class separately. *Important*: the tree index in XGBoost models +#' is zero-based (e.g., use `trees = 0:4` for the first five trees). +#' @param data Deprecated. +#' @param label Deprecated. +#' @param target Deprecated. #' #' @details #' #' This function works for both linear and tree models. #' #' For linear models, the importance is the absolute magnitude of linear coefficients. -#' For that reason, in order to obtain a meaningful ranking by importance for a linear model, -#' the features need to be on the same scale (which you also would want to do when using either -#' L1 or L2 regularization). +#' To obtain a meaningful ranking by importance for linear models, the features need to +#' be on the same scale (which is also recommended when using L1 or L2 regularization). #' -#' @return +#' @return A `data.table` with the following columns: #' -#' For a tree model, a \code{data.table} with the following columns: -#' \itemize{ -#' \item \code{Features} names of the features used in the model; -#' \item \code{Gain} represents fractional contribution of each feature to the model based on -#' the total gain of this feature's splits. Higher percentage means a more important -#' predictive feature. -#' \item \code{Cover} metric of the number of observation related to this feature; -#' \item \code{Frequency} percentage representing the relative number of times -#' a feature have been used in trees. -#' } +#' For a tree model: +#' - `Features`: Names of the features used in the model. +#' - `Gain`: Fractional contribution of each feature to the model based on +#' the total gain of this feature's splits. Higher percentage means higher importance. +#' - `Cover`: Metric of the number of observation related to this feature. +#' - `Frequency`: Percentage of times a feature has been used in trees. #' -#' A linear model's importance \code{data.table} has the following columns: -#' \itemize{ -#' \item \code{Features} names of the features used in the model; -#' \item \code{Weight} the linear coefficient of this feature; -#' \item \code{Class} (only for multiclass models) class label. -#' } +#' For a linear model: +#' - `Features`: Names of the features used in the model. +#' - `Weight`: Linear coefficient of this feature. +#' - `Class`: Class label (only for multiclass models). #' -#' If \code{feature_names} is not provided and \code{model} doesn't have \code{feature_names}, -#' index of the features will be used instead. Because the index is extracted from the model dump +#' If `feature_names` is not provided and `model` doesn't have `feature_names`, +#' the index of the features will be used instead. Because the index is extracted from the model dump #' (based on C++ code), it starts at 0 (as in C/C++ or Python) instead of 1 (usual in R). #' #' @examples #' -#' # binomial classification using gbtree: -#' data(agaricus.train, package='xgboost') -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' # binomial classification using "gbtree": +#' data(agaricus.train, package = "xgboost") +#' +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) +#' #' xgb.importance(model = bst) #' -#' # binomial classification using gblinear: -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, booster = "gblinear", -#' eta = 0.3, nthread = 1, nrounds = 20, objective = "binary:logistic") +#' # binomial classification using "gblinear": +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' booster = "gblinear", +#' eta = 0.3, +#' nthread = 1, +#' nrounds = 20,objective = "binary:logistic" +#' ) +#' #' xgb.importance(model = bst) #' -#' # multiclass classification using gbtree: +#' # multiclass classification using "gbtree": #' nclass <- 3 #' nrounds <- 10 -#' mbst <- xgboost(data = as.matrix(iris[, -5]), label = as.numeric(iris$Species) - 1, -#' max_depth = 3, eta = 0.2, nthread = 2, nrounds = nrounds, -#' objective = "multi:softprob", num_class = nclass) +#' mbst <- xgboost( +#' data = as.matrix(iris[, -5]), +#' label = as.numeric(iris$Species) - 1, +#' max_depth = 3, +#' eta = 0.2, +#' nthread = 2, +#' nrounds = nrounds, +#' objective = "multi:softprob", +#' num_class = nclass +#' ) +#' #' # all classes clumped together: #' xgb.importance(model = mbst) -#' # inspect importances separately for each class: -#' xgb.importance(model = mbst, trees = seq(from=0, by=nclass, length.out=nrounds)) -#' xgb.importance(model = mbst, trees = seq(from=1, by=nclass, length.out=nrounds)) -#' xgb.importance(model = mbst, trees = seq(from=2, by=nclass, length.out=nrounds)) #' -#' # multiclass classification using gblinear: -#' mbst <- xgboost(data = scale(as.matrix(iris[, -5])), label = as.numeric(iris$Species) - 1, -#' booster = "gblinear", eta = 0.2, nthread = 1, nrounds = 15, -#' objective = "multi:softprob", num_class = nclass) +#' # inspect importances separately for each class: +#' xgb.importance( +#' model = mbst, trees = seq(from = 0, by = nclass, length.out = nrounds) +#' ) +#' xgb.importance( +#' model = mbst, trees = seq(from = 1, by = nclass, length.out = nrounds) +#' ) +#' xgb.importance( +#' model = mbst, trees = seq(from = 2, by = nclass, length.out = nrounds) +#' ) +#' +#' # multiclass classification using "gblinear": +#' mbst <- xgboost( +#' data = scale(as.matrix(iris[, -5])), +#' label = as.numeric(iris$Species) - 1, +#' booster = "gblinear", +#' eta = 0.2, +#' nthread = 1, +#' nrounds = 15, +#' objective = "multi:softprob", +#' num_class = nclass +#' ) +#' #' xgb.importance(model = mbst) #' #' @export diff --git a/R-package/R/xgb.model.dt.tree.R b/R-package/R/xgb.model.dt.tree.R index d69169b89..8e74ea4b4 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -1,57 +1,58 @@ -#' Parse a boosted tree model text dump +#' Parse model text dump #' -#' Parse a boosted tree model text dump into a \code{data.table} structure. +#' Parse a boosted tree model text dump into a `data.table` structure. #' -#' @param feature_names character vector of feature names. If the model already -#' contains feature names, those would be used when \code{feature_names=NULL} (default value). -#' Non-null \code{feature_names} could be provided to override those in the model. -#' @param model object of class \code{xgb.Booster} -#' @param text \code{character} vector previously generated by the \code{xgb.dump} -#' function (where parameter \code{with_stats = TRUE} should have been set). -#' \code{text} takes precedence over \code{model}. -#' @param trees an integer vector of tree indices that should be parsed. -#' If set to \code{NULL}, all trees of the model are parsed. -#' It could be useful, e.g., in multiclass classification to get only -#' the trees of one certain class. IMPORTANT: the tree index in xgboost models -#' is zero-based (e.g., use \code{trees = 0:4} for first 5 trees). -#' @param use_int_id a logical flag indicating whether nodes in columns "Yes", "No", "Missing" should be -#' represented as integers (when FALSE) or as "Tree-Node" character strings (when FALSE). -#' @param ... currently not used. +#' @param feature_names Character vector used to overwrite the feature names +#' of the model. The default (`NULL`) uses the original feature names. +#' @param model Object of class `xgb.Booster`. +#' @param text Character vector previously generated by the function [xgb.dump()] +#' (called with parameter `with_stats = TRUE`). `text` takes precedence over `model`. +#' @param trees An integer vector of tree indices that should be used. +#' The default (`NULL`) uses all trees. +#' Useful, e.g., in multiclass classification to get only +#' the trees of one class. *Important*: the tree index in XGBoost models +#' is zero-based (e.g., use `trees = 0:4` for the first five trees). +#' @param use_int_id A logical flag indicating whether nodes in columns "Yes", "No", and +#' "Missing" should be represented as integers (when `TRUE`) or as "Tree-Node" +#' character strings (when `FALSE`, default). +#' @param ... Currently not used. #' #' @return -#' A \code{data.table} with detailed information about model trees' nodes. +#' A `data.table` with detailed information about tree nodes. It has the following columns: +#' - `Tree`: integer ID of a tree in a model (zero-based index). +#' - `Node`: integer ID of a node in a tree (zero-based index). +#' - `ID`: character identifier of a node in a model (only when `use_int_id = FALSE`). +#' - `Feature`: for a branch node, a feature ID or name (when available); +#' for a leaf node, it simply labels it as `"Leaf"`. +#' - `Split`: location of the split for a branch node (split condition is always "less than"). +#' - `Yes`: ID of the next node when the split condition is met. +#' - `No`: ID of the next node when the split condition is not met. +#' - `Missing`: ID of the next node when the branch value is missing. +#' - `Quality`: either the split gain (change in loss) or the leaf value. +#' - `Cover`: metric related to the number of observations either seen by a split +#' or collected by a leaf during training. #' -#' The columns of the \code{data.table} are: -#' -#' \itemize{ -#' \item \code{Tree}: integer ID of a tree in a model (zero-based index) -#' \item \code{Node}: integer ID of a node in a tree (zero-based index) -#' \item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) -#' \item \code{Feature}: for a branch node, it's a feature id or name (when available); -#' for a leaf note, it simply labels it as \code{'Leaf'} -#' \item \code{Split}: location of the split for a branch node (split condition is always "less than") -#' \item \code{Yes}: ID of the next node when the split condition is met -#' \item \code{No}: ID of the next node when the split condition is not met -#' \item \code{Missing}: ID of the next node when branch value is missing -#' \item \code{Quality}: either the split gain (change in loss) or the leaf value -#' \item \code{Cover}: metric related to the number of observation either seen by a split -#' or collected by a leaf during training. -#' } -#' -#' When \code{use_int_id=FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers -#' in the "ID" column. When \code{use_int_id=TRUE}, those columns point to node identifiers from +#' When `use_int_id = FALSE`, columns "Yes", "No", and "Missing" point to model-wide node identifiers +#' in the "ID" column. When `use_int_id = TRUE`, those columns point to node identifiers from #' the corresponding trees in the "Node" column. #' #' @examples #' # Basic use: #' -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' ## Keep the number of threads to 1 for examples #' nthread <- 1 #' data.table::setDTthreads(nthread) #' -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 2, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' #' (dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) #' @@ -60,8 +61,12 @@ #' (dt <- xgb.model.dt.tree(model = bst)) #' #' # How to match feature names of splits that are following a current 'Yes' branch: -#' -#' merge(dt, dt[, .(ID, Y.Feature=Feature)], by.x='Yes', by.y='ID', all.x=TRUE)[order(Tree,Node)] +#' merge( +#' dt, +#' dt[, .(ID, Y.Feature = Feature)], by.x = "Yes", by.y = "ID", all.x = TRUE +#' )[ +#' order(Tree, Node) +#' ] #' #' @export xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, diff --git a/R-package/R/xgb.plot.deepness.R b/R-package/R/xgb.plot.deepness.R index f0fe0f134..092b07d38 100644 --- a/R-package/R/xgb.plot.deepness.R +++ b/R-package/R/xgb.plot.deepness.R @@ -1,65 +1,74 @@ -#' Plot model trees deepness +#' Plot model tree depth #' -#' Visualizes distributions related to depth of tree leafs. -#' \code{xgb.plot.deepness} uses base R graphics, while \code{xgb.ggplot.deepness} uses the ggplot backend. +#' Visualizes distributions related to the depth of tree leaves. +#' - `xgb.plot.deepness()` uses base R graphics, while +#' - `xgb.ggplot.deepness()` uses "ggplot2". #' -#' @param model either an \code{xgb.Booster} model generated by the \code{xgb.train} function -#' or a data.table result of the \code{xgb.model.dt.tree} function. -#' @param plot (base R barplot) whether a barplot should be produced. -#' If FALSE, only a data.table is returned. -#' @param which which distribution to plot (see details). -#' @param ... other parameters passed to \code{barplot} or \code{plot}. +#' @param model Either an `xgb.Booster` model, or the "data.table" returned by [xgb.model.dt.tree()]. +#' @param which Which distribution to plot (see details). +#' @param plot Should the plot be shown? Default is `TRUE`. +#' @param ... Other parameters passed to [graphics::barplot()] or [graphics::plot()]. #' #' @details #' -#' When \code{which="2x1"}, two distributions with respect to the leaf depth +#' When `which = "2x1"`, two distributions with respect to the leaf depth #' are plotted on top of each other: -#' \itemize{ -#' \item the distribution of the number of leafs in a tree model at a certain depth; -#' \item the distribution of average weighted number of observations ("cover") -#' ending up in leafs at certain depth. -#' } -#' Those could be helpful in determining sensible ranges of the \code{max_depth} -#' and \code{min_child_weight} parameters. +#' 1. The distribution of the number of leaves in a tree model at a certain depth. +#' 2. The distribution of the average weighted number of observations ("cover") +#' ending up in leaves at a certain depth. #' -#' When \code{which="max.depth"} or \code{which="med.depth"}, plots of either maximum or median depth -#' per tree with respect to tree number are created. And \code{which="med.weight"} allows to see how +#' Those could be helpful in determining sensible ranges of the `max_depth` +#' and `min_child_weight` parameters. +#' +#' When `which = "max.depth"` or `which = "med.depth"`, plots of either maximum or +#' median depth per tree with respect to the tree number are created. +#' +#' Finally, `which = "med.weight"` allows to see how #' a tree's median absolute leaf weight changes through the iterations. #' -#' This function was inspired by the blog post -#' \url{https://github.com/aysent/random-forest-leaf-visualization}. +#' These functions have been inspired by the blog post +#' . #' #' @return +#' The return value of the two functions is as follows: +#' - `xgb.plot.deepness()`: A "data.table" (invisibly). +#' Each row corresponds to a terminal leaf in the model. It contains its information +#' about depth, cover, and weight (used in calculating predictions). +#' If `plot = TRUE`, also a plot is shown. +#' - `xgb.ggplot.deepness()`: When `which = "2x1"`, a list of two "ggplot" objects, +#' and a single "ggplot" object otherwise. #' -#' Other than producing plots (when \code{plot=TRUE}), the \code{xgb.plot.deepness} function -#' silently returns a processed data.table where each row corresponds to a terminal leaf in a tree model, -#' and contains information about leaf's depth, cover, and weight (which is used in calculating predictions). -#' -#' The \code{xgb.ggplot.deepness} silently returns either a list of two ggplot graphs when \code{which="2x1"} -#' or a single ggplot graph for the other \code{which} options. -#' -#' @seealso -#' -#' \code{\link{xgb.train}}, \code{\link{xgb.model.dt.tree}}. +#' @seealso [xgb.train()] and [xgb.model.dt.tree()]. #' #' @examples #' -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") #' ## Keep the number of threads to 2 for examples #' nthread <- 2 #' data.table::setDTthreads(nthread) #' #' ## Change max_depth to a higher number to get a more significant result -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 6, -#' eta = 0.1, nthread = nthread, nrounds = 50, objective = "binary:logistic", -#' subsample = 0.5, min_child_weight = 2) +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 6, +#' nthread = nthread, +#' nrounds = 50, +#' objective = "binary:logistic", +#' subsample = 0.5, +#' min_child_weight = 2 +#' ) #' #' xgb.plot.deepness(bst) #' xgb.ggplot.deepness(bst) #' -#' xgb.plot.deepness(bst, which='max.depth', pch=16, col=rgb(0,0,1,0.3), cex=2) +#' xgb.plot.deepness( +#' bst, which = "max.depth", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +#' ) #' -#' xgb.plot.deepness(bst, which='med.weight', pch=16, col=rgb(0,0,1,0.3), cex=2) +#' xgb.plot.deepness( +#' bst, which = "med.weight", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +#' ) #' #' @rdname xgb.plot.deepness #' @export diff --git a/R-package/R/xgb.plot.importance.R b/R-package/R/xgb.plot.importance.R index 07220375d..1848a3a86 100644 --- a/R-package/R/xgb.plot.importance.R +++ b/R-package/R/xgb.plot.importance.R @@ -1,64 +1,75 @@ -#' Plot feature importance as a bar graph +#' Plot feature importance #' #' Represents previously calculated feature importance as a bar graph. -#' \code{xgb.plot.importance} uses base R graphics, while \code{xgb.ggplot.importance} uses the ggplot backend. +#' - `xgb.plot.importance()` uses base R graphics, while +#' - `xgb.ggplot.importance()` uses "ggplot". #' -#' @param importance_matrix a \code{data.table} returned by \code{\link{xgb.importance}}. -#' @param top_n maximal number of top features to include into the plot. -#' @param measure the name of importance measure to plot. -#' When \code{NULL}, 'Gain' would be used for trees and 'Weight' would be used for gblinear. -#' @param rel_to_first whether importance values should be represented as relative to the highest ranked feature. -#' See Details. -#' @param left_margin (base R barplot) allows to adjust the left margin size to fit feature names. -#' When it is NULL, the existing \code{par('mar')} is used. -#' @param cex (base R barplot) passed as \code{cex.names} parameter to \code{barplot}. -#' @param plot (base R barplot) whether a barplot should be produced. -#' If FALSE, only a data.table is returned. -#' @param n_clusters (ggplot only) a \code{numeric} vector containing the min and the max range +#' @param importance_matrix A `data.table` as returned by [xgb.importance()]. +#' @param top_n Maximal number of top features to include into the plot. +#' @param measure The name of importance measure to plot. +#' When `NULL`, 'Gain' would be used for trees and 'Weight' would be used for gblinear. +#' @param rel_to_first Whether importance values should be represented as relative to +#' the highest ranked feature, see Details. +#' @param left_margin Adjust the left margin size to fit feature names. +#' When `NULL`, the existing `par("mar")` is used. +#' @param cex Passed as `cex.names` parameter to [graphics::barplot()]. +#' @param plot Should the barplot be shown? Default is `TRUE`. +#' @param n_clusters A numeric vector containing the min and the max range #' of the possible number of clusters of bars. -#' @param ... other parameters passed to \code{barplot} (except horiz, border, cex.names, names.arg, and las). +#' @param ... Other parameters passed to [graphics::barplot()] +#' (except `horiz`, `border`, `cex.names`, `names.arg`, and `las`). +#' Only used in `xgb.plot.importance()`. #' #' @details #' The graph represents each feature as a horizontal bar of length proportional to the importance of a feature. -#' Features are shown ranked in a decreasing importance order. -#' It works for importances from both \code{gblinear} and \code{gbtree} models. +#' Features are sorted by decreasing importance. +#' It works for both "gblinear" and "gbtree" models. #' -#' When \code{rel_to_first = FALSE}, the values would be plotted as they were in \code{importance_matrix}. -#' For gbtree model, that would mean being normalized to the total of 1 +#' When `rel_to_first = FALSE`, the values would be plotted as in `importance_matrix`. +#' For a "gbtree" model, that would mean being normalized to the total of 1 #' ("what is feature's importance contribution relative to the whole model?"). -#' For linear models, \code{rel_to_first = FALSE} would show actual values of the coefficients. -#' Setting \code{rel_to_first = TRUE} allows to see the picture from the perspective of +#' For linear models, `rel_to_first = FALSE` would show actual values of the coefficients. +#' Setting `rel_to_first = TRUE` allows to see the picture from the perspective of #' "what is feature's importance contribution relative to the most important feature?" #' -#' The ggplot-backend method also performs 1-D clustering of the importance values, -#' with bar colors corresponding to different clusters that have somewhat similar importance values. +#' The "ggplot" backend performs 1-D clustering of the importance values, +#' with bar colors corresponding to different clusters having similar importance values. #' #' @return -#' The \code{xgb.plot.importance} function creates a \code{barplot} (when \code{plot=TRUE}) -#' and silently returns a processed data.table with \code{n_top} features sorted by importance. +#' The return value depends on the function: +#' - `xgb.plot.importance()`: Invisibly, a "data.table" with `n_top` features sorted +#' by importance. If `plot = TRUE`, the values are also plotted as barplot. +#' - `xgb.ggplot.importance()`: A customizable "ggplot" object. +#' E.g., to change the title, set `+ ggtitle("A GRAPH NAME")`. #' -#' The \code{xgb.ggplot.importance} function returns a ggplot graph which could be customized afterwards. -#' E.g., to change the title of the graph, add \code{+ ggtitle("A GRAPH NAME")} to the result. -#' -#' @seealso -#' \code{\link[graphics]{barplot}}. +#' @seealso [graphics::barplot()] #' #' @examples #' data(agaricus.train) +#' #' ## Keep the number of threads to 2 for examples #' nthread <- 2 #' data.table::setDTthreads(nthread) #' #' bst <- xgboost( -#' data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, -#' eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 3, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 2, +#' objective = "binary:logistic" #' ) #' #' importance_matrix <- xgb.importance(colnames(agaricus.train$data), model = bst) +#' xgb.plot.importance( +#' importance_matrix, rel_to_first = TRUE, xlab = "Relative importance" +#' ) #' -#' xgb.plot.importance(importance_matrix, rel_to_first = TRUE, xlab = "Relative importance") -#' -#' (gg <- xgb.ggplot.importance(importance_matrix, measure = "Frequency", rel_to_first = TRUE)) +#' gg <- xgb.ggplot.importance( +#' importance_matrix, measure = "Frequency", rel_to_first = TRUE +#' ) +#' gg #' gg + ggplot2::ylab("Frequency") #' #' @rdname xgb.plot.importance diff --git a/R-package/R/xgb.plot.multi.trees.R b/R-package/R/xgb.plot.multi.trees.R index f4d797a61..6402cb767 100644 --- a/R-package/R/xgb.plot.multi.trees.R +++ b/R-package/R/xgb.plot.multi.trees.R @@ -1,14 +1,10 @@ -#' Project all trees on one tree and plot it +#' Project all trees on one tree #' #' Visualization of the ensemble of trees as a single collective unit. #' -#' @param model produced by the \code{xgb.train} function. -#' @param feature_names names of each feature as a \code{character} vector. -#' @param features_keep number of features to keep in each position of the multi trees. -#' @param plot_width width in pixels of the graph to produce -#' @param plot_height height in pixels of the graph to produce -#' @param render a logical flag for whether the graph should be rendered (see Value). -#' @param ... currently not used +#' @inheritParams xgb.plot.tree +#' @param features_keep Number of features to keep in each position of the multi trees, +#' by default 5. #' #' @details #' @@ -24,33 +20,31 @@ #' Moreover, the trees tend to reuse the same features. #' #' The function projects each tree onto one, and keeps for each position the -#' \code{features_keep} first features (based on the Gain per feature measure). +#' `features_keep` first features (based on the Gain per feature measure). #' #' This function is inspired by this blog post: -#' \url{https://wellecks.wordpress.com/2015/02/21/peering-into-the-black-box-visualizing-lambdamart/} +#' #' -#' @return -#' -#' When \code{render = TRUE}: -#' returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -#' Similar to ggplot objects, it needs to be printed to see it when not running from command line. -#' -#' When \code{render = FALSE}: -#' silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. -#' This could be useful if one wants to modify some of the graph attributes -#' before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +#' @inherit xgb.plot.tree return #' #' @examples #' -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' #' ## Keep the number of threads to 2 for examples #' nthread <- 2 #' data.table::setDTthreads(nthread) #' #' bst <- xgboost( -#' data = agaricus.train$data, label = agaricus.train$label, max_depth = 15, -#' eta = 1, nthread = nthread, nrounds = 30, objective = "binary:logistic", -#' min_child_weight = 50, verbose = 0 +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 15, +#' eta = 1, +#' nthread = nthread, +#' nrounds = 30, +#' objective = "binary:logistic", +#' min_child_weight = 50, +#' verbose = 0 #' ) #' #' p <- xgb.plot.multi.trees(model = bst, features_keep = 3) @@ -58,10 +52,13 @@ #' #' \dontrun{ #' # Below is an example of how to save this plot to a file. -#' # Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +#' # Note that for export_graph() to work, the {DiagrammeRsvg} and {rsvg} packages +#' # must also be installed. +#' #' library(DiagrammeR) -#' gr <- xgb.plot.multi.trees(model=bst, features_keep = 3, render=FALSE) -#' export_graph(gr, 'tree.pdf', width=1500, height=600) +#' +#' gr <- xgb.plot.multi.trees(model = bst, features_keep = 3, render = FALSE) +#' export_graph(gr, "tree.pdf", width = 1500, height = 600) #' } #' #' @export diff --git a/R-package/R/xgb.plot.shap.R b/R-package/R/xgb.plot.shap.R index d61bd23d4..35cf664ec 100644 --- a/R-package/R/xgb.plot.shap.R +++ b/R-package/R/xgb.plot.shap.R @@ -1,110 +1,165 @@ -#' SHAP contribution dependency plots +#' SHAP dependence plots #' -#' Visualizing the SHAP feature contribution to prediction dependencies on feature value. +#' Visualizes SHAP values against feature values to gain an impression of feature effects. #' -#' @param data data as a \code{matrix} or \code{dgCMatrix}. -#' @param shap_contrib a matrix of SHAP contributions that was computed earlier for the above -#' \code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}. -#' @param features a vector of either column indices or of feature names to plot. When it is NULL, -#' feature importance is calculated, and \code{top_n} high ranked features are taken. -#' @param top_n when \code{features} is NULL, top_n `[1, 100]` most important features in a model are taken. -#' @param model an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -#' or \code{features} is missing. -#' @param trees passed to \code{\link{xgb.importance}} when \code{features = NULL}. -#' @param target_class is only relevant for multiclass models. When it is set to a 0-based class index, -#' only SHAP contributions for that specific class are used. -#' If it is not set, SHAP importances are averaged over all classes. -#' @param approxcontrib passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}. -#' @param subsample a random fraction of data points to use for plotting. When it is NULL, -#' it is set so that up to 100K data points are used. -#' @param n_col a number of columns in a grid of plots. -#' @param col color of the scatterplot markers. -#' @param pch scatterplot marker. -#' @param discrete_n_uniq a maximal number of unique values in a feature to consider it as discrete. -#' @param discrete_jitter an \code{amount} parameter of jitter added to discrete features' positions. -#' @param ylab a y-axis label in 1D plots. -#' @param plot_NA whether the contributions of cases with missing values should also be plotted. -#' @param col_NA a color of marker for missing value contributions. -#' @param pch_NA a marker type for NA values. -#' @param pos_NA a relative position of the x-location where NA values are shown: -#' \code{min(x) + (max(x) - min(x)) * pos_NA}. -#' @param plot_loess whether to plot loess-smoothed curves. The smoothing is only done for features with -#' more than 5 distinct values. -#' @param col_loess a color to use for the loess curves. -#' @param span_loess the \code{span} parameter in \code{\link[stats]{loess}}'s call. -#' @param which whether to do univariate or bivariate plotting. NOTE: only 1D is implemented so far. -#' @param plot whether a plot should be drawn. If FALSE, only a list of matrices is returned. -#' @param ... other parameters passed to \code{plot}. +#' @param data The data to explain as a `matrix` or `dgCMatrix`. +#' @param shap_contrib Matrix of SHAP contributions of `data`. +#' The default (`NULL`) computes it from `model` and `data`. +#' @param features Vector of column indices or feature names to plot. +#' When `NULL` (default), the `top_n` most important features are selected +#' by [xgb.importance()]. +#' @param top_n How many of the most important features (<= 100) should be selected? +#' By default 1 for SHAP dependence and 10 for SHAP summary). +#' Only used when `features = NULL`. +#' @param model An `xgb.Booster` model. Only required when `shap_contrib = NULL` or +#' `features = NULL`. +#' @param trees Passed to [xgb.importance()] when `features = NULL`. +#' @param target_class Only relevant for multiclass models. The default (`NULL`) +#' averages the SHAP values over all classes. Pass a (0-based) class index +#' to show only SHAP values of that class. +#' @param approxcontrib Passed to `predict()` when `shap_contrib = NULL`. +#' @param subsample Fraction of data points randomly picked for plotting. +#' The default (`NULL`) will use up to 100k data points. +#' @param n_col Number of columns in a grid of plots. +#' @param col Color of the scatterplot markers. +#' @param pch Scatterplot marker. +#' @param discrete_n_uniq Maximal number of unique feature values to consider the +#' feature as discrete. +#' @param discrete_jitter Jitter amount added to the values of discrete features. +#' @param ylab The y-axis label in 1D plots. +#' @param plot_NA Should contributions of cases with missing values be plotted? +#' Default is `TRUE`. +#' @param col_NA Color of marker for missing value contributions. +#' @param pch_NA Marker type for `NA` values. +#' @param pos_NA Relative position of the x-location where `NA` values are shown: +#' `min(x) + (max(x) - min(x)) * pos_NA`. +#' @param plot_loess Should loess-smoothed curves be plotted? (Default is `TRUE`). +#' The smoothing is only done for features with more than 5 distinct values. +#' @param col_loess Color of loess curves. +#' @param span_loess The `span` parameter of [stats::loess()]. +#' @param which Whether to do univariate or bivariate plotting. Currently, only "1d" is implemented. +#' @param plot Should the plot be drawn? (Default is `TRUE`). +#' If `FALSE`, only a list of matrices is returned. +#' @param ... Other parameters passed to [graphics::plot()]. #' #' @details #' #' These scatterplots represent how SHAP feature contributions depend of feature values. -#' The similarity to partial dependency plots is that they also give an idea for how feature values -#' affect predictions. However, in partial dependency plots, we usually see marginal dependencies -#' of model prediction on feature value, while SHAP contribution dependency plots display the estimated -#' contributions of a feature to model prediction for each individual case. +#' The similarity to partial dependence plots is that they also give an idea for how feature values +#' affect predictions. However, in partial dependence plots, we see marginal dependencies +#' of model prediction on feature value, while SHAP dependence plots display the estimated +#' contributions of a feature to the prediction for each individual case. #' -#' When \code{plot_loess = TRUE} is set, feature values are rounded to 3 significant digits and -#' weighted LOESS is computed and plotted, where weights are the numbers of data points +#' When `plot_loess = TRUE`, feature values are rounded to three significant digits and +#' weighted LOESS is computed and plotted, where the weights are the numbers of data points #' at each rounded value. #' -#' Note: SHAP contributions are shown on the scale of model margin. E.g., for a logistic binomial objective, -#' the margin is prediction before a sigmoidal transform into probability-like values. +#' Note: SHAP contributions are on the scale of the model margin. +#' E.g., for a logistic binomial objective, the margin is on log-odds scale. #' Also, since SHAP stands for "SHapley Additive exPlanation" (model prediction = sum of SHAP #' contributions for all features + bias), depending on the objective used, transforming SHAP #' contributions for a feature from the marginal to the prediction space is not necessarily #' a meaningful thing to do. #' #' @return -#' -#' In addition to producing plots (when \code{plot=TRUE}), it silently returns a list of two matrices: -#' \itemize{ -#' \item \code{data} the values of selected features; -#' \item \code{shap_contrib} the contributions of selected features. -#' } +#' In addition to producing plots (when `plot = TRUE`), it silently returns a list of two matrices: +#' - `data`: Feature value matrix. +#' - `shap_contrib`: Corresponding SHAP value matrix. #' #' @references -#' -#' Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} -#' -#' Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +#' 1. Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +#' NIPS Proceedings 2017, +#' 2. Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +#' #' #' @examples #' -#' data(agaricus.train, package='xgboost') -#' data(agaricus.test, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' data(agaricus.test, package = "xgboost") #' #' ## Keep the number of threads to 1 for examples #' nthread <- 1 #' data.table::setDTthreads(nthread) #' nrounds <- 20 #' -#' bst <- xgboost(agaricus.train$data, agaricus.train$label, nrounds = nrounds, -#' eta = 0.1, max_depth = 3, subsample = .5, -#' method = "hist", objective = "binary:logistic", nthread = nthread, verbose = 0) +#' bst <- xgboost( +#' agaricus.train$data, +#' agaricus.train$label, +#' nrounds = nrounds, +#' eta = 0.1, +#' max_depth = 3, +#' subsample = 0.5, +#' objective = "binary:logistic", +#' nthread = nthread, +#' verbose = 0 +#' ) #' #' xgb.plot.shap(agaricus.test$data, model = bst, features = "odor=none") +#' #' contr <- predict(bst, agaricus.test$data, predcontrib = TRUE) #' xgb.plot.shap(agaricus.test$data, contr, model = bst, top_n = 12, n_col = 3) -#' xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) # Summary plot #' -#' # multiclass example - plots for each class separately: +#' # Summary plot +#' xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) +#' +#' # Multiclass example - plots for each class separately: #' nclass <- 3 #' x <- as.matrix(iris[, -5]) #' set.seed(123) #' is.na(x[sample(nrow(x) * 4, 30)]) <- TRUE # introduce some missing values -#' mbst <- xgboost(data = x, label = as.numeric(iris$Species) - 1, nrounds = nrounds, -#' max_depth = 2, eta = 0.3, subsample = .5, nthread = nthread, -#' objective = "multi:softprob", num_class = nclass, verbose = 0) -#' trees0 <- seq(from=0, by=nclass, length.out=nrounds) +#' +#' mbst <- xgboost( +#' data = x, +#' label = as.numeric(iris$Species) - 1, +#' nrounds = nrounds, +#' max_depth = 2, +#' eta = 0.3, +#' subsample = 0.5, +#' nthread = nthread, +#' objective = "multi:softprob", +#' num_class = nclass, +#' verbose = 0 +#' ) +#' trees0 <- seq(from = 0, by = nclass, length.out = nrounds) #' col <- rgb(0, 0, 1, 0.5) -#' xgb.plot.shap(x, model = mbst, trees = trees0, target_class = 0, top_n = 4, -#' n_col = 2, col = col, pch = 16, pch_NA = 17) -#' xgb.plot.shap(x, model = mbst, trees = trees0 + 1, target_class = 1, top_n = 4, -#' n_col = 2, col = col, pch = 16, pch_NA = 17) -#' xgb.plot.shap(x, model = mbst, trees = trees0 + 2, target_class = 2, top_n = 4, -#' n_col = 2, col = col, pch = 16, pch_NA = 17) -#' xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) # Summary plot +#' xgb.plot.shap( +#' x, +#' model = mbst, +#' trees = trees0, +#' target_class = 0, +#' top_n = 4, +#' n_col = 2, +#' col = col, +#' pch = 16, +#' pch_NA = 17 +#' ) +#' +#' xgb.plot.shap( +#' x, +#' model = mbst, +#' trees = trees0 + 1, +#' target_class = 1, +#' top_n = 4, +#' n_col = 2, +#' col = col, +#' pch = 16, +#' pch_NA = 17 +#' ) +#' +#' xgb.plot.shap( +#' x, +#' model = mbst, +#' trees = trees0 + 2, +#' target_class = 2, +#' top_n = 4, +#' n_col = 2, +#' col = col, +#' pch = 16, +#' pch_NA = 17 +#' ) +#' +#' # Summary plot +#' xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) #' #' @rdname xgb.plot.shap #' @export @@ -187,41 +242,48 @@ xgb.plot.shap <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, invisible(list(data = data, shap_contrib = shap_contrib)) } -#' SHAP contribution dependency summary plot +#' SHAP summary plot #' -#' Compare SHAP contributions of different features. +#' Visualizes SHAP contributions of different features. #' -#' A point plot (each point representing one sample from \code{data}) is +#' A point plot (each point representing one observation from `data`) is #' produced for each feature, with the points plotted on the SHAP value axis. -#' Each point (observation) is coloured based on its feature value. The plot -#' hence allows us to see which features have a negative / positive contribution +#' Each point (observation) is coloured based on its feature value. +#' +#' The plot allows to see which features have a negative / positive contribution #' on the model prediction, and whether the contribution is different for larger -#' or smaller values of the feature. We effectively try to replicate the -#' \code{summary_plot} function from . +#' or smaller values of the feature. Inspired by the summary plot of +#' . #' #' @inheritParams xgb.plot.shap #' -#' @return A \code{ggplot2} object. +#' @return A `ggplot2` object. #' @export #' -#' @examples # See \code{\link{xgb.plot.shap}}. -#' @seealso \code{\link{xgb.plot.shap}}, \code{\link{xgb.ggplot.shap.summary}}, -#' \url{https://github.com/shap/shap} +#' @examples +#' # See examples in xgb.plot.shap() +#' +#' @seealso [xgb.plot.shap()], [xgb.ggplot.shap.summary()], +#' and the Python library . xgb.plot.shap.summary <- function(data, shap_contrib = NULL, features = NULL, top_n = 10, model = NULL, trees = NULL, target_class = NULL, approxcontrib = FALSE, subsample = NULL) { # Only ggplot implementation is available. xgb.ggplot.shap.summary(data, shap_contrib, features, top_n, model, trees, target_class, approxcontrib, subsample) } -#' Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. -#' Internal utility function. +#' Prepare data for SHAP plots +#' +#' Internal function used in [xgb.plot.shap()], [xgb.plot.shap.summary()], etc. #' #' @inheritParams xgb.plot.shap +#' @param max_observations Maximum number of observations to consider. #' @keywords internal +#' @noRd #' -#' @return A list containing: 'data', a matrix containing sample observations -#' and their feature values; 'shap_contrib', a matrix containing the SHAP contribution -#' values for these observations. +#' @return +#' A list containing: +#' - `data`: The matrix of feature values. +#' - `shap_contrib`: The matrix with corresponding SHAP values. xgb.shap.data <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, model = NULL, trees = NULL, target_class = NULL, approxcontrib = FALSE, subsample = NULL, max_observations = 100000) { diff --git a/R-package/R/xgb.plot.tree.R b/R-package/R/xgb.plot.tree.R index 956c13cf7..29d00e111 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -1,69 +1,78 @@ -#' Plot a boosted tree model +#' Plot boosted trees #' #' Read a tree model text dump and plot the model. #' -#' @param feature_names names of each feature as a \code{character} vector. -#' @param model produced by the \code{xgb.train} function. -#' @param trees an integer vector of tree indices that should be visualized. -#' If set to \code{NULL}, all trees of the model are included. -#' IMPORTANT: the tree index in xgboost model is zero-based -#' (e.g., use \code{trees = 0:2} for the first 3 trees in a model). -#' @param plot_width the width of the diagram in pixels. -#' @param plot_height the height of the diagram in pixels. -#' @param render a logical flag for whether the graph should be rendered (see Value). +#' @param feature_names Character vector used to overwrite the feature names +#' of the model. The default (`NULL`) uses the original feature names. +#' @param model Object of class `xgb.Booster`. +#' @param trees An integer vector of tree indices that should be used. +#' The default (`NULL`) uses all trees. +#' Useful, e.g., in multiclass classification to get only +#' the trees of one class. *Important*: the tree index in XGBoost models +#' is zero-based (e.g., use `trees = 0:2` for the first three trees). +#' @param plot_width,plot_height Width and height of the graph in pixels. +#' The values are passed to [DiagrammeR::render_graph()]. +#' @param render Should the graph be rendered or not? The default is `TRUE`. #' @param show_node_id a logical flag for whether to show node id's in the graph. #' @param ... currently not used. #' #' @details #' -#' The content of each node is organised that way: -#' -#' \itemize{ -#' \item Feature name. -#' \item \code{Cover}: The sum of second order gradient of training data classified to the leaf. -#' If it is square loss, this simply corresponds to the number of instances seen by a split -#' or collected by a leaf during training. -#' The deeper in the tree a node is, the lower this metric will be. -#' \item \code{Gain} (for split nodes): the information gain metric of a split +#' The content of each node is visualized like this: +#' - *Feature name*. +#' - *Cover:* The sum of second order gradients of training data. +#' For the squared loss, this simply corresponds to the number of instances in the node. +#' The deeper in the tree, the lower the value. +#' - *Gain* (for split nodes): Information gain metric of a split #' (corresponds to the importance of the node in the model). -#' \item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. -#' } -#' The tree root nodes also indicate the Tree index (0-based). +#' - *Value* (for leaves): Margin value that the leaf may contribute to the prediction. +#' +#' The tree root nodes also indicate the tree index (0-based). #' #' The "Yes" branches are marked by the "< split_value" label. -#' The branches that also used for missing values are marked as bold +#' The branches also used for missing values are marked as bold #' (as in "carrying extra capacity"). #' -#' This function uses \href{https://www.graphviz.org/}{GraphViz} as a backend of DiagrammeR. +#' This function uses [GraphViz](https://www.graphviz.org/) as DiagrammeR backend. #' #' @return -#' -#' When \code{render = TRUE}: -#' returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -#' Similar to ggplot objects, it needs to be printed to see it when not running from command line. -#' -#' When \code{render = FALSE}: -#' silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. -#' This could be useful if one wants to modify some of the graph attributes -#' before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +#' The value depends on the `render` parameter: +#' - If `render = TRUE` (default): Rendered graph object which is an htmlwidget of +#' class `grViz`. Similar to "ggplot" objects, it needs to be printed when not +#' running from the command line. +#' - If `render = FALSE`: Graph object which is of DiagrammeR's class `dgr_graph`. +#' This could be useful if one wants to modify some of the graph attributes +#' before rendering the graph with [DiagrammeR::render_graph()]. #' #' @examples -#' data(agaricus.train, package='xgboost') +#' data(agaricus.train, package = "xgboost") +#' +#' bst <- xgboost( +#' data = agaricus.train$data, +#' label = agaricus.train$label, +#' max_depth = 3, +#' eta = 1, +#' nthread = 2, +#' nrounds = 2, +#' objective = "binary:logistic" +#' ) #' -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, -#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") #' # plot all the trees #' xgb.plot.tree(model = bst) +#' #' # plot only the first tree and display the node ID: #' xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) #' #' \dontrun{ #' # Below is an example of how to save this plot to a file. -#' # Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +#' # Note that for export_graph() to work, the {DiagrammeRsvg} +#' # and {rsvg} packages must also be installed. +#' #' library(DiagrammeR) -#' gr <- xgb.plot.tree(model=bst, trees=0:1, render=FALSE) -#' export_graph(gr, 'tree.pdf', width=1500, height=1900) -#' export_graph(gr, 'tree.png', width=1500, height=1900) +#' +#' gr <- xgb.plot.tree(model = bst, trees = 0:1, render = FALSE) +#' export_graph(gr, "tree.pdf", width = 1500, height = 1900) +#' export_graph(gr, "tree.png", width = 1500, height = 1900) #' } #' #' @export diff --git a/R-package/man/xgb.importance.Rd b/R-package/man/xgb.importance.Rd index 12daca365..fca1b70c4 100644 --- a/R-package/man/xgb.importance.Rd +++ b/R-package/man/xgb.importance.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.importance.R \name{xgb.importance} \alias{xgb.importance} -\title{Importance of features in a model.} +\title{Feature importance} \usage{ xgb.importance( feature_names = NULL, @@ -14,88 +14,126 @@ xgb.importance( ) } \arguments{ -\item{feature_names}{character vector of feature names. If the model already -contains feature names, those would be used when \code{feature_names=NULL} (default value). -Non-null \code{feature_names} could be provided to override those in the model.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default is \code{NULL} (use original feature names).} -\item{model}{object of class \code{xgb.Booster}.} +\item{model}{Object of class \code{xgb.Booster}.} -\item{trees}{(only for the gbtree booster) an integer vector of tree indices that should be included -into the importance calculation. If set to \code{NULL}, all trees of the model are parsed. +\item{trees}{An integer vector of tree indices that should be included +into the importance calculation (only for the "gbtree" booster). +The default (\code{NULL}) parses all trees. It could be useful, e.g., in multiclass classification to get feature importances -for each class separately. IMPORTANT: the tree index in xgboost models -is zero-based (e.g., use \code{trees = 0:4} for first 5 trees).} +for each class separately. \emph{Important}: the tree index in XGBoost models +is zero-based (e.g., use \code{trees = 0:4} for the first five trees).} -\item{data}{deprecated.} +\item{data}{Deprecated.} -\item{label}{deprecated.} +\item{label}{Deprecated.} -\item{target}{deprecated.} +\item{target}{Deprecated.} } \value{ -For a tree model, a \code{data.table} with the following columns: +A \code{data.table} with the following columns: + +For a tree model: \itemize{ -\item \code{Features} names of the features used in the model; -\item \code{Gain} represents fractional contribution of each feature to the model based on -the total gain of this feature's splits. Higher percentage means a more important -predictive feature. -\item \code{Cover} metric of the number of observation related to this feature; -\item \code{Frequency} percentage representing the relative number of times -a feature have been used in trees. +\item \code{Features}: Names of the features used in the model. +\item \code{Gain}: Fractional contribution of each feature to the model based on +the total gain of this feature's splits. Higher percentage means higher importance. +\item \code{Cover}: Metric of the number of observation related to this feature. +\item \code{Frequency}: Percentage of times a feature has been used in trees. } -A linear model's importance \code{data.table} has the following columns: +For a linear model: \itemize{ -\item \code{Features} names of the features used in the model; -\item \code{Weight} the linear coefficient of this feature; -\item \code{Class} (only for multiclass models) class label. +\item \code{Features}: Names of the features used in the model. +\item \code{Weight}: Linear coefficient of this feature. +\item \code{Class}: Class label (only for multiclass models). } If \code{feature_names} is not provided and \code{model} doesn't have \code{feature_names}, -index of the features will be used instead. Because the index is extracted from the model dump +the index of the features will be used instead. Because the index is extracted from the model dump (based on C++ code), it starts at 0 (as in C/C++ or Python) instead of 1 (usual in R). } \description{ -Creates a \code{data.table} of feature importances in a model. +Creates a \code{data.table} of feature importances. } \details{ This function works for both linear and tree models. For linear models, the importance is the absolute magnitude of linear coefficients. -For that reason, in order to obtain a meaningful ranking by importance for a linear model, -the features need to be on the same scale (which you also would want to do when using either -L1 or L2 regularization). +To obtain a meaningful ranking by importance for linear models, the features need to +be on the same scale (which is also recommended when using L1 or L2 regularization). } \examples{ -# binomial classification using gbtree: -data(agaricus.train, package='xgboost') -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +# binomial classification using "gbtree": +data(agaricus.train, package = "xgboost") + +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 2, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) + xgb.importance(model = bst) -# binomial classification using gblinear: -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, booster = "gblinear", - eta = 0.3, nthread = 1, nrounds = 20, objective = "binary:logistic") +# binomial classification using "gblinear": +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + booster = "gblinear", + eta = 0.3, + nthread = 1, + nrounds = 20,objective = "binary:logistic" +) + xgb.importance(model = bst) -# multiclass classification using gbtree: +# multiclass classification using "gbtree": nclass <- 3 nrounds <- 10 -mbst <- xgboost(data = as.matrix(iris[, -5]), label = as.numeric(iris$Species) - 1, - max_depth = 3, eta = 0.2, nthread = 2, nrounds = nrounds, - objective = "multi:softprob", num_class = nclass) +mbst <- xgboost( + data = as.matrix(iris[, -5]), + label = as.numeric(iris$Species) - 1, + max_depth = 3, + eta = 0.2, + nthread = 2, + nrounds = nrounds, + objective = "multi:softprob", + num_class = nclass +) + # all classes clumped together: xgb.importance(model = mbst) -# inspect importances separately for each class: -xgb.importance(model = mbst, trees = seq(from=0, by=nclass, length.out=nrounds)) -xgb.importance(model = mbst, trees = seq(from=1, by=nclass, length.out=nrounds)) -xgb.importance(model = mbst, trees = seq(from=2, by=nclass, length.out=nrounds)) -# multiclass classification using gblinear: -mbst <- xgboost(data = scale(as.matrix(iris[, -5])), label = as.numeric(iris$Species) - 1, - booster = "gblinear", eta = 0.2, nthread = 1, nrounds = 15, - objective = "multi:softprob", num_class = nclass) +# inspect importances separately for each class: +xgb.importance( + model = mbst, trees = seq(from = 0, by = nclass, length.out = nrounds) +) +xgb.importance( + model = mbst, trees = seq(from = 1, by = nclass, length.out = nrounds) +) +xgb.importance( + model = mbst, trees = seq(from = 2, by = nclass, length.out = nrounds) +) + +# multiclass classification using "gblinear": +mbst <- xgboost( + data = scale(as.matrix(iris[, -5])), + label = as.numeric(iris$Species) - 1, + booster = "gblinear", + eta = 0.2, + nthread = 1, + nrounds = 15, + objective = "multi:softprob", + num_class = nclass +) + xgb.importance(model = mbst) } diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 131830bde..477c40775 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.model.dt.tree.R \name{xgb.model.dt.tree} \alias{xgb.model.dt.tree} -\title{Parse a boosted tree model text dump} +\title{Parse model text dump} \usage{ xgb.model.dt.tree( feature_names = NULL, @@ -14,49 +14,45 @@ xgb.model.dt.tree( ) } \arguments{ -\item{feature_names}{character vector of feature names. If the model already -contains feature names, those would be used when \code{feature_names=NULL} (default value). -Non-null \code{feature_names} could be provided to override those in the model.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default (\code{NULL}) uses the original feature names.} -\item{model}{object of class \code{xgb.Booster}} +\item{model}{Object of class \code{xgb.Booster}.} -\item{text}{\code{character} vector previously generated by the \code{xgb.dump} -function (where parameter \code{with_stats = TRUE} should have been set). -\code{text} takes precedence over \code{model}.} +\item{text}{Character vector previously generated by the function \code{\link[=xgb.dump]{xgb.dump()}} +(called with parameter \code{with_stats = TRUE}). \code{text} takes precedence over \code{model}.} -\item{trees}{an integer vector of tree indices that should be parsed. -If set to \code{NULL}, all trees of the model are parsed. -It could be useful, e.g., in multiclass classification to get only -the trees of one certain class. IMPORTANT: the tree index in xgboost models -is zero-based (e.g., use \code{trees = 0:4} for first 5 trees).} +\item{trees}{An integer vector of tree indices that should be used. +The default (\code{NULL}) uses all trees. +Useful, e.g., in multiclass classification to get only +the trees of one class. \emph{Important}: the tree index in XGBoost models +is zero-based (e.g., use \code{trees = 0:4} for the first five trees).} -\item{use_int_id}{a logical flag indicating whether nodes in columns "Yes", "No", "Missing" should be -represented as integers (when FALSE) or as "Tree-Node" character strings (when FALSE).} +\item{use_int_id}{A logical flag indicating whether nodes in columns "Yes", "No", and +"Missing" should be represented as integers (when \code{TRUE}) or as "Tree-Node" +character strings (when \code{FALSE}, default).} -\item{...}{currently not used.} +\item{...}{Currently not used.} } \value{ -A \code{data.table} with detailed information about model trees' nodes. - -The columns of the \code{data.table} are: - +A \code{data.table} with detailed information about tree nodes. It has the following columns: \itemize{ -\item \code{Tree}: integer ID of a tree in a model (zero-based index) -\item \code{Node}: integer ID of a node in a tree (zero-based index) -\item \code{ID}: character identifier of a node in a model (only when \code{use_int_id=FALSE}) -\item \code{Feature}: for a branch node, it's a feature id or name (when available); -for a leaf note, it simply labels it as \code{'Leaf'} -\item \code{Split}: location of the split for a branch node (split condition is always "less than") -\item \code{Yes}: ID of the next node when the split condition is met -\item \code{No}: ID of the next node when the split condition is not met -\item \code{Missing}: ID of the next node when branch value is missing -\item \code{Quality}: either the split gain (change in loss) or the leaf value -\item \code{Cover}: metric related to the number of observation either seen by a split +\item \code{Tree}: integer ID of a tree in a model (zero-based index). +\item \code{Node}: integer ID of a node in a tree (zero-based index). +\item \code{ID}: character identifier of a node in a model (only when \code{use_int_id = FALSE}). +\item \code{Feature}: for a branch node, a feature ID or name (when available); +for a leaf node, it simply labels it as \code{"Leaf"}. +\item \code{Split}: location of the split for a branch node (split condition is always "less than"). +\item \code{Yes}: ID of the next node when the split condition is met. +\item \code{No}: ID of the next node when the split condition is not met. +\item \code{Missing}: ID of the next node when the branch value is missing. +\item \code{Quality}: either the split gain (change in loss) or the leaf value. +\item \code{Cover}: metric related to the number of observations either seen by a split or collected by a leaf during training. } -When \code{use_int_id=FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers -in the "ID" column. When \code{use_int_id=TRUE}, those columns point to node identifiers from +When \code{use_int_id = FALSE}, columns "Yes", "No", and "Missing" point to model-wide node identifiers +in the "ID" column. When \code{use_int_id = TRUE}, those columns point to node identifiers from the corresponding trees in the "Node" column. } \description{ @@ -65,13 +61,20 @@ Parse a boosted tree model text dump into a \code{data.table} structure. \examples{ # Basic use: -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") ## Keep the number of threads to 1 for examples nthread <- 1 data.table::setDTthreads(nthread) -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 2, + eta = 1, + nthread = nthread, + nrounds = 2, + objective = "binary:logistic" +) (dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) @@ -80,7 +83,11 @@ bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_dep (dt <- xgb.model.dt.tree(model = bst)) # How to match feature names of splits that are following a current 'Yes' branch: - -merge(dt, dt[, .(ID, Y.Feature=Feature)], by.x='Yes', by.y='ID', all.x=TRUE)[order(Tree,Node)] +merge( + dt, + dt[, .(ID, Y.Feature = Feature)], by.x = "Yes", by.y = "ID", all.x = TRUE +)[ + order(Tree, Node) +] } diff --git a/R-package/man/xgb.plot.deepness.Rd b/R-package/man/xgb.plot.deepness.Rd index 12c5c68e2..43c0dac77 100644 --- a/R-package/man/xgb.plot.deepness.Rd +++ b/R-package/man/xgb.plot.deepness.Rd @@ -3,7 +3,7 @@ \name{xgb.ggplot.deepness} \alias{xgb.ggplot.deepness} \alias{xgb.plot.deepness} -\title{Plot model trees deepness} +\title{Plot model tree depth} \usage{ xgb.ggplot.deepness( model = NULL, @@ -18,66 +18,84 @@ xgb.plot.deepness( ) } \arguments{ -\item{model}{either an \code{xgb.Booster} model generated by the \code{xgb.train} function -or a data.table result of the \code{xgb.model.dt.tree} function.} +\item{model}{Either an \code{xgb.Booster} model, or the "data.table" returned by \code{\link[=xgb.model.dt.tree]{xgb.model.dt.tree()}}.} -\item{which}{which distribution to plot (see details).} +\item{which}{Which distribution to plot (see details).} -\item{plot}{(base R barplot) whether a barplot should be produced. -If FALSE, only a data.table is returned.} +\item{plot}{Should the plot be shown? Default is \code{TRUE}.} -\item{...}{other parameters passed to \code{barplot} or \code{plot}.} +\item{...}{Other parameters passed to \code{\link[graphics:barplot]{graphics::barplot()}} or \code{\link[graphics:plot.default]{graphics::plot()}}.} } \value{ -Other than producing plots (when \code{plot=TRUE}), the \code{xgb.plot.deepness} function -silently returns a processed data.table where each row corresponds to a terminal leaf in a tree model, -and contains information about leaf's depth, cover, and weight (which is used in calculating predictions). - -The \code{xgb.ggplot.deepness} silently returns either a list of two ggplot graphs when \code{which="2x1"} -or a single ggplot graph for the other \code{which} options. +The return value of the two functions is as follows: +\itemize{ +\item \code{xgb.plot.deepness()}: A "data.table" (invisibly). +Each row corresponds to a terminal leaf in the model. It contains its information +about depth, cover, and weight (used in calculating predictions). +If \code{plot = TRUE}, also a plot is shown. +\item \code{xgb.ggplot.deepness()}: When \code{which = "2x1"}, a list of two "ggplot" objects, +and a single "ggplot" object otherwise. +} } \description{ -Visualizes distributions related to depth of tree leafs. -\code{xgb.plot.deepness} uses base R graphics, while \code{xgb.ggplot.deepness} uses the ggplot backend. +Visualizes distributions related to the depth of tree leaves. +\itemize{ +\item \code{xgb.plot.deepness()} uses base R graphics, while +\item \code{xgb.ggplot.deepness()} uses "ggplot2". +} } \details{ -When \code{which="2x1"}, two distributions with respect to the leaf depth +When \code{which = "2x1"}, two distributions with respect to the leaf depth are plotted on top of each other: -\itemize{ -\item the distribution of the number of leafs in a tree model at a certain depth; -\item the distribution of average weighted number of observations ("cover") -ending up in leafs at certain depth. +\enumerate{ +\item The distribution of the number of leaves in a tree model at a certain depth. +\item The distribution of the average weighted number of observations ("cover") +ending up in leaves at a certain depth. } + Those could be helpful in determining sensible ranges of the \code{max_depth} and \code{min_child_weight} parameters. -When \code{which="max.depth"} or \code{which="med.depth"}, plots of either maximum or median depth -per tree with respect to tree number are created. And \code{which="med.weight"} allows to see how +When \code{which = "max.depth"} or \code{which = "med.depth"}, plots of either maximum or +median depth per tree with respect to the tree number are created. + +Finally, \code{which = "med.weight"} allows to see how a tree's median absolute leaf weight changes through the iterations. -This function was inspired by the blog post +These functions have been inspired by the blog post \url{https://github.com/aysent/random-forest-leaf-visualization}. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") ## Keep the number of threads to 2 for examples nthread <- 2 data.table::setDTthreads(nthread) ## Change max_depth to a higher number to get a more significant result -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 6, - eta = 0.1, nthread = nthread, nrounds = 50, objective = "binary:logistic", - subsample = 0.5, min_child_weight = 2) +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 6, + nthread = nthread, + nrounds = 50, + objective = "binary:logistic", + subsample = 0.5, + min_child_weight = 2 +) xgb.plot.deepness(bst) xgb.ggplot.deepness(bst) -xgb.plot.deepness(bst, which='max.depth', pch=16, col=rgb(0,0,1,0.3), cex=2) +xgb.plot.deepness( + bst, which = "max.depth", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +) -xgb.plot.deepness(bst, which='med.weight', pch=16, col=rgb(0,0,1,0.3), cex=2) +xgb.plot.deepness( + bst, which = "med.weight", pch = 16, col = rgb(0, 0, 1, 0.3), cex = 2 +) } \seealso{ -\code{\link{xgb.train}}, \code{\link{xgb.model.dt.tree}}. +\code{\link[=xgb.train]{xgb.train()}} and \code{\link[=xgb.model.dt.tree]{xgb.model.dt.tree()}}. } diff --git a/R-package/man/xgb.plot.importance.Rd b/R-package/man/xgb.plot.importance.Rd index 4dba62afe..e9c5930c2 100644 --- a/R-package/man/xgb.plot.importance.Rd +++ b/R-package/man/xgb.plot.importance.Rd @@ -3,7 +3,7 @@ \name{xgb.ggplot.importance} \alias{xgb.ggplot.importance} \alias{xgb.plot.importance} -\title{Plot feature importance as a bar graph} +\title{Plot feature importance} \usage{ xgb.ggplot.importance( importance_matrix = NULL, @@ -26,74 +26,90 @@ xgb.plot.importance( ) } \arguments{ -\item{importance_matrix}{a \code{data.table} returned by \code{\link{xgb.importance}}.} +\item{importance_matrix}{A \code{data.table} as returned by \code{\link[=xgb.importance]{xgb.importance()}}.} -\item{top_n}{maximal number of top features to include into the plot.} +\item{top_n}{Maximal number of top features to include into the plot.} -\item{measure}{the name of importance measure to plot. +\item{measure}{The name of importance measure to plot. When \code{NULL}, 'Gain' would be used for trees and 'Weight' would be used for gblinear.} -\item{rel_to_first}{whether importance values should be represented as relative to the highest ranked feature. -See Details.} +\item{rel_to_first}{Whether importance values should be represented as relative to +the highest ranked feature, see Details.} -\item{n_clusters}{(ggplot only) a \code{numeric} vector containing the min and the max range +\item{n_clusters}{A numeric vector containing the min and the max range of the possible number of clusters of bars.} -\item{...}{other parameters passed to \code{barplot} (except horiz, border, cex.names, names.arg, and las).} +\item{...}{Other parameters passed to \code{\link[graphics:barplot]{graphics::barplot()}} +(except \code{horiz}, \code{border}, \code{cex.names}, \code{names.arg}, and \code{las}). +Only used in \code{xgb.plot.importance()}.} -\item{left_margin}{(base R barplot) allows to adjust the left margin size to fit feature names. -When it is NULL, the existing \code{par('mar')} is used.} +\item{left_margin}{Adjust the left margin size to fit feature names. +When \code{NULL}, the existing \code{par("mar")} is used.} -\item{cex}{(base R barplot) passed as \code{cex.names} parameter to \code{barplot}.} +\item{cex}{Passed as \code{cex.names} parameter to \code{\link[graphics:barplot]{graphics::barplot()}}.} -\item{plot}{(base R barplot) whether a barplot should be produced. -If FALSE, only a data.table is returned.} +\item{plot}{Should the barplot be shown? Default is \code{TRUE}.} } \value{ -The \code{xgb.plot.importance} function creates a \code{barplot} (when \code{plot=TRUE}) -and silently returns a processed data.table with \code{n_top} features sorted by importance. - -The \code{xgb.ggplot.importance} function returns a ggplot graph which could be customized afterwards. -E.g., to change the title of the graph, add \code{+ ggtitle("A GRAPH NAME")} to the result. +The return value depends on the function: +\itemize{ +\item \code{xgb.plot.importance()}: Invisibly, a "data.table" with \code{n_top} features sorted +by importance. If \code{plot = TRUE}, the values are also plotted as barplot. +\item \code{xgb.ggplot.importance()}: A customizable "ggplot" object. +E.g., to change the title, set \code{+ ggtitle("A GRAPH NAME")}. +} } \description{ Represents previously calculated feature importance as a bar graph. -\code{xgb.plot.importance} uses base R graphics, while \code{xgb.ggplot.importance} uses the ggplot backend. +\itemize{ +\item \code{xgb.plot.importance()} uses base R graphics, while +\item \code{xgb.ggplot.importance()} uses "ggplot". +} } \details{ The graph represents each feature as a horizontal bar of length proportional to the importance of a feature. -Features are shown ranked in a decreasing importance order. -It works for importances from both \code{gblinear} and \code{gbtree} models. +Features are sorted by decreasing importance. +It works for both "gblinear" and "gbtree" models. -When \code{rel_to_first = FALSE}, the values would be plotted as they were in \code{importance_matrix}. -For gbtree model, that would mean being normalized to the total of 1 +When \code{rel_to_first = FALSE}, the values would be plotted as in \code{importance_matrix}. +For a "gbtree" model, that would mean being normalized to the total of 1 ("what is feature's importance contribution relative to the whole model?"). For linear models, \code{rel_to_first = FALSE} would show actual values of the coefficients. Setting \code{rel_to_first = TRUE} allows to see the picture from the perspective of "what is feature's importance contribution relative to the most important feature?" -The ggplot-backend method also performs 1-D clustering of the importance values, -with bar colors corresponding to different clusters that have somewhat similar importance values. +The "ggplot" backend performs 1-D clustering of the importance values, +with bar colors corresponding to different clusters having similar importance values. } \examples{ data(agaricus.train) + ## Keep the number of threads to 2 for examples nthread <- 2 data.table::setDTthreads(nthread) bst <- xgboost( - data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, - eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 3, + eta = 1, + nthread = nthread, + nrounds = 2, + objective = "binary:logistic" ) importance_matrix <- xgb.importance(colnames(agaricus.train$data), model = bst) +xgb.plot.importance( + importance_matrix, rel_to_first = TRUE, xlab = "Relative importance" +) -xgb.plot.importance(importance_matrix, rel_to_first = TRUE, xlab = "Relative importance") - -(gg <- xgb.ggplot.importance(importance_matrix, measure = "Frequency", rel_to_first = TRUE)) +gg <- xgb.ggplot.importance( + importance_matrix, measure = "Frequency", rel_to_first = TRUE +) +gg gg + ggplot2::ylab("Frequency") } \seealso{ -\code{\link[graphics]{barplot}}. +\code{\link[graphics:barplot]{graphics::barplot()}} } diff --git a/R-package/man/xgb.plot.multi.trees.Rd b/R-package/man/xgb.plot.multi.trees.Rd index 4fa526b90..d98a3482c 100644 --- a/R-package/man/xgb.plot.multi.trees.Rd +++ b/R-package/man/xgb.plot.multi.trees.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.plot.multi.trees.R \name{xgb.plot.multi.trees} \alias{xgb.plot.multi.trees} -\title{Project all trees on one tree and plot it} +\title{Project all trees on one tree} \usage{ xgb.plot.multi.trees( model, @@ -15,29 +15,31 @@ xgb.plot.multi.trees( ) } \arguments{ -\item{model}{produced by the \code{xgb.train} function.} +\item{model}{Object of class \code{xgb.Booster}.} -\item{feature_names}{names of each feature as a \code{character} vector.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default (\code{NULL}) uses the original feature names.} -\item{features_keep}{number of features to keep in each position of the multi trees.} +\item{features_keep}{Number of features to keep in each position of the multi trees, +by default 5.} -\item{plot_width}{width in pixels of the graph to produce} +\item{plot_width, plot_height}{Width and height of the graph in pixels. +The values are passed to \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}.} -\item{plot_height}{height in pixels of the graph to produce} +\item{render}{Should the graph be rendered or not? The default is \code{TRUE}.} -\item{render}{a logical flag for whether the graph should be rendered (see Value).} - -\item{...}{currently not used} +\item{...}{currently not used.} } \value{ -When \code{render = TRUE}: -returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -Similar to ggplot objects, it needs to be printed to see it when not running from command line. - -When \code{render = FALSE}: -silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. +The value depends on the \code{render} parameter: +\itemize{ +\item If \code{render = TRUE} (default): Rendered graph object which is an htmlwidget of +class \code{grViz}. Similar to "ggplot" objects, it needs to be printed when not +running from the command line. +\item If \code{render = FALSE}: Graph object which is of DiagrammeR's class \code{dgr_graph}. This could be useful if one wants to modify some of the graph attributes -before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +before rendering the graph with \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}. +} } \description{ Visualization of the ensemble of trees as a single collective unit. @@ -62,15 +64,22 @@ This function is inspired by this blog post: } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") + ## Keep the number of threads to 2 for examples nthread <- 2 data.table::setDTthreads(nthread) bst <- xgboost( - data = agaricus.train$data, label = agaricus.train$label, max_depth = 15, - eta = 1, nthread = nthread, nrounds = 30, objective = "binary:logistic", - min_child_weight = 50, verbose = 0 + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 15, + eta = 1, + nthread = nthread, + nrounds = 30, + objective = "binary:logistic", + min_child_weight = 50, + verbose = 0 ) p <- xgb.plot.multi.trees(model = bst, features_keep = 3) @@ -78,10 +87,13 @@ print(p) \dontrun{ # Below is an example of how to save this plot to a file. -# Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +# Note that for export_graph() to work, the {DiagrammeRsvg} and {rsvg} packages +# must also be installed. + library(DiagrammeR) -gr <- xgb.plot.multi.trees(model=bst, features_keep = 3, render=FALSE) -export_graph(gr, 'tree.pdf', width=1500, height=600) + +gr <- xgb.plot.multi.trees(model = bst, features_keep = 3, render = FALSE) +export_graph(gr, "tree.pdf", width = 1500, height = 600) } } diff --git a/R-package/man/xgb.plot.shap.Rd b/R-package/man/xgb.plot.shap.Rd index 75f8d2d0f..b460fa1fb 100644 --- a/R-package/man/xgb.plot.shap.Rd +++ b/R-package/man/xgb.plot.shap.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.plot.shap.R \name{xgb.plot.shap} \alias{xgb.plot.shap} -\title{SHAP contribution dependency plots} +\title{SHAP dependence plots} \usage{ xgb.plot.shap( data, @@ -33,87 +33,93 @@ xgb.plot.shap( ) } \arguments{ -\item{data}{data as a \code{matrix} or \code{dgCMatrix}.} +\item{data}{The data to explain as a \code{matrix} or \code{dgCMatrix}.} -\item{shap_contrib}{a matrix of SHAP contributions that was computed earlier for the above -\code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}.} +\item{shap_contrib}{Matrix of SHAP contributions of \code{data}. +The default (\code{NULL}) computes it from \code{model} and \code{data}.} -\item{features}{a vector of either column indices or of feature names to plot. When it is NULL, -feature importance is calculated, and \code{top_n} high ranked features are taken.} +\item{features}{Vector of column indices or feature names to plot. +When \code{NULL} (default), the \code{top_n} most important features are selected +by \code{\link[=xgb.importance]{xgb.importance()}}.} -\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} +\item{top_n}{How many of the most important features (<= 100) should be selected? +By default 1 for SHAP dependence and 10 for SHAP summary). +Only used when \code{features = NULL}.} -\item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -or \code{features} is missing.} +\item{model}{An \code{xgb.Booster} model. Only required when \code{shap_contrib = NULL} or +\code{features = NULL}.} -\item{trees}{passed to \code{\link{xgb.importance}} when \code{features = NULL}.} +\item{trees}{Passed to \code{\link[=xgb.importance]{xgb.importance()}} when \code{features = NULL}.} -\item{target_class}{is only relevant for multiclass models. When it is set to a 0-based class index, -only SHAP contributions for that specific class are used. -If it is not set, SHAP importances are averaged over all classes.} +\item{target_class}{Only relevant for multiclass models. The default (\code{NULL}) +averages the SHAP values over all classes. Pass a (0-based) class index +to show only SHAP values of that class.} -\item{approxcontrib}{passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}.} +\item{approxcontrib}{Passed to \code{predict()} when \code{shap_contrib = NULL}.} -\item{subsample}{a random fraction of data points to use for plotting. When it is NULL, -it is set so that up to 100K data points are used.} +\item{subsample}{Fraction of data points randomly picked for plotting. +The default (\code{NULL}) will use up to 100k data points.} -\item{n_col}{a number of columns in a grid of plots.} +\item{n_col}{Number of columns in a grid of plots.} -\item{col}{color of the scatterplot markers.} +\item{col}{Color of the scatterplot markers.} -\item{pch}{scatterplot marker.} +\item{pch}{Scatterplot marker.} -\item{discrete_n_uniq}{a maximal number of unique values in a feature to consider it as discrete.} +\item{discrete_n_uniq}{Maximal number of unique feature values to consider the +feature as discrete.} -\item{discrete_jitter}{an \code{amount} parameter of jitter added to discrete features' positions.} +\item{discrete_jitter}{Jitter amount added to the values of discrete features.} -\item{ylab}{a y-axis label in 1D plots.} +\item{ylab}{The y-axis label in 1D plots.} -\item{plot_NA}{whether the contributions of cases with missing values should also be plotted.} +\item{plot_NA}{Should contributions of cases with missing values be plotted? +Default is \code{TRUE}.} -\item{col_NA}{a color of marker for missing value contributions.} +\item{col_NA}{Color of marker for missing value contributions.} -\item{pch_NA}{a marker type for NA values.} +\item{pch_NA}{Marker type for \code{NA} values.} -\item{pos_NA}{a relative position of the x-location where NA values are shown: +\item{pos_NA}{Relative position of the x-location where \code{NA} values are shown: \code{min(x) + (max(x) - min(x)) * pos_NA}.} -\item{plot_loess}{whether to plot loess-smoothed curves. The smoothing is only done for features with -more than 5 distinct values.} +\item{plot_loess}{Should loess-smoothed curves be plotted? (Default is \code{TRUE}). +The smoothing is only done for features with more than 5 distinct values.} -\item{col_loess}{a color to use for the loess curves.} +\item{col_loess}{Color of loess curves.} -\item{span_loess}{the \code{span} parameter in \code{\link[stats]{loess}}'s call.} +\item{span_loess}{The \code{span} parameter of \code{\link[stats:loess]{stats::loess()}}.} -\item{which}{whether to do univariate or bivariate plotting. NOTE: only 1D is implemented so far.} +\item{which}{Whether to do univariate or bivariate plotting. Currently, only "1d" is implemented.} -\item{plot}{whether a plot should be drawn. If FALSE, only a list of matrices is returned.} +\item{plot}{Should the plot be drawn? (Default is \code{TRUE}). +If \code{FALSE}, only a list of matrices is returned.} -\item{...}{other parameters passed to \code{plot}.} +\item{...}{Other parameters passed to \code{\link[graphics:plot.default]{graphics::plot()}}.} } \value{ -In addition to producing plots (when \code{plot=TRUE}), it silently returns a list of two matrices: +In addition to producing plots (when \code{plot = TRUE}), it silently returns a list of two matrices: \itemize{ -\item \code{data} the values of selected features; -\item \code{shap_contrib} the contributions of selected features. +\item \code{data}: Feature value matrix. +\item \code{shap_contrib}: Corresponding SHAP value matrix. } } \description{ -Visualizing the SHAP feature contribution to prediction dependencies on feature value. +Visualizes SHAP values against feature values to gain an impression of feature effects. } \details{ These scatterplots represent how SHAP feature contributions depend of feature values. -The similarity to partial dependency plots is that they also give an idea for how feature values -affect predictions. However, in partial dependency plots, we usually see marginal dependencies -of model prediction on feature value, while SHAP contribution dependency plots display the estimated -contributions of a feature to model prediction for each individual case. +The similarity to partial dependence plots is that they also give an idea for how feature values +affect predictions. However, in partial dependence plots, we see marginal dependencies +of model prediction on feature value, while SHAP dependence plots display the estimated +contributions of a feature to the prediction for each individual case. -When \code{plot_loess = TRUE} is set, feature values are rounded to 3 significant digits and -weighted LOESS is computed and plotted, where weights are the numbers of data points +When \code{plot_loess = TRUE}, feature values are rounded to three significant digits and +weighted LOESS is computed and plotted, where the weights are the numbers of data points at each rounded value. -Note: SHAP contributions are shown on the scale of model margin. E.g., for a logistic binomial objective, -the margin is prediction before a sigmoidal transform into probability-like values. +Note: SHAP contributions are on the scale of the model margin. +E.g., for a logistic binomial objective, the margin is on log-odds scale. Also, since SHAP stands for "SHapley Additive exPlanation" (model prediction = sum of SHAP contributions for all features + bias), depending on the objective used, transforming SHAP contributions for a feature from the marginal to the prediction space is not necessarily @@ -121,44 +127,99 @@ a meaningful thing to do. } \examples{ -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') +data(agaricus.train, package = "xgboost") +data(agaricus.test, package = "xgboost") ## Keep the number of threads to 1 for examples nthread <- 1 data.table::setDTthreads(nthread) nrounds <- 20 -bst <- xgboost(agaricus.train$data, agaricus.train$label, nrounds = nrounds, - eta = 0.1, max_depth = 3, subsample = .5, - method = "hist", objective = "binary:logistic", nthread = nthread, verbose = 0) +bst <- xgboost( + agaricus.train$data, + agaricus.train$label, + nrounds = nrounds, + eta = 0.1, + max_depth = 3, + subsample = 0.5, + objective = "binary:logistic", + nthread = nthread, + verbose = 0 +) xgb.plot.shap(agaricus.test$data, model = bst, features = "odor=none") + contr <- predict(bst, agaricus.test$data, predcontrib = TRUE) xgb.plot.shap(agaricus.test$data, contr, model = bst, top_n = 12, n_col = 3) -xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) # Summary plot -# multiclass example - plots for each class separately: +# Summary plot +xgb.ggplot.shap.summary(agaricus.test$data, contr, model = bst, top_n = 12) + +# Multiclass example - plots for each class separately: nclass <- 3 x <- as.matrix(iris[, -5]) set.seed(123) is.na(x[sample(nrow(x) * 4, 30)]) <- TRUE # introduce some missing values -mbst <- xgboost(data = x, label = as.numeric(iris$Species) - 1, nrounds = nrounds, - max_depth = 2, eta = 0.3, subsample = .5, nthread = nthread, - objective = "multi:softprob", num_class = nclass, verbose = 0) -trees0 <- seq(from=0, by=nclass, length.out=nrounds) + +mbst <- xgboost( + data = x, + label = as.numeric(iris$Species) - 1, + nrounds = nrounds, + max_depth = 2, + eta = 0.3, + subsample = 0.5, + nthread = nthread, + objective = "multi:softprob", + num_class = nclass, + verbose = 0 +) +trees0 <- seq(from = 0, by = nclass, length.out = nrounds) col <- rgb(0, 0, 1, 0.5) -xgb.plot.shap(x, model = mbst, trees = trees0, target_class = 0, top_n = 4, - n_col = 2, col = col, pch = 16, pch_NA = 17) -xgb.plot.shap(x, model = mbst, trees = trees0 + 1, target_class = 1, top_n = 4, - n_col = 2, col = col, pch = 16, pch_NA = 17) -xgb.plot.shap(x, model = mbst, trees = trees0 + 2, target_class = 2, top_n = 4, - n_col = 2, col = col, pch = 16, pch_NA = 17) -xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) # Summary plot +xgb.plot.shap( + x, + model = mbst, + trees = trees0, + target_class = 0, + top_n = 4, + n_col = 2, + col = col, + pch = 16, + pch_NA = 17 +) + +xgb.plot.shap( + x, + model = mbst, + trees = trees0 + 1, + target_class = 1, + top_n = 4, + n_col = 2, + col = col, + pch = 16, + pch_NA = 17 +) + +xgb.plot.shap( + x, + model = mbst, + trees = trees0 + 2, + target_class = 2, + top_n = 4, + n_col = 2, + col = col, + pch = 16, + pch_NA = 17 +) + +# Summary plot +xgb.ggplot.shap.summary(x, model = mbst, target_class = 0, top_n = 4) } \references{ -Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} - -Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", \url{https://arxiv.org/abs/1706.06060} +\enumerate{ +\item Scott M. Lundberg, Su-In Lee, "A Unified Approach to Interpreting Model Predictions", +NIPS Proceedings 2017, \url{https://arxiv.org/abs/1705.07874} +\item Scott M. Lundberg, Su-In Lee, "Consistent feature attribution for tree ensembles", +\url{https://arxiv.org/abs/1706.06060} +} } diff --git a/R-package/man/xgb.plot.shap.summary.Rd b/R-package/man/xgb.plot.shap.summary.Rd index 910119e6f..b0ad20dd7 100644 --- a/R-package/man/xgb.plot.shap.summary.Rd +++ b/R-package/man/xgb.plot.shap.summary.Rd @@ -3,7 +3,7 @@ \name{xgb.ggplot.shap.summary} \alias{xgb.ggplot.shap.summary} \alias{xgb.plot.shap.summary} -\title{SHAP contribution dependency summary plot} +\title{SHAP summary plot} \usage{ xgb.ggplot.shap.summary( data, @@ -30,49 +30,54 @@ xgb.plot.shap.summary( ) } \arguments{ -\item{data}{data as a \code{matrix} or \code{dgCMatrix}.} +\item{data}{The data to explain as a \code{matrix} or \code{dgCMatrix}.} -\item{shap_contrib}{a matrix of SHAP contributions that was computed earlier for the above -\code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}.} +\item{shap_contrib}{Matrix of SHAP contributions of \code{data}. +The default (\code{NULL}) computes it from \code{model} and \code{data}.} -\item{features}{a vector of either column indices or of feature names to plot. When it is NULL, -feature importance is calculated, and \code{top_n} high ranked features are taken.} +\item{features}{Vector of column indices or feature names to plot. +When \code{NULL} (default), the \code{top_n} most important features are selected +by \code{\link[=xgb.importance]{xgb.importance()}}.} -\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} +\item{top_n}{How many of the most important features (<= 100) should be selected? +By default 1 for SHAP dependence and 10 for SHAP summary). +Only used when \code{features = NULL}.} -\item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -or \code{features} is missing.} +\item{model}{An \code{xgb.Booster} model. Only required when \code{shap_contrib = NULL} or +\code{features = NULL}.} -\item{trees}{passed to \code{\link{xgb.importance}} when \code{features = NULL}.} +\item{trees}{Passed to \code{\link[=xgb.importance]{xgb.importance()}} when \code{features = NULL}.} -\item{target_class}{is only relevant for multiclass models. When it is set to a 0-based class index, -only SHAP contributions for that specific class are used. -If it is not set, SHAP importances are averaged over all classes.} +\item{target_class}{Only relevant for multiclass models. The default (\code{NULL}) +averages the SHAP values over all classes. Pass a (0-based) class index +to show only SHAP values of that class.} -\item{approxcontrib}{passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}.} +\item{approxcontrib}{Passed to \code{predict()} when \code{shap_contrib = NULL}.} -\item{subsample}{a random fraction of data points to use for plotting. When it is NULL, -it is set so that up to 100K data points are used.} +\item{subsample}{Fraction of data points randomly picked for plotting. +The default (\code{NULL}) will use up to 100k data points.} } \value{ A \code{ggplot2} object. } \description{ -Compare SHAP contributions of different features. +Visualizes SHAP contributions of different features. } \details{ -A point plot (each point representing one sample from \code{data}) is +A point plot (each point representing one observation from \code{data}) is produced for each feature, with the points plotted on the SHAP value axis. -Each point (observation) is coloured based on its feature value. The plot -hence allows us to see which features have a negative / positive contribution +Each point (observation) is coloured based on its feature value. + +The plot allows to see which features have a negative / positive contribution on the model prediction, and whether the contribution is different for larger -or smaller values of the feature. We effectively try to replicate the -\code{summary_plot} function from \url{https://github.com/shap/shap}. +or smaller values of the feature. Inspired by the summary plot of +\url{https://github.com/shap/shap}. } \examples{ -# See \code{\link{xgb.plot.shap}}. +# See examples in xgb.plot.shap() + } \seealso{ -\code{\link{xgb.plot.shap}}, \code{\link{xgb.ggplot.shap.summary}}, -\url{https://github.com/shap/shap} +\code{\link[=xgb.plot.shap]{xgb.plot.shap()}}, \code{\link[=xgb.ggplot.shap.summary]{xgb.ggplot.shap.summary()}}, +and the Python library \url{https://github.com/shap/shap}. } diff --git a/R-package/man/xgb.plot.tree.Rd b/R-package/man/xgb.plot.tree.Rd index 224e393ce..7571487eb 100644 --- a/R-package/man/xgb.plot.tree.Rd +++ b/R-package/man/xgb.plot.tree.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/xgb.plot.tree.R \name{xgb.plot.tree} \alias{xgb.plot.tree} -\title{Plot a boosted tree model} +\title{Plot boosted trees} \usage{ xgb.plot.tree( feature_names = NULL, @@ -16,76 +16,89 @@ xgb.plot.tree( ) } \arguments{ -\item{feature_names}{names of each feature as a \code{character} vector.} +\item{feature_names}{Character vector used to overwrite the feature names +of the model. The default (\code{NULL}) uses the original feature names.} -\item{model}{produced by the \code{xgb.train} function.} +\item{model}{Object of class \code{xgb.Booster}.} -\item{trees}{an integer vector of tree indices that should be visualized. -If set to \code{NULL}, all trees of the model are included. -IMPORTANT: the tree index in xgboost model is zero-based -(e.g., use \code{trees = 0:2} for the first 3 trees in a model).} +\item{trees}{An integer vector of tree indices that should be used. +The default (\code{NULL}) uses all trees. +Useful, e.g., in multiclass classification to get only +the trees of one class. \emph{Important}: the tree index in XGBoost models +is zero-based (e.g., use \code{trees = 0:2} for the first three trees).} -\item{plot_width}{the width of the diagram in pixels.} +\item{plot_width, plot_height}{Width and height of the graph in pixels. +The values are passed to \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}.} -\item{plot_height}{the height of the diagram in pixels.} - -\item{render}{a logical flag for whether the graph should be rendered (see Value).} +\item{render}{Should the graph be rendered or not? The default is \code{TRUE}.} \item{show_node_id}{a logical flag for whether to show node id's in the graph.} \item{...}{currently not used.} } \value{ -When \code{render = TRUE}: -returns a rendered graph object which is an \code{htmlwidget} of class \code{grViz}. -Similar to ggplot objects, it needs to be printed to see it when not running from command line. - -When \code{render = FALSE}: -silently returns a graph object which is of DiagrammeR's class \code{dgr_graph}. +The value depends on the \code{render} parameter: +\itemize{ +\item If \code{render = TRUE} (default): Rendered graph object which is an htmlwidget of +class \code{grViz}. Similar to "ggplot" objects, it needs to be printed when not +running from the command line. +\item If \code{render = FALSE}: Graph object which is of DiagrammeR's class \code{dgr_graph}. This could be useful if one wants to modify some of the graph attributes -before rendering the graph with \code{\link[DiagrammeR]{render_graph}}. +before rendering the graph with \code{\link[DiagrammeR:render_graph]{DiagrammeR::render_graph()}}. +} } \description{ Read a tree model text dump and plot the model. } \details{ -The content of each node is organised that way: - +The content of each node is visualized like this: \itemize{ -\item Feature name. -\item \code{Cover}: The sum of second order gradient of training data classified to the leaf. -If it is square loss, this simply corresponds to the number of instances seen by a split -or collected by a leaf during training. -The deeper in the tree a node is, the lower this metric will be. -\item \code{Gain} (for split nodes): the information gain metric of a split +\item \emph{Feature name}. +\item \emph{Cover:} The sum of second order gradients of training data. +For the squared loss, this simply corresponds to the number of instances in the node. +The deeper in the tree, the lower the value. +\item \emph{Gain} (for split nodes): Information gain metric of a split (corresponds to the importance of the node in the model). -\item \code{Value} (for leafs): the margin value that the leaf may contribute to prediction. +\item \emph{Value} (for leaves): Margin value that the leaf may contribute to the prediction. } -The tree root nodes also indicate the Tree index (0-based). + +The tree root nodes also indicate the tree index (0-based). The "Yes" branches are marked by the "< split_value" label. -The branches that also used for missing values are marked as bold +The branches also used for missing values are marked as bold (as in "carrying extra capacity"). -This function uses \href{https://www.graphviz.org/}{GraphViz} as a backend of DiagrammeR. +This function uses \href{https://www.graphviz.org/}{GraphViz} as DiagrammeR backend. } \examples{ -data(agaricus.train, package='xgboost') +data(agaricus.train, package = "xgboost") + +bst <- xgboost( + data = agaricus.train$data, + label = agaricus.train$label, + max_depth = 3, + eta = 1, + nthread = 2, + nrounds = 2, + objective = "binary:logistic" +) -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 3, - eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") # plot all the trees xgb.plot.tree(model = bst) + # plot only the first tree and display the node ID: xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) \dontrun{ # Below is an example of how to save this plot to a file. -# Note that for `export_graph` to work, the DiagrammeRsvg and rsvg packages must also be installed. +# Note that for export_graph() to work, the {DiagrammeRsvg} +# and {rsvg} packages must also be installed. + library(DiagrammeR) -gr <- xgb.plot.tree(model=bst, trees=0:1, render=FALSE) -export_graph(gr, 'tree.pdf', width=1500, height=1900) -export_graph(gr, 'tree.png', width=1500, height=1900) + +gr <- xgb.plot.tree(model = bst, trees = 0:1, render = FALSE) +export_graph(gr, "tree.pdf", width = 1500, height = 1900) +export_graph(gr, "tree.png", width = 1500, height = 1900) } } diff --git a/R-package/man/xgb.shap.data.Rd b/R-package/man/xgb.shap.data.Rd deleted file mode 100644 index 6c4336cde..000000000 --- a/R-package/man/xgb.shap.data.Rd +++ /dev/null @@ -1,55 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.plot.shap.R -\name{xgb.shap.data} -\alias{xgb.shap.data} -\title{Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. -Internal utility function.} -\usage{ -xgb.shap.data( - data, - shap_contrib = NULL, - features = NULL, - top_n = 1, - model = NULL, - trees = NULL, - target_class = NULL, - approxcontrib = FALSE, - subsample = NULL, - max_observations = 1e+05 -) -} -\arguments{ -\item{data}{data as a \code{matrix} or \code{dgCMatrix}.} - -\item{shap_contrib}{a matrix of SHAP contributions that was computed earlier for the above -\code{data}. When it is NULL, it is computed internally using \code{model} and \code{data}.} - -\item{features}{a vector of either column indices or of feature names to plot. When it is NULL, -feature importance is calculated, and \code{top_n} high ranked features are taken.} - -\item{top_n}{when \code{features} is NULL, top_n \verb{[1, 100]} most important features in a model are taken.} - -\item{model}{an \code{xgb.Booster} model. It has to be provided when either \code{shap_contrib} -or \code{features} is missing.} - -\item{trees}{passed to \code{\link{xgb.importance}} when \code{features = NULL}.} - -\item{target_class}{is only relevant for multiclass models. When it is set to a 0-based class index, -only SHAP contributions for that specific class are used. -If it is not set, SHAP importances are averaged over all classes.} - -\item{approxcontrib}{passed to \code{\link{predict.xgb.Booster}} when \code{shap_contrib = NULL}.} - -\item{subsample}{a random fraction of data points to use for plotting. When it is NULL, -it is set so that up to 100K data points are used.} -} -\value{ -A list containing: 'data', a matrix containing sample observations -and their feature values; 'shap_contrib', a matrix containing the SHAP contribution -values for these observations. -} -\description{ -Prepare data for SHAP plots. To be used in xgb.plot.shap, xgb.plot.shap.summary, etc. -Internal utility function. -} -\keyword{internal} From a197899161fa70e681101de4232745fdfe737804 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 26 Dec 2023 13:29:55 +0100 Subject: [PATCH 064/109] [R] avoid leaking exception objects (#9916) --- R-package/src/xgboost_R.cc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 79b9e6037..fb05c33b4 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -161,15 +161,13 @@ 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[256]; +char cpp_ex_msg[512]; /*! * \brief macro to annotate end of api */ #define R_API_END() \ - } catch(dmlc::Error& e) { \ - Rf_error("%s", e.what()); \ } catch(std::exception &e) { \ - std::strncpy(cpp_ex_msg, e.what(), 256); \ + std::strncpy(cpp_ex_msg, e.what(), 512); \ goto throw_cpp_ex_as_R_err; \ } \ if (false) { \ From a7226c02223246be78a59c3a4e8c32d1c68c1ff9 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 28 Dec 2023 22:45:13 +0800 Subject: [PATCH 065/109] Fix feature names with special characters. (#9923) --- src/common/common.h | 12 +++ src/tree/tree_model.cc | 118 +++++++++++++++--------------- tests/cpp/tree/test_tree_model.cc | 2 +- tests/python/test_basic_models.py | 24 ++++-- 4 files changed, 88 insertions(+), 68 deletions(-) diff --git a/src/common/common.h b/src/common/common.h index 4b20ce7c2..950dee521 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -66,8 +66,20 @@ inline std::vector Split(const std::string& s, char delim) { return ret; } +/** + * @brief Add escapes for a UTF-8 string. + */ void EscapeU8(std::string const &string, std::string *p_buffer); +/** + * @brief Add escapes for a UTF-8 string with newly created buffer as return. + */ +inline std::string EscapeU8(std::string const &str) { + std::string buffer; + EscapeU8(str, &buffer); + return buffer; +} + template XGBOOST_DEVICE T Max(T a, T b) { return a < b ? b : a; diff --git a/src/tree/tree_model.cc b/src/tree/tree_model.cc index d37be14b8..f18b51926 100644 --- a/src/tree/tree_model.cc +++ b/src/tree/tree_model.cc @@ -1,5 +1,5 @@ /** - * Copyright 2015-2023 by Contributors + * Copyright 2015-2023, XGBoost Contributors * \file tree_model.cc * \brief model structure for tree */ @@ -15,9 +15,9 @@ #include #include "../common/categorical.h" -#include "../common/common.h" +#include "../common/common.h" // for EscapeU8 #include "../predictor/predict_fn.h" -#include "io_utils.h" // GetElem +#include "io_utils.h" // for GetElem #include "param.h" #include "xgboost/base.h" #include "xgboost/data.h" @@ -207,8 +207,9 @@ TreeGenerator* TreeGenerator::Create(std::string const& attrs, FeatureMap const& __make_ ## TreeGenReg ## _ ## UniqueId ## __ = \ ::dmlc::Registry< ::xgboost::TreeGenReg>::Get()->__REGISTER__(Name) -std::vector GetSplitCategories(RegTree const &tree, int32_t nidx) { - auto const &csr = tree.GetCategoriesMatrix(); +namespace { +std::vector GetSplitCategories(RegTree const& tree, int32_t nidx) { + auto const& csr = tree.GetCategoriesMatrix(); auto seg = csr.node_ptr[nidx]; auto split = common::KCatBitField{csr.categories.subspan(seg.beg, seg.size)}; @@ -221,7 +222,7 @@ std::vector GetSplitCategories(RegTree const &tree, int32_t nidx) { return cats; } -std::string PrintCatsAsSet(std::vector const &cats) { +std::string PrintCatsAsSet(std::vector const& cats) { std::stringstream ss; ss << "{"; for (size_t i = 0; i < cats.size(); ++i) { @@ -234,6 +235,15 @@ std::string PrintCatsAsSet(std::vector const &cats) { return ss.str(); } +std::string GetFeatureName(FeatureMap const& fmap, bst_feature_t split_index) { + CHECK_LE(fmap.Size(), std::numeric_limits::max()); + auto fname = split_index < static_cast(fmap.Size()) + ? fmap.Name(split_index) + : ('f' + std::to_string(split_index)); + return common::EscapeU8(fname); +} +} // anonymous namespace + class TextGenerator : public TreeGenerator { using SuperT = TreeGenerator; @@ -263,7 +273,7 @@ class TextGenerator : public TreeGenerator { std::string result = SuperT::Match( kIndicatorTemplate, {{"{nid}", std::to_string(nid)}, - {"{fname}", fmap_.Name(split_index)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, {"{yes}", std::to_string(nyes)}, {"{no}", std::to_string(tree[nid].DefaultChild())}}); return result; @@ -277,8 +287,7 @@ class TextGenerator : public TreeGenerator { template_str, {{"{tabs}", SuperT::Tabs(depth)}, {"{nid}", std::to_string(nid)}, - {"{fname}", split_index < fmap_.Size() ? fmap_.Name(split_index) : - std::to_string(split_index)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, {"{cond}", cond}, {"{left}", std::to_string(tree[nid].LeftChild())}, {"{right}", std::to_string(tree[nid].RightChild())}, @@ -308,7 +317,7 @@ class TextGenerator : public TreeGenerator { std::string PlainNode(RegTree const& tree, int32_t nid, uint32_t depth) const override { auto cond = tree[nid].SplitCond(); static std::string const kNodeTemplate = - "{tabs}{nid}:[f{fname}<{cond}] yes={left},no={right},missing={missing}"; + "{tabs}{nid}:[{fname}<{cond}] yes={left},no={right},missing={missing}"; return SplitNodeImpl(tree, nid, kNodeTemplate, SuperT::ToStr(cond), depth); } @@ -376,7 +385,7 @@ class JsonGenerator : public TreeGenerator { return result; } - std::string LeafNode(RegTree const& tree, int32_t nid, uint32_t) const override { + std::string LeafNode(RegTree const& tree, bst_node_t nid, uint32_t) const override { static std::string const kLeafTemplate = R"L({ "nodeid": {nid}, "leaf": {leaf} {stat}})L"; static std::string const kStatTemplate = @@ -392,26 +401,22 @@ class JsonGenerator : public TreeGenerator { return result; } - std::string Indicator(RegTree const& tree, int32_t nid, uint32_t depth) const override { + std::string Indicator(RegTree const& tree, bst_node_t nid, uint32_t depth) const override { int32_t nyes = tree[nid].DefaultLeft() ? tree[nid].RightChild() : tree[nid].LeftChild(); static std::string const kIndicatorTemplate = R"ID( "nodeid": {nid}, "depth": {depth}, "split": "{fname}", "yes": {yes}, "no": {no})ID"; auto split_index = tree[nid].SplitIndex(); - auto fname = fmap_.Name(split_index); - std::string qfname; // quoted - common::EscapeU8(fname, &qfname); - auto result = SuperT::Match( - kIndicatorTemplate, - {{"{nid}", std::to_string(nid)}, - {"{depth}", std::to_string(depth)}, - {"{fname}", qfname}, - {"{yes}", std::to_string(nyes)}, - {"{no}", std::to_string(tree[nid].DefaultChild())}}); + auto result = + SuperT::Match(kIndicatorTemplate, {{"{nid}", std::to_string(nid)}, + {"{depth}", std::to_string(depth)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{yes}", std::to_string(nyes)}, + {"{no}", std::to_string(tree[nid].DefaultChild())}}); return result; } - std::string Categorical(RegTree const& tree, int32_t nid, uint32_t depth) const override { + std::string Categorical(RegTree const& tree, bst_node_t nid, uint32_t depth) const override { auto cats = GetSplitCategories(tree, nid); static std::string const kCategoryTemplate = R"I( "nodeid": {nid}, "depth": {depth}, "split": "{fname}", )I" @@ -429,22 +434,17 @@ class JsonGenerator : public TreeGenerator { return results; } - std::string SplitNodeImpl(RegTree const &tree, int32_t nid, - std::string const &template_str, std::string cond, - uint32_t depth) const { + std::string SplitNodeImpl(RegTree const& tree, bst_node_t nid, std::string const& template_str, + std::string cond, uint32_t depth) const { auto split_index = tree[nid].SplitIndex(); - auto fname = split_index < fmap_.Size() ? fmap_.Name(split_index) : std::to_string(split_index); - std::string qfname; // quoted - common::EscapeU8(fname, &qfname); - std::string const result = SuperT::Match( - template_str, - {{"{nid}", std::to_string(nid)}, - {"{depth}", std::to_string(depth)}, - {"{fname}", qfname}, - {"{cond}", cond}, - {"{left}", std::to_string(tree[nid].LeftChild())}, - {"{right}", std::to_string(tree[nid].RightChild())}, - {"{missing}", std::to_string(tree[nid].DefaultChild())}}); + std::string const result = + SuperT::Match(template_str, {{"{nid}", std::to_string(nid)}, + {"{depth}", std::to_string(depth)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{cond}", cond}, + {"{left}", std::to_string(tree[nid].LeftChild())}, + {"{right}", std::to_string(tree[nid].RightChild())}, + {"{missing}", std::to_string(tree[nid].DefaultChild())}}); return result; } @@ -605,9 +605,8 @@ class GraphvizGenerator : public TreeGenerator { auto const& extra = kwargs["graph_attrs"]; static std::string const kGraphTemplate = " graph [ {key}=\"{value}\" ]\n"; for (auto const& kv : extra) { - param_.graph_attrs += SuperT::Match(kGraphTemplate, - {{"{key}", kv.first}, - {"{value}", kv.second}}); + param_.graph_attrs += + SuperT::Match(kGraphTemplate, {{"{key}", kv.first}, {"{value}", kv.second}}); } kwargs.erase("graph_attrs"); @@ -646,20 +645,18 @@ class GraphvizGenerator : public TreeGenerator { // Only indicator is different, so we combine all different node types into this // function. std::string PlainNode(RegTree const& tree, int32_t nid, uint32_t) const override { - auto split = tree[nid].SplitIndex(); + auto split_index = tree[nid].SplitIndex(); auto cond = tree[nid].SplitCond(); - static std::string const kNodeTemplate = - " {nid} [ label=\"{fname}{<}{cond}\" {params}]\n"; + static std::string const kNodeTemplate = " {nid} [ label=\"{fname}{<}{cond}\" {params}]\n"; - // Indicator only has fname. - bool has_less = (split >= fmap_.Size()) || fmap_.TypeOf(split) != FeatureMap::kIndicator; - std::string result = SuperT::Match(kNodeTemplate, { - {"{nid}", std::to_string(nid)}, - {"{fname}", split < fmap_.Size() ? fmap_.Name(split) : - 'f' + std::to_string(split)}, - {"{<}", has_less ? "<" : ""}, - {"{cond}", has_less ? SuperT::ToStr(cond) : ""}, - {"{params}", param_.condition_node_params}}); + bool has_less = + (split_index >= fmap_.Size()) || fmap_.TypeOf(split_index) != FeatureMap::kIndicator; + std::string result = + SuperT::Match(kNodeTemplate, {{"{nid}", std::to_string(nid)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{<}", has_less ? "<" : ""}, + {"{cond}", has_less ? SuperT::ToStr(cond) : ""}, + {"{params}", param_.condition_node_params}}); result += BuildEdge(tree, nid, tree[nid].LeftChild(), true); result += BuildEdge(tree, nid, tree[nid].RightChild(), false); @@ -672,14 +669,13 @@ class GraphvizGenerator : public TreeGenerator { " {nid} [ label=\"{fname}:{cond}\" {params}]\n"; auto cats = GetSplitCategories(tree, nid); auto cats_str = PrintCatsAsSet(cats); - auto split = tree[nid].SplitIndex(); - std::string result = SuperT::Match( - kLabelTemplate, - {{"{nid}", std::to_string(nid)}, - {"{fname}", split < fmap_.Size() ? fmap_.Name(split) - : 'f' + std::to_string(split)}, - {"{cond}", cats_str}, - {"{params}", param_.condition_node_params}}); + auto split_index = tree[nid].SplitIndex(); + + std::string result = + SuperT::Match(kLabelTemplate, {{"{nid}", std::to_string(nid)}, + {"{fname}", GetFeatureName(fmap_, split_index)}, + {"{cond}", cats_str}, + {"{params}", param_.condition_node_params}}); result += BuildEdge(tree, nid, tree[nid].LeftChild(), true); result += BuildEdge(tree, nid, tree[nid].RightChild(), false); diff --git a/tests/cpp/tree/test_tree_model.cc b/tests/cpp/tree/test_tree_model.cc index 44708ebd1..2dc1893dd 100644 --- a/tests/cpp/tree/test_tree_model.cc +++ b/tests/cpp/tree/test_tree_model.cc @@ -404,7 +404,7 @@ TEST(Tree, DumpText) { } ASSERT_EQ(n_conditions, 3ul); - ASSERT_NE(str.find("[f0<0]"), std::string::npos); + ASSERT_NE(str.find("[f0<0]"), std::string::npos) << str; ASSERT_NE(str.find("[f1<1]"), std::string::npos); ASSERT_NE(str.find("[f2<2]"), std::string::npos); diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index f0c80124d..45bef1f25 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -28,10 +28,11 @@ def json_model(model_path: str, parameters: dict) -> dict: if model_path.endswith("ubj"): import ubjson + with open(model_path, "rb") as ubjfd: model = ubjson.load(ubjfd) else: - with open(model_path, 'r') as fd: + with open(model_path, "r") as fd: model = json.load(fd) return model @@ -439,25 +440,34 @@ class TestModels: 'objective': 'multi:softmax'} validate_model(parameters) - def test_special_model_dump_characters(self): + def test_special_model_dump_characters(self) -> None: params = {"objective": "reg:squarederror", "max_depth": 3} - feature_names = ['"feature 0"', "\tfeature\n1", "feature 2"] + feature_names = ['"feature 0"', "\tfeature\n1", """feature "2"."""] X, y, w = tm.make_regression(n_samples=128, n_features=3, use_cupy=False) Xy = xgb.DMatrix(X, label=y, feature_names=feature_names) booster = xgb.train(params, Xy, num_boost_round=3) + json_dump = booster.get_dump(dump_format="json") assert len(json_dump) == 3 - def validate(obj: dict) -> None: + def validate_json(obj: dict) -> None: for k, v in obj.items(): if k == "split": assert v in feature_names elif isinstance(v, dict): - validate(v) + validate_json(v) for j_tree in json_dump: loaded = json.loads(j_tree) - validate(loaded) + validate_json(loaded) + + dot_dump = booster.get_dump(dump_format="dot") + for d in dot_dump: + assert d.find(r"feature \"2\"") != -1 + + text_dump = booster.get_dump(dump_format="text") + for d in text_dump: + assert d.find(r"feature \"2\"") != -1 def test_categorical_model_io(self): X, y = tm.make_categorical(256, 16, 71, False) @@ -485,6 +495,7 @@ class TestModels: @pytest.mark.skipif(**tm.no_sklearn()) def test_attributes(self): from sklearn.datasets import load_iris + X, y = load_iris(return_X_y=True) cls = xgb.XGBClassifier(n_estimators=2) cls.fit(X, y, early_stopping_rounds=1, eval_set=[(X, y)]) @@ -674,6 +685,7 @@ class TestModels: @pytest.mark.skipif(**tm.no_pandas()) def test_feature_info(self): import pandas as pd + rows = 100 cols = 10 X = rng.randn(rows, cols) From ef8bdaa047ebf6504245a08d7046c49528cc1a13 Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Fri, 29 Dec 2023 11:15:38 -0800 Subject: [PATCH 066/109] [CI] Update machine images (#9932) --- cmake/modules/FindLibR.cmake | 5 +++- doc/contrib/ci.rst | 7 ++--- tests/buildkite/build-containers.sh | 3 +- tests/buildkite/build-cpu-arm64.sh | 2 +- tests/buildkite/build-cpu.sh | 2 +- tests/buildkite/build-cuda-with-rmm.sh | 6 ++-- tests/buildkite/build-cuda.sh | 6 ++-- tests/buildkite/build-gpu-rpkg.sh | 2 +- tests/buildkite/build-jvm-doc.sh | 2 +- tests/buildkite/build-jvm-packages-gpu.sh | 2 +- tests/buildkite/build-jvm-packages.sh | 4 +-- tests/buildkite/deploy-jvm-packages.sh | 2 +- .../aws-stack-creator/create_stack.py | 2 +- .../aws-stack-creator/metadata.py | 28 +++++++++---------- .../linux-amd64-gpu-bootstrap.yml | 12 ++++---- .../windows-gpu-bootstrap.yml | 25 ++++++----------- tests/buildkite/run-clang-tidy.sh | 2 +- tests/buildkite/test-cpp-gpu.sh | 4 +-- tests/buildkite/test-cpp-mgpu.sh | 2 +- .../test-integration-jvm-packages.sh | 2 +- tests/buildkite/test-python-cpu-arm64.sh | 2 +- tests/buildkite/test-python-cpu.sh | 2 +- tests/buildkite/test-python-gpu.sh | 2 +- tests/ci_build/build_r_pkg_with_cuda_win64.sh | 6 ++-- tests/ci_build/ci_build.sh | 28 ++++++++++++------- tests/cpp/common/test_device_helpers.cu | 2 +- 26 files changed, 82 insertions(+), 80 deletions(-) diff --git a/cmake/modules/FindLibR.cmake b/cmake/modules/FindLibR.cmake index 1eb384238..c406ae0d9 100644 --- a/cmake/modules/FindLibR.cmake +++ b/cmake/modules/FindLibR.cmake @@ -65,7 +65,10 @@ function(create_rlib_for_msvc) execute_process(COMMAND ${DLLTOOL_EXE} "--input-def" "${CMAKE_CURRENT_BINARY_DIR}/R.def" - "--output-lib" "${CMAKE_CURRENT_BINARY_DIR}/R.lib") + "--output-lib" "${CMAKE_CURRENT_BINARY_DIR}/R.lib" + "--temp-prefix" "Rlibtemp" + COMMAND_ECHO STDOUT + COMMAND_ERROR_IS_FATAL ANY) endfunction() diff --git a/doc/contrib/ci.rst b/doc/contrib/ci.rst index 2db6f80bc..af9e65562 100644 --- a/doc/contrib/ci.rst +++ b/doc/contrib/ci.rst @@ -59,7 +59,7 @@ For your convenience, we provide the wrapper script ``tests/ci_build/ci_build.sh .. code-block:: bash - tests/ci_build/ci_build.sh --build-arg \ + tests/ci_build/ci_build.sh --use-gpus --build-arg \ ... where: @@ -68,8 +68,7 @@ where: container definition (Dockerfile) located at ``tests/ci_build/Dockerfile.``. For example, setting the container type to ``gpu`` will cause the script to load the Dockerfile ``tests/ci_build/Dockerfile.gpu``. -* ```` must be either ``docker`` or ``nvidia-docker``. Choose ``nvidia-docker`` - as long as you need to run any GPU code. +* Specify ``--use-gpus`` to run any GPU code. This flag will grant the container access to all NVIDIA GPUs in the base machine. Omit the flag if the access to GPUs is not necessary. * ```` is a build argument to be passed to Docker. Must be of form ``VAR=VALUE``. Example: ``--build-arg CUDA_VERSION_ARG=11.0``. You can pass multiple ``--build-arg``. * ```` is the command to run inside the Docker container. This can be more than one argument. @@ -83,7 +82,7 @@ arguments to Docker. For example: # Allocate extra space in /dev/shm to enable NCCL export CI_DOCKER_EXTRA_PARAMS_INIT='--shm-size=4g' # Run multi-GPU test suite - tests/ci_build/ci_build.sh gpu nvidia-docker --build-arg CUDA_VERSION_ARG=11.0 \ + tests/ci_build/ci_build.sh gpu --use-gpus --build-arg CUDA_VERSION_ARG=11.0 \ tests/ci_build/test_python.sh mgpu To pass multiple extra arguments: diff --git a/tests/buildkite/build-containers.sh b/tests/buildkite/build-containers.sh index f46e6ccd0..9aec33d1f 100755 --- a/tests/buildkite/build-containers.sh +++ b/tests/buildkite/build-containers.sh @@ -22,6 +22,7 @@ case "${container}" in gpu) BUILD_ARGS="$BUILD_ARGS --build-arg CUDA_VERSION_ARG=$CUDA_VERSION" + BUILD_ARGS="$BUILD_ARGS --build-arg NCCL_VERSION_ARG=$NCCL_VERSION" BUILD_ARGS="$BUILD_ARGS --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION" ;; @@ -43,4 +44,4 @@ case "${container}" in esac # Run a no-op command. This will simply build the container and push it to the private registry -tests/ci_build/ci_build.sh ${container} docker ${BUILD_ARGS} bash +tests/ci_build/ci_build.sh ${container} ${BUILD_ARGS} bash diff --git a/tests/buildkite/build-cpu-arm64.sh b/tests/buildkite/build-cpu-arm64.sh index fd00a7971..3bbc95472 100755 --- a/tests/buildkite/build-cpu-arm64.sh +++ b/tests/buildkite/build-cpu-arm64.sh @@ -8,7 +8,7 @@ echo "--- Build CPU code targeting ARM64" source tests/buildkite/conftest.sh -command_wrapper="tests/ci_build/ci_build.sh aarch64 docker" +command_wrapper="tests/ci_build/ci_build.sh aarch64" echo "--- Build libxgboost from the source" $command_wrapper tests/ci_build/build_via_cmake.sh --conda-env=aarch64_test \ diff --git a/tests/buildkite/build-cpu.sh b/tests/buildkite/build-cpu.sh index 73e88d8aa..11679d644 100755 --- a/tests/buildkite/build-cpu.sh +++ b/tests/buildkite/build-cpu.sh @@ -6,7 +6,7 @@ echo "--- Build CPU code" source tests/buildkite/conftest.sh -command_wrapper="tests/ci_build/ci_build.sh cpu docker" +command_wrapper="tests/ci_build/ci_build.sh cpu" $command_wrapper rm -fv dmlc-core/include/dmlc/build_config_default.h # This step is not necessary, but here we include it, to ensure that diff --git a/tests/buildkite/build-cuda-with-rmm.sh b/tests/buildkite/build-cuda-with-rmm.sh index 615608249..559bad8a7 100755 --- a/tests/buildkite/build-cuda-with-rmm.sh +++ b/tests/buildkite/build-cuda-with-rmm.sh @@ -15,7 +15,7 @@ else arch_flag="" fi -command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 docker --build-arg "` +command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 --build-arg "` `"CUDA_VERSION_ARG=$CUDA_VERSION --build-arg "` `"NCCL_VERSION_ARG=$NCCL_VERSION --build-arg "` `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION" @@ -40,13 +40,13 @@ $command_wrapper python tests/ci_build/rename_whl.py python-package/dist/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} echo "--- Audit binary wheel to ensure it's compliant with manylinux2014 standard" -tests/ci_build/ci_build.sh auditwheel_x86_64 docker auditwheel repair \ +tests/ci_build/ci_build.sh auditwheel_x86_64 auditwheel repair \ --plat ${WHEEL_TAG} python-package/dist/*.whl $command_wrapper python tests/ci_build/rename_whl.py wheelhouse/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} mv -v wheelhouse/*.whl python-package/dist/ # Make sure that libgomp.so is vendored in the wheel -tests/ci_build/ci_build.sh auditwheel_x86_64 docker bash -c \ +tests/ci_build/ci_build.sh auditwheel_x86_64 bash -c \ "unzip -l python-package/dist/*.whl | grep libgomp || exit -1" echo "--- Upload Python wheel" diff --git a/tests/buildkite/build-cuda.sh b/tests/buildkite/build-cuda.sh index 7bd3492a2..5abc5ca5a 100755 --- a/tests/buildkite/build-cuda.sh +++ b/tests/buildkite/build-cuda.sh @@ -15,7 +15,7 @@ else arch_flag="" fi -command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 docker --build-arg "` +command_wrapper="tests/ci_build/ci_build.sh gpu_build_centos7 --build-arg "` `"CUDA_VERSION_ARG=$CUDA_VERSION --build-arg "` `"NCCL_VERSION_ARG=$NCCL_VERSION --build-arg "` `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION" @@ -39,13 +39,13 @@ $command_wrapper python tests/ci_build/rename_whl.py python-package/dist/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} echo "--- Audit binary wheel to ensure it's compliant with manylinux2014 standard" -tests/ci_build/ci_build.sh auditwheel_x86_64 docker auditwheel repair \ +tests/ci_build/ci_build.sh auditwheel_x86_64 auditwheel repair \ --plat ${WHEEL_TAG} python-package/dist/*.whl $command_wrapper python tests/ci_build/rename_whl.py wheelhouse/*.whl \ ${BUILDKITE_COMMIT} ${WHEEL_TAG} mv -v wheelhouse/*.whl python-package/dist/ # Make sure that libgomp.so is vendored in the wheel -tests/ci_build/ci_build.sh auditwheel_x86_64 docker bash -c \ +tests/ci_build/ci_build.sh auditwheel_x86_64 bash -c \ "unzip -l python-package/dist/*.whl | grep libgomp || exit -1" echo "--- Upload Python wheel" diff --git a/tests/buildkite/build-gpu-rpkg.sh b/tests/buildkite/build-gpu-rpkg.sh index 585dc79ae..78a534615 100755 --- a/tests/buildkite/build-gpu-rpkg.sh +++ b/tests/buildkite/build-gpu-rpkg.sh @@ -6,7 +6,7 @@ source tests/buildkite/conftest.sh echo "--- Build XGBoost R package with CUDA" -tests/ci_build/ci_build.sh gpu_build_r_centos7 docker \ +tests/ci_build/ci_build.sh gpu_build_r_centos7 \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ --build-arg R_VERSION_ARG=${R_VERSION} \ tests/ci_build/build_r_pkg_with_cuda.sh \ diff --git a/tests/buildkite/build-jvm-doc.sh b/tests/buildkite/build-jvm-doc.sh index a2d658e48..d168eb8cc 100755 --- a/tests/buildkite/build-jvm-doc.sh +++ b/tests/buildkite/build-jvm-doc.sh @@ -5,7 +5,7 @@ set -euo pipefail source tests/buildkite/conftest.sh echo "--- Build JVM packages doc" -tests/ci_build/ci_build.sh jvm docker tests/ci_build/build_jvm_doc.sh ${BRANCH_NAME} +tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_doc.sh ${BRANCH_NAME} if [[ ($is_pull_request == 0) && ($is_release_branch == 1) ]] then echo "--- Upload JVM packages doc" diff --git a/tests/buildkite/build-jvm-packages-gpu.sh b/tests/buildkite/build-jvm-packages-gpu.sh index 6a9a29cb3..64be7cc0a 100755 --- a/tests/buildkite/build-jvm-packages-gpu.sh +++ b/tests/buildkite/build-jvm-packages-gpu.sh @@ -13,7 +13,7 @@ else arch_flag="" fi -tests/ci_build/ci_build.sh jvm_gpu_build nvidia-docker \ +tests/ci_build/ci_build.sh jvm_gpu_build --use-gpus \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ --build-arg NCCL_VERSION_ARG=${NCCL_VERSION} \ tests/ci_build/build_jvm_packages.sh \ diff --git a/tests/buildkite/build-jvm-packages.sh b/tests/buildkite/build-jvm-packages.sh index 33cfffe71..12393c561 100755 --- a/tests/buildkite/build-jvm-packages.sh +++ b/tests/buildkite/build-jvm-packages.sh @@ -5,13 +5,13 @@ set -euo pipefail source tests/buildkite/conftest.sh echo "--- Build XGBoost JVM packages scala 2.12" -tests/ci_build/ci_build.sh jvm docker tests/ci_build/build_jvm_packages.sh \ +tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_packages.sh \ ${SPARK_VERSION} echo "--- Build XGBoost JVM packages scala 2.13" -tests/ci_build/ci_build.sh jvm docker tests/ci_build/build_jvm_packages.sh \ +tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_packages.sh \ ${SPARK_VERSION} "" "" "true" echo "--- Stash XGBoost4J JARs" diff --git a/tests/buildkite/deploy-jvm-packages.sh b/tests/buildkite/deploy-jvm-packages.sh index a3410b294..812a6c5ca 100755 --- a/tests/buildkite/deploy-jvm-packages.sh +++ b/tests/buildkite/deploy-jvm-packages.sh @@ -7,7 +7,7 @@ source tests/buildkite/conftest.sh if [[ ($is_pull_request == 0) && ($is_release_branch == 1) ]] then echo "--- Deploy JVM packages to xgboost-maven-repo S3 repo" - tests/ci_build/ci_build.sh jvm_gpu_build docker \ + tests/ci_build/ci_build.sh jvm_gpu_build \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ --build-arg NCCL_VERSION_ARG=${NCCL_VERSION} \ tests/ci_build/deploy_jvm_packages.sh ${SPARK_VERSION} diff --git a/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py b/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py index 4277eed53..8f8db348a 100644 --- a/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py +++ b/tests/buildkite/infrastructure/aws-stack-creator/create_stack.py @@ -63,7 +63,7 @@ def format_params(args, *, stack_id, agent_iam_policy): params["BuildkiteAgentToken"] = args.agent_token params["VpcId"] = default_vpc.id params["Subnets"] = ",".join(subnets) - params["ManagedPolicyARN"] = agent_iam_policy + params["ManagedPolicyARNs"] = agent_iam_policy params.update(COMMON_STACK_PARAMS) return [{"ParameterKey": k, "ParameterValue": v} for k, v in params.items()] diff --git a/tests/buildkite/infrastructure/aws-stack-creator/metadata.py b/tests/buildkite/infrastructure/aws-stack-creator/metadata.py index 36d8595a6..3b56a2d8c 100644 --- a/tests/buildkite/infrastructure/aws-stack-creator/metadata.py +++ b/tests/buildkite/infrastructure/aws-stack-creator/metadata.py @@ -1,34 +1,34 @@ AMI_ID = { # Managed by XGBoost team "linux-amd64-gpu": { - "us-west-2": "ami-094271bed4788ddb5", + "us-west-2": "ami-08c3bc1dd5ec8bc5c", }, "linux-amd64-mgpu": { - "us-west-2": "ami-094271bed4788ddb5", + "us-west-2": "ami-08c3bc1dd5ec8bc5c", }, "windows-gpu": { - "us-west-2": "ami-0839681594a1d7627", + "us-west-2": "ami-03c7f2156f93b22a7", }, "windows-cpu": { - "us-west-2": "ami-0839681594a1d7627", + "us-west-2": "ami-03c7f2156f93b22a7", }, # Managed by BuildKite # from https://s3.amazonaws.com/buildkite-aws-stack/latest/aws-stack.yml "linux-amd64-cpu": { - "us-west-2": "ami-00f2127550cf03658", + "us-west-2": "ami-015e64acb52b3e595", }, "pipeline-loader": { - "us-west-2": "ami-00f2127550cf03658", + "us-west-2": "ami-015e64acb52b3e595", }, "linux-arm64-cpu": { - "us-west-2": "ami-0c5789068f4a2d1b5", + "us-west-2": "ami-0884e9c23a2fa98d0", }, } STACK_PARAMS = { "linux-amd64-gpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "g4dn.xlarge", + "InstanceTypes": "g4dn.xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "8", @@ -38,7 +38,7 @@ STACK_PARAMS = { }, "linux-amd64-mgpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "g4dn.12xlarge", + "InstanceTypes": "g4dn.12xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "1", @@ -48,7 +48,7 @@ STACK_PARAMS = { }, "windows-gpu": { "InstanceOperatingSystem": "windows", - "InstanceType": "g4dn.2xlarge", + "InstanceTypes": "g4dn.2xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "2", @@ -58,7 +58,7 @@ STACK_PARAMS = { }, "windows-cpu": { "InstanceOperatingSystem": "windows", - "InstanceType": "c5a.2xlarge", + "InstanceTypes": "c5a.2xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "2", @@ -68,7 +68,7 @@ STACK_PARAMS = { }, "linux-amd64-cpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "c5a.4xlarge", + "InstanceTypes": "c5a.4xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "16", @@ -78,7 +78,7 @@ STACK_PARAMS = { }, "pipeline-loader": { "InstanceOperatingSystem": "linux", - "InstanceType": "t3a.micro", + "InstanceTypes": "t3a.micro", "AgentsPerInstance": "1", "MinSize": "2", "MaxSize": "2", @@ -88,7 +88,7 @@ STACK_PARAMS = { }, "linux-arm64-cpu": { "InstanceOperatingSystem": "linux", - "InstanceType": "c6g.4xlarge", + "InstanceTypes": "c6g.4xlarge", "AgentsPerInstance": "1", "MinSize": "0", "MaxSize": "8", diff --git a/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml b/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml index a5c82a7fa..88403911c 100644 --- a/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml +++ b/tests/buildkite/infrastructure/worker-image-pipeline/linux-amd64-gpu-bootstrap.yml @@ -12,15 +12,13 @@ phases: - | yum groupinstall -y "Development tools" yum install -y kernel-devel-$(uname -r) + dnf install -y kernel-modules-extra aws s3 cp --recursive s3://ec2-linux-nvidia-drivers/latest/ . chmod +x NVIDIA-Linux-x86_64*.run - CC=/usr/bin/gcc10-cc ./NVIDIA-Linux-x86_64*.run --silent + ./NVIDIA-Linux-x86_64*.run --silent - amazon-linux-extras install docker - systemctl --now enable docker - distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \ - && curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.repo \ - | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo + curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | tee /etc/yum.repos.d/nvidia-container-toolkit.repo + yum install -y nvidia-container-toolkit yum clean expire-cache - yum install -y nvidia-docker2 + nvidia-ctk runtime configure --runtime=docker systemctl restart docker diff --git a/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml b/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml index 03fb105a7..e4d212fda 100644 --- a/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml +++ b/tests/buildkite/infrastructure/worker-image-pipeline/windows-gpu-bootstrap.yml @@ -15,9 +15,9 @@ phases: choco --version choco feature enable -n=allowGlobalConfirmation - # CMake 3.25 - Write-Host '>>> Installing CMake 3.25...' - choco install cmake --version 3.25.2 --installargs "ADD_CMAKE_TO_PATH=System" + # CMake 3.27 + Write-Host '>>> Installing CMake 3.27...' + choco install cmake --version 3.27.9 --installargs "ADD_CMAKE_TO_PATH=System" if ($LASTEXITCODE -ne 0) { throw "Last command failed" } # Notepad++ @@ -25,15 +25,14 @@ phases: choco install notepadplusplus if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - # Miniconda - Write-Host '>>> Installing Miniconda...' - choco install miniconda3 /RegisterPython:1 /D:C:\tools\miniconda3 - C:\tools\miniconda3\Scripts\conda.exe init --user --system + # Mambaforge + Write-Host '>>> Installing Mambaforge...' + choco install mambaforge /RegisterPython:1 /D:C:\tools\mambaforge + C:\tools\mambaforge\Scripts\conda.exe init --user --system if ($LASTEXITCODE -ne 0) { throw "Last command failed" } . "C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1" if ($LASTEXITCODE -ne 0) { throw "Last command failed" } conda config --set auto_activate_base false - conda config --prepend channels conda-forge # Install Java 11 Write-Host '>>> Installing Java 11...' @@ -59,15 +58,9 @@ phases: choco install cuda --version=11.8.0.52206 if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - # Install Python packages - Write-Host '>>> Installing Python packages...' - conda activate - conda install -y mamba - if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - # Install R Write-Host '>>> Installing R...' - choco install r.project --version=3.6.3 + choco install r.project --version=4.3.2 if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - choco install rtools --version=3.5.0.4 + choco install rtools --version=4.3.5550 if ($LASTEXITCODE -ne 0) { throw "Last command failed" } diff --git a/tests/buildkite/run-clang-tidy.sh b/tests/buildkite/run-clang-tidy.sh index 1a664f568..95ff010c2 100755 --- a/tests/buildkite/run-clang-tidy.sh +++ b/tests/buildkite/run-clang-tidy.sh @@ -6,6 +6,6 @@ echo "--- Run clang-tidy" source tests/buildkite/conftest.sh -tests/ci_build/ci_build.sh clang_tidy docker \ +tests/ci_build/ci_build.sh clang_tidy \ --build-arg CUDA_VERSION_ARG=${CUDA_VERSION} \ python3 tests/ci_build/tidy.py --cuda-archs 75 diff --git a/tests/buildkite/test-cpp-gpu.sh b/tests/buildkite/test-cpp-gpu.sh index 36f54cd3d..d7197db2e 100755 --- a/tests/buildkite/test-cpp-gpu.sh +++ b/tests/buildkite/test-cpp-gpu.sh @@ -7,7 +7,7 @@ source tests/buildkite/conftest.sh echo "--- Run Google Tests with CUDA, using a GPU" buildkite-agent artifact download "build/testxgboost" . --step build-cuda chmod +x build/testxgboost -tests/ci_build/ci_build.sh gpu nvidia-docker \ +tests/ci_build/ci_build.sh gpu --use-gpus \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ @@ -17,7 +17,7 @@ echo "--- Run Google Tests with CUDA, using a GPU, RMM enabled" rm -rfv build/ buildkite-agent artifact download "build/testxgboost" . --step build-cuda-with-rmm chmod +x build/testxgboost -tests/ci_build/ci_build.sh gpu nvidia-docker \ +tests/ci_build/ci_build.sh gpu --use-gpus \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ diff --git a/tests/buildkite/test-cpp-mgpu.sh b/tests/buildkite/test-cpp-mgpu.sh index 2aac47407..65614b191 100755 --- a/tests/buildkite/test-cpp-mgpu.sh +++ b/tests/buildkite/test-cpp-mgpu.sh @@ -10,7 +10,7 @@ export CI_DOCKER_EXTRA_PARAMS_INIT='--shm-size=4g' echo "--- Run Google Tests with CUDA, using multiple GPUs" buildkite-agent artifact download "build/testxgboost" . --step build-cuda chmod +x build/testxgboost -tests/ci_build/ci_build.sh gpu nvidia-docker \ +tests/ci_build/ci_build.sh gpu --use-gpus \ --build-arg CUDA_VERSION_ARG=$CUDA_VERSION \ --build-arg RAPIDS_VERSION_ARG=$RAPIDS_VERSION \ --build-arg NCCL_VERSION_ARG=$NCCL_VERSION \ diff --git a/tests/buildkite/test-integration-jvm-packages.sh b/tests/buildkite/test-integration-jvm-packages.sh index 9f477720a..51f74afe9 100755 --- a/tests/buildkite/test-integration-jvm-packages.sh +++ b/tests/buildkite/test-integration-jvm-packages.sh @@ -9,5 +9,5 @@ buildkite-agent artifact download "jvm-packages/xgboost4j/target/*.jar" . --step buildkite-agent artifact download "jvm-packages/xgboost4j-spark/target/*.jar" . --step build-jvm-packages buildkite-agent artifact download "jvm-packages/xgboost4j-example/target/*.jar" . --step build-jvm-packages export CI_DOCKER_EXTRA_PARAMS_INIT='-e RUN_INTEGRATION_TEST=1' -tests/ci_build/ci_build.sh jvm_cross docker --build-arg JDK_VERSION=${JDK_VERSION} \ +tests/ci_build/ci_build.sh jvm_cross --build-arg JDK_VERSION=${JDK_VERSION} \ --build-arg SPARK_VERSION=${SPARK_VERSION} tests/ci_build/test_jvm_cross.sh diff --git a/tests/buildkite/test-python-cpu-arm64.sh b/tests/buildkite/test-python-cpu-arm64.sh index ed1ad101b..68a428034 100755 --- a/tests/buildkite/test-python-cpu-arm64.sh +++ b/tests/buildkite/test-python-cpu-arm64.sh @@ -8,4 +8,4 @@ echo "--- Test Python CPU ARM64" buildkite-agent artifact download "python-package/dist/*.whl" . --step build-cpu-arm64 buildkite-agent artifact download "xgboost" . --step build-cpu-arm64 chmod +x ./xgboost -tests/ci_build/ci_build.sh aarch64 docker tests/ci_build/test_python.sh cpu-arm64 +tests/ci_build/ci_build.sh aarch64 tests/ci_build/test_python.sh cpu-arm64 diff --git a/tests/buildkite/test-python-cpu.sh b/tests/buildkite/test-python-cpu.sh index 938e1184e..6c53dc282 100755 --- a/tests/buildkite/test-python-cpu.sh +++ b/tests/buildkite/test-python-cpu.sh @@ -13,4 +13,4 @@ chmod +x ./xgboost export BUILDKITE_ANALYTICS_TOKEN=$(get_aws_secret buildkite/test_analytics/cpu) set_buildkite_env_vars_in_container -tests/ci_build/ci_build.sh cpu docker tests/ci_build/test_python.sh cpu +tests/ci_build/ci_build.sh cpu tests/ci_build/test_python.sh cpu diff --git a/tests/buildkite/test-python-gpu.sh b/tests/buildkite/test-python-gpu.sh index c2376c021..bb61a980d 100755 --- a/tests/buildkite/test-python-gpu.sh +++ b/tests/buildkite/test-python-gpu.sh @@ -22,7 +22,7 @@ chmod +x build/testxgboost # Allocate extra space in /dev/shm to enable NCCL export CI_DOCKER_EXTRA_PARAMS_INIT='--shm-size=4g' -command_wrapper="tests/ci_build/ci_build.sh gpu nvidia-docker --build-arg "` +command_wrapper="tests/ci_build/ci_build.sh gpu --use-gpus --build-arg "` `"CUDA_VERSION_ARG=$CUDA_VERSION --build-arg "` `"RAPIDS_VERSION_ARG=$RAPIDS_VERSION --build-arg "` `"NCCL_VERSION_ARG=$NCCL_VERSION" diff --git a/tests/ci_build/build_r_pkg_with_cuda_win64.sh b/tests/ci_build/build_r_pkg_with_cuda_win64.sh index d44a418d1..580358883 100644 --- a/tests/ci_build/build_r_pkg_with_cuda_win64.sh +++ b/tests/ci_build/build_r_pkg_with_cuda_win64.sh @@ -18,7 +18,7 @@ mv xgboost/ xgboost_rpack/ mkdir build cd build -cmake .. -G"Visual Studio 17 2022" -A x64 -DUSE_CUDA=ON -DR_LIB=ON -DLIBR_HOME="c:\\Program Files\\R\\R-3.6.3" +cmake .. -G"Visual Studio 17 2022" -A x64 -DUSE_CUDA=ON -DR_LIB=ON -DLIBR_HOME="c:\\Program Files\\R\\R-4.3.2" -DCMAKE_PREFIX_PATH="C:\\rtools43\\x86_64-w64-mingw32.static.posix\\bin" cmake --build . --config Release --parallel cd .. @@ -32,5 +32,5 @@ cp -v lib/xgboost.dll xgboost_rpack/src/ echo 'all:' > xgboost_rpack/src/Makefile echo 'all:' > xgboost_rpack/src/Makefile.win mv xgboost_rpack/ xgboost/ -/c/Rtools/bin/tar -cvf xgboost_r_gpu_win64_${commit_hash}.tar xgboost/ -/c/Rtools/bin/gzip -9c xgboost_r_gpu_win64_${commit_hash}.tar > xgboost_r_gpu_win64_${commit_hash}.tar.gz +/c/Rtools43/usr/bin/tar -cvf xgboost_r_gpu_win64_${commit_hash}.tar xgboost/ +/c/Rtools43/usr/bin/gzip -9c xgboost_r_gpu_win64_${commit_hash}.tar > xgboost_r_gpu_win64_${commit_hash}.tar.gz diff --git a/tests/ci_build/ci_build.sh b/tests/ci_build/ci_build.sh index ef0c69183..a2f2d6063 100755 --- a/tests/ci_build/ci_build.sh +++ b/tests/ci_build/ci_build.sh @@ -2,14 +2,14 @@ # # Execute command within a docker container # -# Usage: ci_build.sh +# Usage: ci_build.sh [--use-gpus] # [--dockerfile ] [-it] # [--build-arg ] # # CONTAINER_TYPE: Type of the docker container used the run the build: e.g., # (cpu | gpu) # -# DOCKER_BINARY: Command to invoke docker, e.g. (docker | nvidia-docker). +# --use-gpus: Whether to grant the container access to NVIDIA GPUs. # # DOCKERFILE_PATH: (Optional) Path to the Dockerfile used for docker build. If # this optional value is not supplied (via the --dockerfile @@ -29,9 +29,12 @@ shift 1 DOCKERFILE_PATH="${SCRIPT_DIR}/Dockerfile.${CONTAINER_TYPE}" DOCKER_CONTEXT_PATH="${SCRIPT_DIR}" -# Get docker binary command (should be either docker or nvidia-docker) -DOCKER_BINARY="$1" -shift 1 +GPU_FLAG='' +if [[ "$1" == "--use-gpus" ]]; then + echo "Using NVIDIA GPUs" + GPU_FLAG='--gpus all' + shift 1 +fi if [[ "$1" == "--dockerfile" ]]; then DOCKERFILE_PATH="$2" @@ -144,21 +147,21 @@ then DOCKER_CACHE_REPO="${DOCKER_CACHE_ECR_ID}.dkr.ecr.${DOCKER_CACHE_ECR_REGION}.amazonaws.com" echo "Using AWS ECR; repo URL = ${DOCKER_CACHE_REPO}" # Login for Docker registry - echo "\$(aws ecr get-login --no-include-email --region ${DOCKER_CACHE_ECR_REGION} --registry-ids ${DOCKER_CACHE_ECR_ID})" - $(aws ecr get-login --no-include-email --region ${DOCKER_CACHE_ECR_REGION} --registry-ids ${DOCKER_CACHE_ECR_ID}) + echo "aws ecr get-login-password --region ${DOCKER_CACHE_ECR_REGION} | docker login --username AWS --password-stdin ${DOCKER_CACHE_REPO}" + aws ecr get-login-password --region ${DOCKER_CACHE_ECR_REGION} | docker login --username AWS --password-stdin ${DOCKER_CACHE_REPO} # Pull pre-build container from Docker build cache, # if one exists for the particular branch or pull request DOCKER_TAG="${BRANCH_NAME//\//-}" # Slashes are not allow in Docker tag echo "docker pull --quiet ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG}" if time docker pull --quiet "${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG}" then - CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG}" + CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:${DOCKER_TAG} --build-arg BUILDKIT_INLINE_CACHE=1" else # If the build cache is empty of the particular branch or pull request, # use the build cache associated with the master branch echo "docker pull --quiet ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master" docker pull --quiet "${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master" || true - CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master" + CACHE_FROM_CMD="--cache-from ${DOCKER_CACHE_REPO}/${DOCKER_IMG_NAME}:master --build-arg BUILDKIT_INLINE_CACHE=1" fi else CACHE_FROM_CMD='' @@ -166,11 +169,15 @@ fi echo "docker build \ ${CI_DOCKER_BUILD_ARG} \ + --progress=plain \ + --ulimit nofile=1024000:1024000 \ -t ${DOCKER_IMG_NAME} \ -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT_PATH} \ ${CACHE_FROM_CMD}" docker build \ ${CI_DOCKER_BUILD_ARG} \ + --progress=plain \ + --ulimit nofile=1024000:1024000 \ -t "${DOCKER_IMG_NAME}" \ -f "${DOCKERFILE_PATH}" "${DOCKER_CONTEXT_PATH}" \ ${CACHE_FROM_CMD} @@ -231,7 +238,8 @@ echo "Running '${COMMAND[*]}' inside ${DOCKER_IMG_NAME}..." # and share the PID namespace (--pid=host) so the process inside does not have # pid 1 and SIGKILL is propagated to the process inside (jenkins can kill it). set -x -${DOCKER_BINARY} run --rm --pid=host \ +docker run --rm --pid=host \ + ${GPU_FLAG} \ -v "${WORKSPACE}":/workspace \ -w /workspace \ ${USER_IDS} \ diff --git a/tests/cpp/common/test_device_helpers.cu b/tests/cpp/common/test_device_helpers.cu index 7ae8faf03..1d10a48ad 100644 --- a/tests/cpp/common/test_device_helpers.cu +++ b/tests/cpp/common/test_device_helpers.cu @@ -165,7 +165,7 @@ TEST(SegmentedUnique, Regression) { } } -TEST(Allocator, OOM) { +TEST(Allocator, DISABLED_OOM) { auto size = dh::AvailableMemory(0) * 4; ASSERT_THROW({dh::caching_device_vector vec(size);}, dmlc::Error); ASSERT_THROW({dh::device_vector vec(size);}, dmlc::Error); From e40c4260edddc0f96d640ca0e762d4e65fd57a8a Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 30 Dec 2023 06:28:27 +0100 Subject: [PATCH 067/109] [R] Enable 'dot' dump format (#9930) --- R-package/R/xgb.dump.R | 14 ++++++++-- R-package/R/xgb.plot.tree.R | 49 +++++++++++++++++++++++++++++++--- R-package/man/xgb.dump.Rd | 11 ++++++-- R-package/man/xgb.plot.tree.Rd | 35 ++++++++++++++++++++++-- 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/R-package/R/xgb.dump.R b/R-package/R/xgb.dump.R index a2de26c26..4421836d1 100644 --- a/R-package/R/xgb.dump.R +++ b/R-package/R/xgb.dump.R @@ -13,7 +13,10 @@ #' When this option is on, the model dump contains two additional values: #' gain is the approximate loss function gain we get in each split; #' cover is the sum of second order gradient in each node. -#' @param dump_format either 'text' or 'json' format could be specified. +#' @param dump_format either 'text', 'json', or 'dot' (graphviz) format could be specified. +#' +#' Format 'dot' for a single tree can be passed directly to packages that consume this format +#' for graph visualization, such as function [DiagrammeR::grViz()] #' @param ... currently not used #' #' @return @@ -37,9 +40,13 @@ #' # print in JSON format: #' cat(xgb.dump(bst, with_stats = TRUE, dump_format='json')) #' +#' # plot first tree leveraging the 'dot' format +#' if (requireNamespace('DiagrammeR', quietly = TRUE)) { +#' DiagrammeR::grViz(xgb.dump(bst, dump_format = "dot")[[1L]]) +#' } #' @export xgb.dump <- function(model, fname = NULL, fmap = "", with_stats = FALSE, - dump_format = c("text", "json"), ...) { + dump_format = c("text", "json", "dot"), ...) { check.deprecation(...) dump_format <- match.arg(dump_format) if (!inherits(model, "xgb.Booster")) @@ -52,6 +59,9 @@ xgb.dump <- function(model, fname = NULL, fmap = "", with_stats = FALSE, model <- xgb.Booster.complete(model) model_dump <- .Call(XGBoosterDumpModel_R, model$handle, NVL(fmap, "")[1], as.integer(with_stats), as.character(dump_format)) + if (dump_format == "dot") { + return(sapply(model_dump, function(x) gsub("^booster\\[\\d+\\]\\n", "\\1", x))) + } if (is.null(fname)) model_dump <- gsub('\t', '', model_dump, fixed = TRUE) diff --git a/R-package/R/xgb.plot.tree.R b/R-package/R/xgb.plot.tree.R index 29d00e111..8b12d8a68 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -14,11 +14,33 @@ #' The values are passed to [DiagrammeR::render_graph()]. #' @param render Should the graph be rendered or not? The default is `TRUE`. #' @param show_node_id a logical flag for whether to show node id's in the graph. +#' @param style Style to use for the plot. Options are:\itemize{ +#' \item `"xgboost"`: will use the plot style defined in the core XGBoost library, +#' which is shared between different interfaces through the 'dot' format. This +#' style was not available before version 2.1.0 in R. It always plots the trees +#' vertically (from top to bottom). +#' \item `"R"`: will use the style defined from XGBoost's R interface, which predates +#' the introducition of the standardized style from the core library. It might plot +#' the trees horizontally (from left to right). +#' } +#' +#' Note that `style="xgboost"` is only supported when all of the following conditions are met:\itemize{ +#' \item Only a single tree is being plotted. +#' \item Node IDs are not added to the graph. +#' \item The graph is being returned as `htmlwidget` (`render=TRUE`). +#' } #' @param ... currently not used. #' #' @details #' -#' The content of each node is visualized like this: +#' When using `style="xgboost"`, the content of each node is visualized as follows: +#' - For non-terminal nodes, it will display the split condition (number or name if +#' available, and the condition that would decide to which node to go next). +#' - Those nodes will be connected to their children by arrows that indicate whether the +#' branch corresponds to the condition being met or not being met. +#' - Terminal (leaf) nodes contain the margin to add when ending there. +#' +#' When using `style="R"`, the content of each node is visualized like this: #' - *Feature name*. #' - *Cover:* The sum of second order gradients of training data. #' For the squared loss, this simply corresponds to the number of instances in the node. @@ -57,8 +79,13 @@ #' objective = "binary:logistic" #' ) #' +#' # plot the first tree, using the style from xgboost's core library +#' # (this plot should look identical to the ones generated from other +#' # interfaces like the python package for xgboost) +#' xgb.plot.tree(model = bst, trees = 1, style = "xgboost") +#' #' # plot all the trees -#' xgb.plot.tree(model = bst) +#' xgb.plot.tree(model = bst, trees = NULL) #' #' # plot only the first tree and display the node ID: #' xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) @@ -77,7 +104,7 @@ #' #' @export xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot_width = NULL, plot_height = NULL, - render = TRUE, show_node_id = FALSE, ...) { + render = TRUE, show_node_id = FALSE, style = c("R", "xgboost"), ...) { check.deprecation(...) if (!inherits(model, "xgb.Booster")) { stop("model: Has to be an object of class xgb.Booster") @@ -87,6 +114,22 @@ xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot stop("DiagrammeR package is required for xgb.plot.tree", call. = FALSE) } + style <- as.character(head(style, 1L)) + stopifnot(style %in% c("R", "xgboost")) + if (style == "xgboost") { + if (NROW(trees) != 1L || !render || show_node_id) { + stop("style='xgboost' is only supported for single, rendered tree, without node IDs.") + } + if (!is.null(feature_names)) { + stop( + "style='xgboost' cannot override 'feature_names'. Will automatically take them from the model." + ) + } + + txt <- xgb.dump(model, dump_format = "dot") + return(DiagrammeR::grViz(txt[[trees + 1]], width = plot_width, height = plot_height)) + } + dt <- xgb.model.dt.tree(feature_names = feature_names, model = model, trees = trees) dt[, label := paste0(Feature, "\nCover: ", Cover, ifelse(Feature == "Leaf", "\nValue: ", "\nGain: "), Quality)] diff --git a/R-package/man/xgb.dump.Rd b/R-package/man/xgb.dump.Rd index 791e74d96..2cdb6b16a 100644 --- a/R-package/man/xgb.dump.Rd +++ b/R-package/man/xgb.dump.Rd @@ -9,7 +9,7 @@ xgb.dump( fname = NULL, fmap = "", with_stats = FALSE, - dump_format = c("text", "json"), + dump_format = c("text", "json", "dot"), ... ) } @@ -29,7 +29,10 @@ When this option is on, the model dump contains two additional values: gain is the approximate loss function gain we get in each split; cover is the sum of second order gradient in each node.} -\item{dump_format}{either 'text' or 'json' format could be specified.} +\item{dump_format}{either 'text', 'json', or 'dot' (graphviz) format could be specified. + +Format 'dot' for a single tree can be passed directly to packages that consume this format +for graph visualization, such as function \code{\link[DiagrammeR:grViz]{DiagrammeR::grViz()}}} \item{...}{currently not used} } @@ -57,4 +60,8 @@ print(xgb.dump(bst, with_stats = TRUE)) # print in JSON format: cat(xgb.dump(bst, with_stats = TRUE, dump_format='json')) +# plot first tree leveraging the 'dot' format +if (requireNamespace('DiagrammeR', quietly = TRUE)) { + DiagrammeR::grViz(xgb.dump(bst, dump_format = "dot")[[1L]]) +} } diff --git a/R-package/man/xgb.plot.tree.Rd b/R-package/man/xgb.plot.tree.Rd index 7571487eb..a09bb7183 100644 --- a/R-package/man/xgb.plot.tree.Rd +++ b/R-package/man/xgb.plot.tree.Rd @@ -12,6 +12,7 @@ xgb.plot.tree( plot_height = NULL, render = TRUE, show_node_id = FALSE, + style = c("R", "xgboost"), ... ) } @@ -34,6 +35,22 @@ The values are passed to \code{\link[DiagrammeR:render_graph]{DiagrammeR::render \item{show_node_id}{a logical flag for whether to show node id's in the graph.} +\item{style}{Style to use for the plot. Options are:\itemize{ +\item \code{"xgboost"}: will use the plot style defined in the core XGBoost library, +which is shared between different interfaces through the 'dot' format. This +style was not available before version 2.1.0 in R. It always plots the trees +vertically (from top to bottom). +\item \code{"R"}: will use the style defined from XGBoost's R interface, which predates +the introducition of the standardized style from the core library. It might plot +the trees horizontally (from left to right). +} + +Note that \code{style="xgboost"} is only supported when all of the following conditions are met:\itemize{ +\item Only a single tree is being plotted. +\item Node IDs are not added to the graph. +\item The graph is being returned as \code{htmlwidget} (\code{render=TRUE}). +}} + \item{...}{currently not used.} } \value{ @@ -51,7 +68,16 @@ before rendering the graph with \code{\link[DiagrammeR:render_graph]{DiagrammeR: Read a tree model text dump and plot the model. } \details{ -The content of each node is visualized like this: +When using \code{style="xgboost"}, the content of each node is visualized as follows: +\itemize{ +\item For non-terminal nodes, it will display the split condition (number or name if +available, and the condition that would decide to which node to go next). +\item Those nodes will be connected to their children by arrows that indicate whether the +branch corresponds to the condition being met or not being met. +\item Terminal (leaf) nodes contain the margin to add when ending there. +} + +When using \code{style="R"}, the content of each node is visualized like this: \itemize{ \item \emph{Feature name}. \item \emph{Cover:} The sum of second order gradients of training data. @@ -83,8 +109,13 @@ bst <- xgboost( objective = "binary:logistic" ) +# plot the first tree, using the style from xgboost's core library +# (this plot should look identical to the ones generated from other +# interfaces like the python package for xgboost) +xgb.plot.tree(model = bst, trees = 1, style = "xgboost") + # plot all the trees -xgb.plot.tree(model = bst) +xgb.plot.tree(model = bst, trees = NULL) # plot only the first tree and display the node ID: xgb.plot.tree(model = bst, trees = 0, show_node_id = TRUE) From 8b9c98b65b511339c473cdc3043c390aa545fc2c Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sun, 31 Dec 2023 03:45:04 +0100 Subject: [PATCH 068/109] [R] Clearer function signatures for S3 methods (#9937) --- R-package/R/xgb.DMatrix.R | 19 ++++++++----------- R-package/man/getinfo.Rd | 6 ++---- R-package/man/setinfo.Rd | 6 ++---- R-package/man/slice.xgb.DMatrix.Rd | 6 ++---- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 11d1105e6..6acd1e6b2 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -340,7 +340,6 @@ dimnames.xgb.DMatrix <- function(x) { #' Get information of an xgb.DMatrix object #' @param object Object of class \code{xgb.DMatrix} #' @param name the name of the information field to get (see details) -#' @param ... other parameters #' #' @details #' The \code{name} field can be one of the following: @@ -372,11 +371,11 @@ dimnames.xgb.DMatrix <- function(x) { #' stopifnot(all(labels2 == 1-labels)) #' @rdname getinfo #' @export -getinfo <- function(object, ...) UseMethod("getinfo") +getinfo <- function(object, name) UseMethod("getinfo") #' @rdname getinfo #' @export -getinfo.xgb.DMatrix <- function(object, name, ...) { +getinfo.xgb.DMatrix <- function(object, name) { allowed_int_fields <- 'group' allowed_float_fields <- c( 'label', 'weight', 'base_margin', @@ -421,7 +420,6 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' @param object Object of class "xgb.DMatrix" #' @param name the name of the field to get #' @param info the specific field of information to set -#' @param ... Not used. #' #' @details #' See the documentation for \link{xgb.DMatrix} for possible fields that can be set @@ -445,17 +443,17 @@ getinfo.xgb.DMatrix <- function(object, name, ...) { #' stopifnot(all.equal(labels2, 1-labels)) #' @rdname setinfo #' @export -setinfo <- function(object, ...) UseMethod("setinfo") +setinfo <- function(object, name, info) UseMethod("setinfo") #' @rdname setinfo #' @export -setinfo.xgb.DMatrix <- function(object, name, info, ...) { - .internal.setinfo.xgb.DMatrix(object, name, info, ...) +setinfo.xgb.DMatrix <- function(object, name, info) { + .internal.setinfo.xgb.DMatrix(object, name, info) attr(object, "fields")[[name]] <- TRUE return(TRUE) } -.internal.setinfo.xgb.DMatrix <- function(object, name, info, ...) { +.internal.setinfo.xgb.DMatrix <- function(object, name, info) { if (name == "label") { if (NROW(info) != nrow(object)) stop("The length of labels must equal to the number of rows in the input data") @@ -538,7 +536,6 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { #' @param object Object of class "xgb.DMatrix" #' @param idxset a integer vector of indices of rows needed #' @param colset currently not used (columns subsetting is not available) -#' @param ... other parameters (currently not used) #' #' @examples #' data(agaricus.train, package='xgboost') @@ -552,11 +549,11 @@ setinfo.xgb.DMatrix <- function(object, name, info, ...) { #' #' @rdname slice.xgb.DMatrix #' @export -slice <- function(object, ...) UseMethod("slice") +slice <- function(object, idxset) UseMethod("slice") #' @rdname slice.xgb.DMatrix #' @export -slice.xgb.DMatrix <- function(object, idxset, ...) { +slice.xgb.DMatrix <- function(object, idxset) { if (!inherits(object, "xgb.DMatrix")) { stop("object must be xgb.DMatrix") } diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index 71f855d8a..cb552886b 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -5,15 +5,13 @@ \alias{getinfo.xgb.DMatrix} \title{Get information of an xgb.DMatrix object} \usage{ -getinfo(object, ...) +getinfo(object, name) -\method{getinfo}{xgb.DMatrix}(object, name, ...) +\method{getinfo}{xgb.DMatrix}(object, name) } \arguments{ \item{object}{Object of class \code{xgb.DMatrix}} -\item{...}{other parameters} - \item{name}{the name of the information field to get (see details)} } \description{ diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd index 299e72675..549fc9b20 100644 --- a/R-package/man/setinfo.Rd +++ b/R-package/man/setinfo.Rd @@ -5,15 +5,13 @@ \alias{setinfo.xgb.DMatrix} \title{Set information of an xgb.DMatrix object} \usage{ -setinfo(object, ...) +setinfo(object, name, info) -\method{setinfo}{xgb.DMatrix}(object, name, info, ...) +\method{setinfo}{xgb.DMatrix}(object, name, info) } \arguments{ \item{object}{Object of class "xgb.DMatrix"} -\item{...}{Not used.} - \item{name}{the name of the field to get} \item{info}{the specific field of information to set} diff --git a/R-package/man/slice.xgb.DMatrix.Rd b/R-package/man/slice.xgb.DMatrix.Rd index cb65083e2..a2dfb699b 100644 --- a/R-package/man/slice.xgb.DMatrix.Rd +++ b/R-package/man/slice.xgb.DMatrix.Rd @@ -7,17 +7,15 @@ \title{Get a new DMatrix containing the specified rows of original xgb.DMatrix object} \usage{ -slice(object, ...) +slice(object, idxset) -\method{slice}{xgb.DMatrix}(object, idxset, ...) +\method{slice}{xgb.DMatrix}(object, idxset) \method{[}{xgb.DMatrix}(object, idxset, colset = NULL) } \arguments{ \item{object}{Object of class "xgb.DMatrix"} -\item{...}{other parameters (currently not used)} - \item{idxset}{a integer vector of indices of rows needed} \item{colset}{currently not used (columns subsetting is not available)} From 73713de6016163252958463147c9c6cd509e79b1 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sun, 31 Dec 2023 06:01:00 +0100 Subject: [PATCH 069/109] [R] rename Quality -> Gain (#9938) --- R-package/R/xgb.model.dt.tree.R | 10 +++++----- R-package/R/xgb.plot.deepness.R | 4 ++-- R-package/R/xgb.plot.multi.trees.R | 4 ++-- R-package/R/xgb.plot.tree.R | 4 ++-- R-package/man/xgb.model.dt.tree.Rd | 2 +- R-package/tests/testthat/test_helpers.R | 4 ++-- R-package/tests/testthat/test_update.R | 16 ++++++++-------- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/R-package/R/xgb.model.dt.tree.R b/R-package/R/xgb.model.dt.tree.R index 8e74ea4b4..9a32d82a0 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -28,7 +28,7 @@ #' - `Yes`: ID of the next node when the split condition is met. #' - `No`: ID of the next node when the split condition is not met. #' - `Missing`: ID of the next node when the branch value is missing. -#' - `Quality`: either the split gain (change in loss) or the leaf value. +#' - `Gain`: either the split gain (change in loss) or the leaf value. #' - `Cover`: metric related to the number of observations either seen by a split #' or collected by a leaf during training. #' @@ -122,7 +122,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, # parse branch lines branch_rx <- paste0("f(\\d+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") - branch_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Quality", "Cover") + branch_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") td[ isLeaf == FALSE, (branch_cols) := { @@ -132,7 +132,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, xtr[, 3:5] <- add.tree.id(xtr[, 3:5], Tree) if (length(xtr) == 0) { as.data.table( - list(Feature = "NA", Split = "NA", Yes = "NA", No = "NA", Missing = "NA", Quality = "NA", Cover = "NA") + list(Feature = "NA", Split = "NA", Yes = "NA", No = "NA", Missing = "NA", Gain = "NA", Cover = "NA") ) } else { as.data.table(xtr) @@ -152,7 +152,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, # parse leaf lines leaf_rx <- paste0("leaf=(", anynumber_regex, "),cover=(", anynumber_regex, ")") - leaf_cols <- c("Feature", "Quality", "Cover") + leaf_cols <- c("Feature", "Gain", "Cover") td[ isLeaf == TRUE, (leaf_cols) := { @@ -167,7 +167,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, ] # convert some columns to numeric - numeric_cols <- c("Split", "Quality", "Cover") + numeric_cols <- c("Split", "Gain", "Cover") td[, (numeric_cols) := lapply(.SD, as.numeric), .SDcols = numeric_cols] if (use_int_id) { int_cols <- c("Yes", "No", "Missing") diff --git a/R-package/R/xgb.plot.deepness.R b/R-package/R/xgb.plot.deepness.R index 092b07d38..8e1972374 100644 --- a/R-package/R/xgb.plot.deepness.R +++ b/R-package/R/xgb.plot.deepness.R @@ -92,7 +92,7 @@ xgb.plot.deepness <- function(model = NULL, which = c("2x1", "max.depth", "med.d stop("Model tree columns are not as expected!\n", " Note that this function works only for tree models.") - dt_depths <- merge(get.leaf.depth(dt_tree), dt_tree[, .(ID, Cover, Weight = Quality)], by = "ID") + dt_depths <- merge(get.leaf.depth(dt_tree), dt_tree[, .(ID, Cover, Weight = Gain)], by = "ID") setkeyv(dt_depths, c("Tree", "ID")) # count by depth levels, and also calculate average cover at a depth dt_summaries <- dt_depths[, .(.N, Cover = mean(Cover)), Depth] @@ -157,6 +157,6 @@ get.leaf.depth <- function(dt_tree) { # They are mainly column names inferred by Data.table... globalVariables( c( - ".N", "N", "Depth", "Quality", "Cover", "Tree", "ID", "Yes", "No", "Feature", "Leaf", "Weight" + ".N", "N", "Depth", "Gain", "Cover", "Tree", "ID", "Yes", "No", "Feature", "Leaf", "Weight" ) ) diff --git a/R-package/R/xgb.plot.multi.trees.R b/R-package/R/xgb.plot.multi.trees.R index 6402cb767..88616cfb7 100644 --- a/R-package/R/xgb.plot.multi.trees.R +++ b/R-package/R/xgb.plot.multi.trees.R @@ -95,13 +95,13 @@ xgb.plot.multi.trees <- function(model, feature_names = NULL, features_keep = 5, data.table::set(tree.matrix, j = nm, value = sub("^\\d+-", "", tree.matrix[[nm]])) nodes.dt <- tree.matrix[ - , .(Quality = sum(Quality)) + , .(Gain = sum(Gain)) , by = .(abs.node.position, Feature) ][, .(Text = paste0( paste0( Feature[seq_len(min(length(Feature), features_keep))], " (", - format(Quality[seq_len(min(length(Quality), features_keep))], digits = 5), + format(Gain[seq_len(min(length(Gain), features_keep))], digits = 5), ")" ), collapse = "\n" diff --git a/R-package/R/xgb.plot.tree.R b/R-package/R/xgb.plot.tree.R index 8b12d8a68..c75a42e84 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -132,7 +132,7 @@ xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot dt <- xgb.model.dt.tree(feature_names = feature_names, model = model, trees = trees) - dt[, label := paste0(Feature, "\nCover: ", Cover, ifelse(Feature == "Leaf", "\nValue: ", "\nGain: "), Quality)] + dt[, label := paste0(Feature, "\nCover: ", Cover, ifelse(Feature == "Leaf", "\nValue: ", "\nGain: "), Gain)] if (show_node_id) dt[, label := paste0(ID, ": ", label)] dt[Node == 0, label := paste0("Tree ", Tree, "\n", label)] @@ -199,4 +199,4 @@ xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot # Avoid error messages during CRAN check. # The reason is that these variables are never declared # They are mainly column names inferred by Data.table... -globalVariables(c("Feature", "ID", "Cover", "Quality", "Split", "Yes", "No", "Missing", ".", "shape", "filledcolor", "label")) +globalVariables(c("Feature", "ID", "Cover", "Gain", "Split", "Yes", "No", "Missing", ".", "shape", "filledcolor", "label")) diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 477c40775..330998ab8 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -46,7 +46,7 @@ for a leaf node, it simply labels it as \code{"Leaf"}. \item \code{Yes}: ID of the next node when the split condition is met. \item \code{No}: ID of the next node when the split condition is not met. \item \code{Missing}: ID of the next node when the branch value is missing. -\item \code{Quality}: either the split gain (change in loss) or the leaf value. +\item \code{Gain}: either the split gain (change in loss) or the leaf value. \item \code{Cover}: metric related to the number of observations either seen by a split or collected by a leaf during training. } diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index de6a099fc..7fae052b4 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -275,7 +275,7 @@ test_that("xgb.Booster serializing as R object works", { test_that("xgb.model.dt.tree works with and without feature names", { .skip_if_vcd_not_available() - names.dt.trees <- c("Tree", "Node", "ID", "Feature", "Split", "Yes", "No", "Missing", "Quality", "Cover") + names.dt.trees <- c("Tree", "Node", "ID", "Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") dt.tree <- xgb.model.dt.tree(feature_names = feature.names, model = bst.Tree) expect_equal(names.dt.trees, names(dt.tree)) if (!flag_32bit) @@ -341,7 +341,7 @@ test_that("xgb.importance works with and without feature names", { trees = trees )[ Feature != "Leaf", .( - Gain = sum(Quality), + Gain = sum(Gain), Cover = sum(Cover), Frequency = .N ), diff --git a/R-package/tests/testthat/test_update.R b/R-package/tests/testthat/test_update.R index cf8b6f007..f37bb0d21 100644 --- a/R-package/tests/testthat/test_update.R +++ b/R-package/tests/testthat/test_update.R @@ -53,9 +53,9 @@ test_that("updating the model works", { # should be the same evaluation but different gains and larger cover expect_equal(bst2$evaluation_log, bst2r$evaluation_log) if (!win32_flag) { - expect_equal(tr2[Feature == 'Leaf']$Quality, tr2r[Feature == 'Leaf']$Quality) + expect_equal(tr2[Feature == 'Leaf']$Gain, tr2r[Feature == 'Leaf']$Gain) } - expect_gt(sum(abs(tr2[Feature != 'Leaf']$Quality - tr2r[Feature != 'Leaf']$Quality)), 100) + expect_gt(sum(abs(tr2[Feature != 'Leaf']$Gain - tr2r[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr2r$Cover) / sum(tr2$Cover), 1.5) # process type 'update' for no-subsampling model, refreshing the tree stats AND leaves from training data: @@ -72,8 +72,8 @@ test_that("updating the model works", { tr2u <- xgb.model.dt.tree(model = bst2u) # should be the same evaluation but different gains and larger cover expect_equal(bst2$evaluation_log, bst2u$evaluation_log) - expect_equal(tr2[Feature == 'Leaf']$Quality, tr2u[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr2[Feature != 'Leaf']$Quality - tr2u[Feature != 'Leaf']$Quality)), 100) + expect_equal(tr2[Feature == 'Leaf']$Gain, tr2u[Feature == 'Leaf']$Gain) + expect_gt(sum(abs(tr2[Feature != 'Leaf']$Gain - tr2u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr2u$Cover) / sum(tr2$Cover), 1.5) # the results should be the same as for the model with an extra 'refresh' updater expect_equal(bst2r$evaluation_log, bst2u$evaluation_log) @@ -87,8 +87,8 @@ test_that("updating the model works", { tr1ut <- xgb.model.dt.tree(model = bst1ut) # should be the same evaluations but different gains and smaller cover (test data is smaller) expect_equal(bst1$evaluation_log, bst1ut$evaluation_log) - expect_equal(tr1[Feature == 'Leaf']$Quality, tr1ut[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr1[Feature != 'Leaf']$Quality - tr1ut[Feature != 'Leaf']$Quality)), 100) + expect_equal(tr1[Feature == 'Leaf']$Gain, tr1ut[Feature == 'Leaf']$Gain) + expect_gt(sum(abs(tr1[Feature != 'Leaf']$Gain - tr1ut[Feature != 'Leaf']$Gain)), 100) expect_lt(sum(tr1ut$Cover) / sum(tr1$Cover), 0.5) }) @@ -111,7 +111,7 @@ test_that("updating works for multiclass & multitree", { # should be the same evaluation but different gains and larger cover expect_equal(bst0$evaluation_log, bst0u$evaluation_log) - expect_equal(tr0[Feature == 'Leaf']$Quality, tr0u[Feature == 'Leaf']$Quality) - expect_gt(sum(abs(tr0[Feature != 'Leaf']$Quality - tr0u[Feature != 'Leaf']$Quality)), 100) + expect_equal(tr0[Feature == 'Leaf']$Gain, tr0u[Feature == 'Leaf']$Gain) + expect_gt(sum(abs(tr0[Feature != 'Leaf']$Gain - tr0u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr0u$Cover) / sum(tr0$Cover), 1.5) }) From 32cbab1cc00e5640fd79fd8557c098128d7efbec Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 2 Jan 2024 08:20:51 +0100 Subject: [PATCH 070/109] [R] put 'verbose' in correct argument (#9942) --- R-package/R/xgb.train.R | 16 ++++++++-------- R-package/man/xgb.train.Rd | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/R-package/R/xgb.train.R b/R-package/R/xgb.train.R index d93a0643d..e20c1af3e 100644 --- a/R-package/R/xgb.train.R +++ b/R-package/R/xgb.train.R @@ -251,9 +251,9 @@ #' watchlist <- list(train = dtrain, eval = dtest) #' #' ## A simple xgb.train example: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread, #' objective = "binary:logistic", eval_metric = "auc") -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) #' #' ## An xgb.train example where custom objective and evaluation metric are #' ## used: @@ -272,13 +272,13 @@ #' #' # These functions could be used by passing them either: #' # as 'objective' and 'eval_metric' parameters in the params list: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread, #' objective = logregobj, eval_metric = evalerror) -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) #' #' # or through the ... arguments: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread) -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread) +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, #' objective = logregobj, eval_metric = evalerror) #' #' # or as dedicated 'obj' and 'feval' parameters of xgb.train: @@ -287,10 +287,10 @@ #' #' #' ## An xgb.train example of using variable learning rates at each iteration: -#' param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +#' param <- list(max_depth = 2, eta = 1, nthread = nthread, #' objective = "binary:logistic", eval_metric = "auc") #' my_etas <- list(eta = c(0.5, 0.1)) -#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +#' bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, #' callbacks = list(cb.reset.parameters(my_etas))) #' #' ## Early stopping: diff --git a/R-package/man/xgb.train.Rd b/R-package/man/xgb.train.Rd index 0ef2e2216..b2eaff27c 100644 --- a/R-package/man/xgb.train.Rd +++ b/R-package/man/xgb.train.Rd @@ -303,9 +303,9 @@ dtest <- with( watchlist <- list(train = dtrain, eval = dtest) ## A simple xgb.train example: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +param <- list(max_depth = 2, eta = 1, nthread = nthread, objective = "binary:logistic", eval_metric = "auc") -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) ## An xgb.train example where custom objective and evaluation metric are ## used: @@ -324,13 +324,13 @@ evalerror <- function(preds, dtrain) { # These functions could be used by passing them either: # as 'objective' and 'eval_metric' parameters in the params list: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +param <- list(max_depth = 2, eta = 1, nthread = nthread, objective = logregobj, eval_metric = evalerror) -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist) +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0) # or through the ... arguments: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread) -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +param <- list(max_depth = 2, eta = 1, nthread = nthread) +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, objective = logregobj, eval_metric = evalerror) # or as dedicated 'obj' and 'feval' parameters of xgb.train: @@ -339,10 +339,10 @@ bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, ## An xgb.train example of using variable learning rates at each iteration: -param <- list(max_depth = 2, eta = 1, verbose = 0, nthread = nthread, +param <- list(max_depth = 2, eta = 1, nthread = nthread, objective = "binary:logistic", eval_metric = "auc") my_etas <- list(eta = c(0.5, 0.1)) -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, +bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_etas))) ## Early stopping: From 9e33a102021aa2fa2283d5a1e6447f24c3ce9633 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Tue, 2 Jan 2024 14:20:01 +0100 Subject: [PATCH 071/109] [R] Replace `xgboost()` with `xgb.train()` in most tests and examples (#9941) --- R-package/R/utils.R | 5 +- R-package/R/xgb.Booster.R | 15 ++-- R-package/R/xgb.load.R | 6 +- R-package/R/xgb.save.R | 6 +- R-package/R/xgb.save.raw.R | 4 +- R-package/R/xgb.serialize.R | 4 +- R-package/demo/create_sparse_matrix.R | 4 +- R-package/demo/interaction_constraints.R | 18 ++-- R-package/demo/poisson_regression.R | 4 +- .../a-compatibility-note-for-saveRDS-save.Rd | 5 +- R-package/man/predict.xgb.Booster.Rd | 15 ++-- R-package/man/xgb.load.Rd | 6 +- R-package/man/xgb.save.Rd | 6 +- R-package/man/xgb.save.raw.Rd | 4 +- R-package/man/xgb.serialize.Rd | 4 +- R-package/tests/testthat/test_basic.R | 84 ++++++++++--------- R-package/tests/testthat/test_callbacks.R | 8 +- R-package/tests/testthat/test_gc_safety.R | 4 +- R-package/tests/testthat/test_helpers.R | 49 ++++++----- .../testthat/test_interaction_constraints.R | 6 +- R-package/tests/testthat/test_interactions.R | 10 +-- R-package/tests/testthat/test_io.R | 4 +- R-package/tests/testthat/test_monotone.R | 6 +- .../tests/testthat/test_parameter_exposure.R | 14 ++-- .../tests/testthat/test_poisson_regression.R | 4 +- R-package/tests/testthat/test_unicode.R | 6 +- R-package/vignettes/xgboostfromJSON.Rmd | 5 +- 27 files changed, 156 insertions(+), 150 deletions(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index bf08c481d..1798e4ad1 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -383,8 +383,9 @@ NULL #' #' @examples #' data(agaricus.train, package='xgboost') -#' bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +#' bst <- xgb.train(data = xgb.DMatrix(agaricus.train$data, label = agaricus.train$label), +#' max_depth = 2, eta = 1, nthread = 2, nrounds = 2, +#' objective = "binary:logistic") #' #' # Save as a stand-alone file; load it with xgb.load() #' xgb.save(bst, 'xgb.model') diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 4e980641a..371f3d129 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -272,9 +272,8 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' train <- agaricus.train #' test <- agaricus.test #' -#' bst <- xgboost( -#' data = train$data, -#' label = train$label, +#' bst <- xgb.train( +#' data = xgb.DMatrix(train$data, label = train$label), #' max_depth = 2, #' eta = 0.5, #' nthread = nthread, @@ -316,9 +315,8 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' #' set.seed(11) #' -#' bst <- xgboost( -#' data = as.matrix(iris[, -5]), -#' label = lb, +#' bst <- xgb.train( +#' data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), #' max_depth = 4, #' eta = 0.5, #' nthread = 2, @@ -341,9 +339,8 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' # compare with predictions from softmax: #' set.seed(11) #' -#' bst <- xgboost( -#' data = as.matrix(iris[, -5]), -#' label = lb, +#' bst <- xgb.train( +#' data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), #' max_depth = 4, #' eta = 0.5, #' nthread = 2, diff --git a/R-package/R/xgb.load.R b/R-package/R/xgb.load.R index cbdbdacc3..e8f9e0023 100644 --- a/R-package/R/xgb.load.R +++ b/R-package/R/xgb.load.R @@ -29,8 +29,10 @@ #' #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost( -#' data = train$data, label = train$label, max_depth = 2, eta = 1, +#' bst <- xgb.train( +#' data = xgb.DMatrix(train$data, label = train$label), +#' max_depth = 2, +#' eta = 1, #' nthread = nthread, #' nrounds = 2, #' objective = "binary:logistic" diff --git a/R-package/R/xgb.save.R b/R-package/R/xgb.save.R index ab55bc4a9..32b7d9618 100644 --- a/R-package/R/xgb.save.R +++ b/R-package/R/xgb.save.R @@ -32,8 +32,10 @@ #' #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost( -#' data = train$data, label = train$label, max_depth = 2, eta = 1, +#' bst <- xgb.train( +#' data = xgb.DMatrix(train$data, label = train$label), +#' max_depth = 2, +#' eta = 1, #' nthread = nthread, #' nrounds = 2, #' objective = "binary:logistic" diff --git a/R-package/R/xgb.save.raw.R b/R-package/R/xgb.save.raw.R index cad0fb0e0..63c06e071 100644 --- a/R-package/R/xgb.save.raw.R +++ b/R-package/R/xgb.save.raw.R @@ -23,8 +23,8 @@ #' #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +#' bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, +#' eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") #' #' raw <- xgb.save.raw(bst) #' bst <- xgb.load.raw(raw) diff --git a/R-package/R/xgb.serialize.R b/R-package/R/xgb.serialize.R index 00bbb4293..c20d2b51c 100644 --- a/R-package/R/xgb.serialize.R +++ b/R-package/R/xgb.serialize.R @@ -9,8 +9,8 @@ #' data(agaricus.test, package='xgboost') #' train <- agaricus.train #' test <- agaricus.test -#' bst <- xgboost(data = train$data, label = train$label, max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") +#' bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, +#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") #' raw <- xgb.serialize(bst) #' bst <- xgb.unserialize(raw) #' diff --git a/R-package/demo/create_sparse_matrix.R b/R-package/demo/create_sparse_matrix.R index f8afb14ba..08a40608c 100644 --- a/R-package/demo/create_sparse_matrix.R +++ b/R-package/demo/create_sparse_matrix.R @@ -81,8 +81,8 @@ output_vector <- df[, Y := 0][Improved == "Marked", Y := 1][, Y] # Following is the same process as other demo cat("Learning...\n") -bst <- xgboost(data = sparse_matrix, label = output_vector, max_depth = 9, - eta = 1, nthread = 2, nrounds = 10, objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = output_vector), max_depth = 9, + eta = 1, nthread = 2, nrounds = 10, objective = "binary:logistic") importance <- xgb.importance(feature_names = colnames(sparse_matrix), model = bst) print(importance) diff --git a/R-package/demo/interaction_constraints.R b/R-package/demo/interaction_constraints.R index 9e694e3eb..72287513e 100644 --- a/R-package/demo/interaction_constraints.R +++ b/R-package/demo/interaction_constraints.R @@ -74,26 +74,26 @@ cols2ids <- function(object, col_names) { interaction_list_fid <- cols2ids(interaction_list, colnames(train)) # Fit model with interaction constraints -bst <- xgboost(data = train, label = y, max_depth = 4, - eta = 0.1, nthread = 2, nrounds = 1000, - interaction_constraints = interaction_list_fid) +bst <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 4, + eta = 0.1, nthread = 2, nrounds = 1000, + interaction_constraints = interaction_list_fid) bst_tree <- xgb.model.dt.tree(colnames(train), bst) bst_interactions <- treeInteractions(bst_tree, 4) # interactions constrained to combinations of V1*V2 and V3*V4*V5 # Fit model without interaction constraints -bst2 <- xgboost(data = train, label = y, max_depth = 4, - eta = 0.1, nthread = 2, nrounds = 1000) +bst2 <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 4, + eta = 0.1, nthread = 2, nrounds = 1000) bst2_tree <- xgb.model.dt.tree(colnames(train), bst2) bst2_interactions <- treeInteractions(bst2_tree, 4) # much more interactions # Fit model with both interaction and monotonicity constraints -bst3 <- xgboost(data = train, label = y, max_depth = 4, - eta = 0.1, nthread = 2, nrounds = 1000, - interaction_constraints = interaction_list_fid, - monotone_constraints = c(-1, 0, 0, 0, 0, 0, 0, 0, 0, 0)) +bst3 <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 4, + eta = 0.1, nthread = 2, nrounds = 1000, + interaction_constraints = interaction_list_fid, + monotone_constraints = c(-1, 0, 0, 0, 0, 0, 0, 0, 0, 0)) bst3_tree <- xgb.model.dt.tree(colnames(train), bst3) bst3_interactions <- treeInteractions(bst3_tree, 4) diff --git a/R-package/demo/poisson_regression.R b/R-package/demo/poisson_regression.R index 121ac17f2..685314b30 100644 --- a/R-package/demo/poisson_regression.R +++ b/R-package/demo/poisson_regression.R @@ -1,6 +1,6 @@ data(mtcars) head(mtcars) -bst <- xgboost(data = as.matrix(mtcars[, -11]), label = mtcars[, 11], - objective = 'count:poisson', nrounds = 5) +bst <- xgb.train(data = xgb.DMatrix(as.matrix(mtcars[, -11]), label = mtcars[, 11]), + objective = 'count:poisson', nrounds = 5) pred <- predict(bst, as.matrix(mtcars[, -11])) sqrt(mean((pred - mtcars[, 11]) ^ 2)) diff --git a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd index 85b52243c..023cff9fd 100644 --- a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd +++ b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd @@ -33,8 +33,9 @@ For more details and explanation about model persistence and archival, consult t } \examples{ data(agaricus.train, package='xgboost') -bst <- xgboost(data = agaricus.train$data, label = agaricus.train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(agaricus.train$data, label = agaricus.train$label), + max_depth = 2, eta = 1, nthread = 2, nrounds = 2, + objective = "binary:logistic") # Save as a stand-alone file; load it with xgb.load() xgb.save(bst, 'xgb.model') diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index 135177dda..f47cab321 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -136,9 +136,8 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost( - data = train$data, - label = train$label, +bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 0.5, nthread = nthread, @@ -180,9 +179,8 @@ num_class <- 3 set.seed(11) -bst <- xgboost( - data = as.matrix(iris[, -5]), - label = lb, +bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 4, eta = 0.5, nthread = 2, @@ -205,9 +203,8 @@ sum(pred_labels != lb) / length(lb) # compare with predictions from softmax: set.seed(11) -bst <- xgboost( - data = as.matrix(iris[, -5]), - label = lb, +bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 4, eta = 0.5, nthread = 2, diff --git a/R-package/man/xgb.load.Rd b/R-package/man/xgb.load.Rd index 1a406cc21..63f551d7a 100644 --- a/R-package/man/xgb.load.Rd +++ b/R-package/man/xgb.load.Rd @@ -34,8 +34,10 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, eta = 1, +bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), + max_depth = 2, + eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" diff --git a/R-package/man/xgb.save.Rd b/R-package/man/xgb.save.Rd index a7e160a12..22c6c8fa3 100644 --- a/R-package/man/xgb.save.Rd +++ b/R-package/man/xgb.save.Rd @@ -38,8 +38,10 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, eta = 1, +bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), + max_depth = 2, + eta = 1, nthread = nthread, nrounds = 2, objective = "binary:logistic" diff --git a/R-package/man/xgb.save.raw.Rd b/R-package/man/xgb.save.raw.Rd index 083551933..498272148 100644 --- a/R-package/man/xgb.save.raw.Rd +++ b/R-package/man/xgb.save.raw.Rd @@ -32,8 +32,8 @@ data.table::setDTthreads(nthread) train <- agaricus.train test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, + eta = 1, nthread = nthread, nrounds = 2,objective = "binary:logistic") raw <- xgb.save.raw(bst) bst <- xgb.load.raw(raw) diff --git a/R-package/man/xgb.serialize.Rd b/R-package/man/xgb.serialize.Rd index 952441d98..5bf4205f8 100644 --- a/R-package/man/xgb.serialize.Rd +++ b/R-package/man/xgb.serialize.Rd @@ -21,8 +21,8 @@ data(agaricus.train, package='xgboost') data(agaricus.test, package='xgboost') train <- agaricus.train test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") +bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, + eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") raw <- xgb.serialize(bst) bst <- xgb.unserialize(raw) diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 8ecf86e87..d4b3a6be3 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -16,10 +16,11 @@ n_threads <- 1 test_that("train and predict binary classification", { nrounds <- 2 expect_output( - bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = nrounds, - objective = "binary:logistic", eval_metric = "error" + objective = "binary:logistic", eval_metric = "error", + watchlist = list(train = xgb.DMatrix(train$data, label = train$label)) ), "train-error" ) @@ -104,9 +105,8 @@ test_that("dart prediction works", { rnorm(100) set.seed(1994) - booster_by_xgboost <- xgboost( - data = d, - label = y, + booster_by_xgboost <- xgb.train( + data = xgb.DMatrix(d, label = y), max_depth = 2, booster = "dart", rate_drop = 0.5, @@ -151,10 +151,11 @@ test_that("train and predict softprob", { lb <- as.numeric(iris$Species) - 1 set.seed(11) expect_output( - bst <- xgboost( - data = as.matrix(iris[, -5]), label = lb, + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 3, eta = 0.5, nthread = n_threads, nrounds = 5, - objective = "multi:softprob", num_class = 3, eval_metric = "merror" + objective = "multi:softprob", num_class = 3, eval_metric = "merror", + watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ), "train-merror" ) @@ -201,10 +202,11 @@ test_that("train and predict softmax", { lb <- as.numeric(iris$Species) - 1 set.seed(11) expect_output( - bst <- xgboost( - data = as.matrix(iris[, -5]), label = lb, + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 3, eta = 0.5, nthread = n_threads, nrounds = 5, - objective = "multi:softmax", num_class = 3, eval_metric = "merror" + objective = "multi:softmax", num_class = 3, eval_metric = "merror", + watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ), "train-merror" ) @@ -222,11 +224,12 @@ test_that("train and predict RF", { set.seed(11) lb <- train$label # single iteration - bst <- xgboost( - data = train$data, label = lb, max_depth = 5, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = lb), max_depth = 5, nthread = n_threads, nrounds = 1, objective = "binary:logistic", eval_metric = "error", - num_parallel_tree = 20, subsample = 0.6, colsample_bytree = 0.1 + num_parallel_tree = 20, subsample = 0.6, colsample_bytree = 0.1, + watchlist = list(train = xgb.DMatrix(train$data, label = lb)) ) expect_equal(bst$niter, 1) expect_equal(xgb.ntree(bst), 20) @@ -248,12 +251,13 @@ test_that("train and predict RF with softprob", { lb <- as.numeric(iris$Species) - 1 nrounds <- 15 set.seed(11) - bst <- xgboost( - data = as.matrix(iris[, -5]), label = lb, + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(iris[, -5]), label = lb), max_depth = 3, eta = 0.9, nthread = n_threads, nrounds = nrounds, objective = "multi:softprob", eval_metric = "merror", num_class = 3, verbose = 0, - num_parallel_tree = 4, subsample = 0.5, colsample_bytree = 0.5 + num_parallel_tree = 4, subsample = 0.5, colsample_bytree = 0.5, + watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ) expect_equal(bst$niter, 15) expect_equal(xgb.ntree(bst), 15 * 3 * 4) @@ -271,10 +275,11 @@ test_that("train and predict RF with softprob", { test_that("use of multiple eval metrics works", { expect_output( - bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", - eval_metric = "error", eval_metric = "auc", eval_metric = "logloss" + eval_metric = "error", eval_metric = "auc", eval_metric = "logloss", + watchlist = list(train = xgb.DMatrix(train$data, label = train$label)) ), "train-error.*train-auc.*train-logloss" ) @@ -282,10 +287,11 @@ test_that("use of multiple eval metrics works", { expect_equal(dim(bst$evaluation_log), c(2, 4)) expect_equal(colnames(bst$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) expect_output( - bst2 <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst2 <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", - eval_metric = list("error", "auc", "logloss") + eval_metric = list("error", "auc", "logloss"), + watchlist = list(train = xgb.DMatrix(train$data, label = train$label)) ), "train-error.*train-auc.*train-logloss" ) @@ -361,7 +367,7 @@ test_that("xgb.cv works", { expect_is(cv, "xgb.cv.synchronous") expect_false(is.null(cv$evaluation_log)) expect_lt(cv$evaluation_log[, min(test_error_mean)], 0.03) - expect_lt(cv$evaluation_log[, min(test_error_std)], 0.008) + expect_lt(cv$evaluation_log[, min(test_error_std)], 0.0085) expect_equal(cv$niter, 2) expect_false(is.null(cv$folds) && is.list(cv$folds)) expect_length(cv$folds, 5) @@ -391,8 +397,8 @@ test_that("xgb.cv works with stratified folds", { test_that("train and predict with non-strict classes", { # standard dense matrix input train_dense <- as.matrix(train$data) - bst <- xgboost( - data = train_dense, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train_dense, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", verbose = 0 ) @@ -402,8 +408,8 @@ test_that("train and predict with non-strict classes", { class(train_dense) <- "shmatrix" expect_true(is.matrix(train_dense)) expect_error( - bst <- xgboost( - data = train_dense, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train_dense, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", verbose = 0 ), @@ -416,8 +422,8 @@ test_that("train and predict with non-strict classes", { class(train_dense) <- c("pphmatrix", "shmatrix") expect_true(is.matrix(train_dense)) expect_error( - bst <- xgboost( - data = train_dense, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train_dense, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", verbose = 0 ), @@ -480,8 +486,8 @@ test_that("colsample_bytree works", { }) test_that("Configuration works", { - bst <- xgboost( - data = train$data, label = train$label, max_depth = 2, + bst <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, eta = 1, nthread = n_threads, nrounds = 2, objective = "binary:logistic", eval_metric = "error", eval_metric = "auc", eval_metric = "logloss" ) @@ -521,8 +527,8 @@ test_that("strict_shape works", { y <- as.numeric(iris$Species) - 1 X <- as.matrix(iris[, -5]) - bst <- xgboost( - data = X, label = y, + bst <- xgb.train( + data = xgb.DMatrix(X, label = y), max_depth = 2, nrounds = n_rounds, nthread = n_threads, objective = "multi:softprob", num_class = 3, eval_metric = "merror" ) @@ -536,8 +542,8 @@ test_that("strict_shape works", { X <- agaricus.train$data y <- agaricus.train$label - bst <- xgboost( - data = X, label = y, max_depth = 2, nthread = n_threads, + bst <- xgb.train( + data = xgb.DMatrix(X, label = y), max_depth = 2, nthread = n_threads, nrounds = n_rounds, objective = "binary:logistic", eval_metric = "error", eval_metric = "auc", eval_metric = "logloss" ) @@ -555,8 +561,8 @@ test_that("'predict' accepts CSR data", { x_csc <- as(X[1L, , drop = FALSE], "CsparseMatrix") x_csr <- as(x_csc, "RsparseMatrix") x_spv <- as(x_csc, "sparseVector") - bst <- xgboost( - data = X, label = y, objective = "binary:logistic", + bst <- xgb.train( + data = xgb.DMatrix(X, label = y), objective = "binary:logistic", nrounds = 5L, verbose = FALSE, nthread = n_threads, ) p_csc <- predict(bst, x_csc) diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index b5d3c5310..63a4c3f25 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -265,14 +265,14 @@ test_that("early stopping works with titanic", { dtx <- model.matrix(~ 0 + ., data = titanic[, c("Pclass", "Sex")]) dty <- titanic$Survived - xgboost::xgboost( - data = dtx, - label = dty, + xgboost::xgb.train( + data = xgb.DMatrix(dtx, label = dty), objective = "binary:logistic", eval_metric = "auc", nrounds = 100, early_stopping_rounds = 3, - nthread = n_threads + nthread = n_threads, + watchlist = list(train = xgb.DMatrix(dtx, label = dty)) ) expect_true(TRUE) # should not crash diff --git a/R-package/tests/testthat/test_gc_safety.R b/R-package/tests/testthat/test_gc_safety.R index f77af1eab..44d8f81a4 100644 --- a/R-package/tests/testthat/test_gc_safety.R +++ b/R-package/tests/testthat/test_gc_safety.R @@ -6,8 +6,8 @@ test_that("train and prediction when gctorture is on", { train <- agaricus.train test <- agaricus.test gctorture(TRUE) - bst <- xgboost(data = train$data, label = train$label, max.depth = 2, - eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") + bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max.depth = 2, + eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic") pred <- predict(bst, test$data) gctorture(FALSE) expect_length(pred, length(test$label)) diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index 7fae052b4..fd1fffbac 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -25,15 +25,15 @@ if (isTRUE(VCD_AVAILABLE)) { label <- df[, ifelse(Improved == "Marked", 1, 0)] # binary - bst.Tree <- xgboost(data = sparse_matrix, label = label, max_depth = 9, - eta = 1, nthread = 2, nrounds = nrounds, verbose = 0, - objective = "binary:logistic", booster = "gbtree", - base_score = 0.5) + bst.Tree <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), max_depth = 9, + eta = 1, nthread = 2, nrounds = nrounds, verbose = 0, + objective = "binary:logistic", booster = "gbtree", + base_score = 0.5) - bst.GLM <- xgboost(data = sparse_matrix, label = label, - eta = 1, nthread = 1, nrounds = nrounds, verbose = 0, - objective = "binary:logistic", booster = "gblinear", - base_score = 0.5) + bst.GLM <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), + eta = 1, nthread = 1, nrounds = nrounds, verbose = 0, + objective = "binary:logistic", booster = "gblinear", + base_score = 0.5) feature.names <- colnames(sparse_matrix) } @@ -41,13 +41,13 @@ if (isTRUE(VCD_AVAILABLE)) { # multiclass mlabel <- as.numeric(iris$Species) - 1 nclass <- 3 -mbst.Tree <- xgboost(data = as.matrix(iris[, -5]), label = mlabel, verbose = 0, - max_depth = 3, eta = 0.5, nthread = 2, nrounds = nrounds, - objective = "multi:softprob", num_class = nclass, base_score = 0) +mbst.Tree <- xgb.train(data = xgb.DMatrix(as.matrix(iris[, -5]), label = mlabel), verbose = 0, + max_depth = 3, eta = 0.5, nthread = 2, nrounds = nrounds, + objective = "multi:softprob", num_class = nclass, base_score = 0) -mbst.GLM <- xgboost(data = as.matrix(iris[, -5]), label = mlabel, verbose = 0, - booster = "gblinear", eta = 0.1, nthread = 1, nrounds = nrounds, - objective = "multi:softprob", num_class = nclass, base_score = 0) +mbst.GLM <- xgb.train(data = xgb.DMatrix(as.matrix(iris[, -5]), label = mlabel), verbose = 0, + booster = "gblinear", eta = 0.1, nthread = 1, nrounds = nrounds, + objective = "multi:softprob", num_class = nclass, base_score = 0) test_that("xgb.dump works", { @@ -71,8 +71,9 @@ test_that("xgb.dump works for gblinear", { expect_length(xgb.dump(bst.GLM), 14) # also make sure that it works properly for a sparse model where some coefficients # are 0 from setting large L1 regularization: - bst.GLM.sp <- xgboost(data = sparse_matrix, label = label, eta = 1, nthread = 2, nrounds = 1, - alpha = 2, objective = "binary:logistic", booster = "gblinear") + bst.GLM.sp <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), eta = 1, + nthread = 2, nrounds = 1, + alpha = 2, objective = "binary:logistic", booster = "gblinear") d.sp <- xgb.dump(bst.GLM.sp) expect_length(d.sp, 14) expect_gt(sum(d.sp == "0"), 0) @@ -168,7 +169,7 @@ test_that("SHAPs sum to predictions, with or without DART", { nrounds <- 30 for (booster in list("gbtree", "dart")) { - fit <- xgboost( + fit <- xgb.train( params = c( list( nthread = 2, @@ -177,8 +178,7 @@ test_that("SHAPs sum to predictions, with or without DART", { eval_metric = "rmse"), if (booster == "dart") list(rate_drop = .01, one_drop = TRUE)), - data = d, - label = y, + data = xgb.DMatrix(d, label = y), nrounds = nrounds) pr <- function(...) { @@ -360,9 +360,8 @@ test_that("xgb.importance works with and without feature names", { expect_equal(importance_from_dump(), importance, tolerance = 1e-6) ## decision stump - m <- xgboost::xgboost( - data = as.matrix(data.frame(x = c(0, 1))), - label = c(1, 2), + m <- xgboost::xgb.train( + data = xgb.DMatrix(as.matrix(data.frame(x = c(0, 1))), label = c(1, 2)), nrounds = 1, base_score = 0.5, nthread = 2 @@ -393,9 +392,9 @@ test_that("xgb.importance works with GLM model", { test_that("xgb.model.dt.tree and xgb.importance work with a single split model", { .skip_if_vcd_not_available() - bst1 <- xgboost(data = sparse_matrix, label = label, max_depth = 1, - eta = 1, nthread = 2, nrounds = 1, verbose = 0, - objective = "binary:logistic") + bst1 <- xgb.train(data = xgb.DMatrix(sparse_matrix, label = label), max_depth = 1, + eta = 1, nthread = 2, nrounds = 1, verbose = 0, + objective = "binary:logistic") expect_error(dt <- xgb.model.dt.tree(model = bst1), regexp = NA) # no error expect_equal(nrow(dt), 3) expect_error(imp <- xgb.importance(model = bst1), regexp = NA) # no error diff --git a/R-package/tests/testthat/test_interaction_constraints.R b/R-package/tests/testthat/test_interaction_constraints.R index ee4c453b3..cfffb029c 100644 --- a/R-package/tests/testthat/test_interaction_constraints.R +++ b/R-package/tests/testthat/test_interaction_constraints.R @@ -13,9 +13,9 @@ train <- matrix(c(x1, x2, x3), ncol = 3) test_that("interaction constraints for regression", { # Fit a model that only allows interaction between x1 and x2 - bst <- xgboost(data = train, label = y, max_depth = 3, - eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, - interaction_constraints = list(c(0, 1))) + bst <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 3, + eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, + interaction_constraints = list(c(0, 1))) # Set all observations to have the same x3 values then increment # by the same amount diff --git a/R-package/tests/testthat/test_interactions.R b/R-package/tests/testthat/test_interactions.R index 398531e0e..645efc12a 100644 --- a/R-package/tests/testthat/test_interactions.R +++ b/R-package/tests/testthat/test_interactions.R @@ -98,15 +98,14 @@ test_that("SHAP contribution values are not NAN", { ivs <- c("x1", "x2") - fit <- xgboost( + fit <- xgb.train( verbose = 0, params = list( objective = "reg:squarederror", eval_metric = "rmse", nthread = n_threads ), - data = as.matrix(subset(d, fold == 2)[, ivs]), - label = subset(d, fold == 2)$y, + data = xgb.DMatrix(as.matrix(subset(d, fold == 2)[, ivs]), label = subset(d, fold == 2)$y), nrounds = 3 ) @@ -169,9 +168,8 @@ test_that("multiclass feature interactions work", { test_that("SHAP single sample works", { train <- agaricus.train test <- agaricus.test - booster <- xgboost( - data = train$data, - label = train$label, + booster <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, nrounds = 4, objective = "binary:logistic", diff --git a/R-package/tests/testthat/test_io.R b/R-package/tests/testthat/test_io.R index 8cf5a9ae9..3c64ddc72 100644 --- a/R-package/tests/testthat/test_io.R +++ b/R-package/tests/testthat/test_io.R @@ -7,8 +7,8 @@ test <- agaricus.test test_that("load/save raw works", { nrounds <- 8 - booster <- xgboost( - data = train$data, label = train$label, + booster <- xgb.train( + data = xgb.DMatrix(train$data, label = train$label), nrounds = nrounds, objective = "binary:logistic", nthread = 2 ) diff --git a/R-package/tests/testthat/test_monotone.R b/R-package/tests/testthat/test_monotone.R index cb5827698..671c02bd0 100644 --- a/R-package/tests/testthat/test_monotone.R +++ b/R-package/tests/testthat/test_monotone.R @@ -7,9 +7,9 @@ train <- matrix(x, ncol = 1) test_that("monotone constraints for regression", { - bst <- xgboost(data = train, label = y, max_depth = 2, - eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, - monotone_constraints = -1) + bst <- xgb.train(data = xgb.DMatrix(train, label = y), max_depth = 2, + eta = 0.1, nthread = 2, nrounds = 100, verbose = 0, + monotone_constraints = -1) pred <- predict(bst, train) diff --git a/R-package/tests/testthat/test_parameter_exposure.R b/R-package/tests/testthat/test_parameter_exposure.R index ea71ca7b7..5b12fde01 100644 --- a/R-package/tests/testthat/test_parameter_exposure.R +++ b/R-package/tests/testthat/test_parameter_exposure.R @@ -10,13 +10,13 @@ dtest <- xgb.DMatrix( agaricus.test$data, label = agaricus.test$label, nthread = 2 ) -bst <- xgboost(data = dtrain, - max_depth = 2, - eta = 1, - nrounds = 10, - nthread = 1, - verbose = 0, - objective = "binary:logistic") +bst <- xgb.train(data = dtrain, + max_depth = 2, + eta = 1, + nrounds = 10, + nthread = 1, + verbose = 0, + objective = "binary:logistic") test_that("call is exposed to R", { expect_false(is.null(bst$call)) diff --git a/R-package/tests/testthat/test_poisson_regression.R b/R-package/tests/testthat/test_poisson_regression.R index 55918b57a..e251a13ad 100644 --- a/R-package/tests/testthat/test_poisson_regression.R +++ b/R-package/tests/testthat/test_poisson_regression.R @@ -4,8 +4,8 @@ set.seed(1994) test_that("Poisson regression works", { data(mtcars) - bst <- xgboost( - data = as.matrix(mtcars[, -11]), label = mtcars[, 11], + bst <- xgb.train( + data = xgb.DMatrix(as.matrix(mtcars[, -11]), label = mtcars[, 11]), objective = 'count:poisson', nrounds = 10, verbose = 0, nthread = 2 ) expect_equal(class(bst), "xgb.Booster") diff --git a/R-package/tests/testthat/test_unicode.R b/R-package/tests/testthat/test_unicode.R index c8a225716..718d58109 100644 --- a/R-package/tests/testthat/test_unicode.R +++ b/R-package/tests/testthat/test_unicode.R @@ -8,9 +8,9 @@ set.seed(1994) test_that("Can save and load models with Unicode paths", { nrounds <- 2 - bst <- xgboost(data = train$data, label = train$label, max_depth = 2, - eta = 1, nthread = 2, nrounds = nrounds, objective = "binary:logistic", - eval_metric = "error") + bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, + eta = 1, nthread = 2, nrounds = nrounds, objective = "binary:logistic", + eval_metric = "error") tmpdir <- tempdir() lapply(c("모델.json", "がうる・ぐら.json", "类继承.ubj"), function(x) { path <- file.path(tmpdir, x) diff --git a/R-package/vignettes/xgboostfromJSON.Rmd b/R-package/vignettes/xgboostfromJSON.Rmd index e7ccdf3a9..f5bc3ad9b 100644 --- a/R-package/vignettes/xgboostfromJSON.Rmd +++ b/R-package/vignettes/xgboostfromJSON.Rmd @@ -52,9 +52,8 @@ labels <- c(1, 1, 1, data <- data.frame(dates = dates, labels = labels) -bst <- xgboost( - data = as.matrix(data$dates), - label = labels, +bst <- xgb.train( + data = xgb.DMatrix(as.matrix(data$dates), label = labels), nthread = 2, nrounds = 1, objective = "binary:logistic", From 49247458f9ede5e4073f5a38b4d6deafc20238c8 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 3 Jan 2024 08:26:55 +0100 Subject: [PATCH 072/109] [R] Minor improvements for evaluation printing (#9940) --- R-package/R/callbacks.R | 3 ++- R-package/R/xgb.cv.R | 5 +++-- R-package/tests/testthat/test_callbacks.R | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/R-package/R/callbacks.R b/R-package/R/callbacks.R index 54f821a79..f8f3b5a30 100644 --- a/R-package/R/callbacks.R +++ b/R-package/R/callbacks.R @@ -770,7 +770,8 @@ xgb.gblinear.history <- function(model, class_index = NULL) { if (!is.null(eval_err)) { if (length(eval_res) != length(eval_err)) stop('eval_res & eval_err lengths mismatch') - res <- paste0(sprintf("%s:%f+%f", enames, eval_res, eval_err), collapse = '\t') + # Note: UTF-8 code for plus/minus sign is U+00B1 + res <- paste0(sprintf("%s:%f\U00B1%f", enames, eval_res, eval_err), collapse = '\t') } else { res <- paste0(sprintf("%s:%f", enames, eval_res), collapse = '\t') } diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index 1c17d86f0..b0d8c4ebe 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -244,8 +244,9 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing ) }) msg <- simplify2array(msg) - bst_evaluation <- rowMeans(msg) - bst_evaluation_err <- sqrt(rowMeans(msg^2) - bst_evaluation^2) # nolint + # Note: these variables might look unused here, but they are used in the callbacks + bst_evaluation <- rowMeans(msg) # nolint + bst_evaluation_err <- apply(msg, 1, sd) # nolint for (f in cb$post_iter) f() diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index 63a4c3f25..de5150380 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -57,7 +57,7 @@ test_that("cb.print.evaluation works as expected", { expect_output(f5(), "\\[7\\]\ttrain-auc:0.900000\ttest-auc:0.800000") bst_evaluation_err <- c('train-auc' = 0.1, 'test-auc' = 0.2) - expect_output(f1(), "\\[7\\]\ttrain-auc:0.900000\\+0.100000\ttest-auc:0.800000\\+0.200000") + expect_output(f1(), "\\[7\\]\ttrain-auc:0.900000±0.100000\ttest-auc:0.800000±0.200000") }) test_that("cb.evaluation.log works as expected", { From 3c004a4145c667df84cf7785a672defbde30c2b6 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 3 Jan 2024 10:29:21 +0100 Subject: [PATCH 073/109] [R] Add missing DMatrix functions (#9929) * `XGDMatrixGetQuantileCut` * `XGDMatrixNumNonMissing` * `XGDMatrixGetDataAsCSR` --------- Co-authored-by: Jiaming Yuan --- R-package/NAMESPACE | 5 + R-package/R/xgb.DMatrix.R | 105 ++++++++++++++ R-package/R/xgboost.R | 3 +- R-package/man/xgb.get.DMatrix.data.Rd | 19 +++ .../man/xgb.get.DMatrix.num.non.missing.Rd | 17 +++ R-package/man/xgb.get.DMatrix.qcut.Rd | 58 ++++++++ R-package/src/Makevars.in | 1 + R-package/src/Makevars.win | 1 + R-package/src/init.c | 6 + R-package/src/xgboost_R.cc | 130 +++++++++++++++++- R-package/src/xgboost_R.h | 25 ++++ R-package/tests/testthat/test_dmatrix.R | 59 ++++++++ src/data/array_interface.cc | 13 ++ src/data/array_interface.h | 5 - 14 files changed, 438 insertions(+), 9 deletions(-) create mode 100644 R-package/man/xgb.get.DMatrix.data.Rd create mode 100644 R-package/man/xgb.get.DMatrix.num.non.missing.Rd create mode 100644 R-package/man/xgb.get.DMatrix.qcut.Rd create mode 100644 src/data/array_interface.cc diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index 40ede23a5..e6f7a82b8 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -37,6 +37,9 @@ export(xgb.create.features) export(xgb.cv) export(xgb.dump) export(xgb.gblinear.history) +export(xgb.get.DMatrix.data) +export(xgb.get.DMatrix.num.non.missing) +export(xgb.get.DMatrix.qcut) export(xgb.get.config) export(xgb.ggplot.deepness) export(xgb.ggplot.importance) @@ -60,6 +63,7 @@ export(xgb.unserialize) export(xgboost) import(methods) importClassesFrom(Matrix,dgCMatrix) +importClassesFrom(Matrix,dgRMatrix) importClassesFrom(Matrix,dgeMatrix) importFrom(Matrix,colSums) importFrom(Matrix,sparse.model.matrix) @@ -83,6 +87,7 @@ importFrom(graphics,points) importFrom(graphics,title) importFrom(jsonlite,fromJSON) importFrom(jsonlite,toJSON) +importFrom(methods,new) importFrom(stats,median) importFrom(stats,predict) importFrom(utils,head) diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index 6acd1e6b2..cc16e18da 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -526,6 +526,111 @@ setinfo.xgb.DMatrix <- function(object, name, info) { stop("setinfo: unknown info name ", name) } +#' @title Get Quantile Cuts from DMatrix +#' @description Get the quantile cuts (a.k.a. borders) from an `xgb.DMatrix` +#' that has been quantized for the histogram method (`tree_method="hist"`). +#' +#' These cuts are used in order to assign observations to bins - i.e. these are ordered +#' boundaries which are used to determine assignment condition `border_low < x < border_high`. +#' As such, the first and last bin will be outside of the range of the data, so as to include +#' all of the observations there. +#' +#' If a given column has 'n' bins, then there will be 'n+1' cuts / borders for that column, +#' which will be output in sorted order from lowest to highest. +#' +#' Different columns can have different numbers of bins according to their range. +#' @param dmat An `xgb.DMatrix` object, as returned by \link{xgb.DMatrix}. +#' @param output Output format for the quantile cuts. Possible options are:\itemize{ +#' \item `"list"` will return the output as a list with one entry per column, where +#' each column will have a numeric vector with the cuts. The list will be named if +#' `dmat` has column names assigned to it. +#' \item `"arrays"` will return a list with entries `indptr` (base-0 indexing) and +#' `data`. Here, the cuts for column 'i' are obtained by slicing 'data' from entries +#' `indptr[i]+1` to `indptr[i+1]`. +#' } +#' @return The quantile cuts, in the format specified by parameter `output`. +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars$mpg +#' x <- as.matrix(mtcars[, -1]) +#' dm <- xgb.DMatrix(x, label = y, nthread = 1) +#' +#' # DMatrix is not quantized right away, but will be once a hist model is generated +#' model <- xgb.train( +#' data = dm, +#' params = list( +#' tree_method = "hist", +#' max_bin = 8, +#' nthread = 1 +#' ), +#' nrounds = 3 +#' ) +#' +#' # Now can get the quantile cuts +#' xgb.get.DMatrix.qcut(dm) +#' @export +xgb.get.DMatrix.qcut <- function(dmat, output = c("list", "arrays")) { # nolint + stopifnot(inherits(dmat, "xgb.DMatrix")) + output <- head(output, 1L) + stopifnot(output %in% c("list", "arrays")) + res <- .Call(XGDMatrixGetQuantileCut_R, dmat) + if (output == "arrays") { + return(res) + } else { + feature_names <- getinfo(dmat, "feature_name") + ncols <- length(res$indptr) - 1 + out <- lapply( + seq(1, ncols), + function(col) { + st <- res$indptr[col] + end <- res$indptr[col + 1] + if (end <= st) { + return(numeric()) + } + return(res$data[seq(1 + st, end)]) + } + ) + if (NROW(feature_names)) { + names(out) <- feature_names + } + return(out) + } +} + +#' @title Get Number of Non-Missing Entries in DMatrix +#' @param dmat An `xgb.DMatrix` object, as returned by \link{xgb.DMatrix}. +#' @return The number of non-missing entries in the DMatrix +#' @export +xgb.get.DMatrix.num.non.missing <- function(dmat) { # nolint + stopifnot(inherits(dmat, "xgb.DMatrix")) + return(.Call(XGDMatrixNumNonMissing_R, dmat)) +} + +#' @title Get DMatrix Data +#' @param dmat An `xgb.DMatrix` object, as returned by \link{xgb.DMatrix}. +#' @return The data held in the DMatrix, as a sparse CSR matrix (class `dgRMatrix` +#' from package `Matrix`). If it had feature names, these will be added as column names +#' in the output. +#' @export +xgb.get.DMatrix.data <- function(dmat) { + stopifnot(inherits(dmat, "xgb.DMatrix")) + res <- .Call(XGDMatrixGetDataAsCSR_R, dmat) + out <- methods::new("dgRMatrix") + nrows <- as.integer(length(res$indptr) - 1) + out@p <- res$indptr + out@j <- res$indices + out@x <- res$data + out@Dim <- as.integer(c(nrows, res$ncols)) + + feature_names <- getinfo(dmat, "feature_name") + dim_names <- list(NULL, NULL) + if (NROW(feature_names)) { + dim_names[[2L]] <- feature_names + } + out@Dimnames <- dim_names + return(out) +} #' Get a new DMatrix containing the specified rows of #' original xgb.DMatrix object diff --git a/R-package/R/xgboost.R b/R-package/R/xgboost.R index f61c535e2..af6253a72 100644 --- a/R-package/R/xgboost.R +++ b/R-package/R/xgboost.R @@ -82,7 +82,7 @@ NULL NULL # Various imports -#' @importClassesFrom Matrix dgCMatrix dgeMatrix +#' @importClassesFrom Matrix dgCMatrix dgeMatrix dgRMatrix #' @importFrom Matrix colSums #' @importFrom Matrix sparse.model.matrix #' @importFrom Matrix sparseVector @@ -98,6 +98,7 @@ NULL #' @importFrom data.table setnames #' @importFrom jsonlite fromJSON #' @importFrom jsonlite toJSON +#' @importFrom methods new #' @importFrom utils object.size str tail #' @importFrom stats predict #' @importFrom stats median diff --git a/R-package/man/xgb.get.DMatrix.data.Rd b/R-package/man/xgb.get.DMatrix.data.Rd new file mode 100644 index 000000000..36783f583 --- /dev/null +++ b/R-package/man/xgb.get.DMatrix.data.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.get.DMatrix.data} +\alias{xgb.get.DMatrix.data} +\title{Get DMatrix Data} +\usage{ +xgb.get.DMatrix.data(dmat) +} +\arguments{ +\item{dmat}{An \code{xgb.DMatrix} object, as returned by \link{xgb.DMatrix}.} +} +\value{ +The data held in the DMatrix, as a sparse CSR matrix (class \code{dgRMatrix} +from package \code{Matrix}). If it had feature names, these will be added as column names +in the output. +} +\description{ +Get DMatrix Data +} diff --git a/R-package/man/xgb.get.DMatrix.num.non.missing.Rd b/R-package/man/xgb.get.DMatrix.num.non.missing.Rd new file mode 100644 index 000000000..4eb2697f8 --- /dev/null +++ b/R-package/man/xgb.get.DMatrix.num.non.missing.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.get.DMatrix.num.non.missing} +\alias{xgb.get.DMatrix.num.non.missing} +\title{Get Number of Non-Missing Entries in DMatrix} +\usage{ +xgb.get.DMatrix.num.non.missing(dmat) +} +\arguments{ +\item{dmat}{An \code{xgb.DMatrix} object, as returned by \link{xgb.DMatrix}.} +} +\value{ +The number of non-missing entries in the DMatrix +} +\description{ +Get Number of Non-Missing Entries in DMatrix +} diff --git a/R-package/man/xgb.get.DMatrix.qcut.Rd b/R-package/man/xgb.get.DMatrix.qcut.Rd new file mode 100644 index 000000000..8f7c3da75 --- /dev/null +++ b/R-package/man/xgb.get.DMatrix.qcut.Rd @@ -0,0 +1,58 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.DMatrix.R +\name{xgb.get.DMatrix.qcut} +\alias{xgb.get.DMatrix.qcut} +\title{Get Quantile Cuts from DMatrix} +\usage{ +xgb.get.DMatrix.qcut(dmat, output = c("list", "arrays")) +} +\arguments{ +\item{dmat}{An \code{xgb.DMatrix} object, as returned by \link{xgb.DMatrix}.} + +\item{output}{Output format for the quantile cuts. Possible options are:\itemize{ +\item \code{"list"} will return the output as a list with one entry per column, where +each column will have a numeric vector with the cuts. The list will be named if +\code{dmat} has column names assigned to it. +\item \code{"arrays"} will return a list with entries \code{indptr} (base-0 indexing) and +\code{data}. Here, the cuts for column 'i' are obtained by slicing 'data' from entries +\code{indptr[i]+1} to \code{indptr[i+1]}. +}} +} +\value{ +The quantile cuts, in the format specified by parameter \code{output}. +} +\description{ +Get the quantile cuts (a.k.a. borders) from an \code{xgb.DMatrix} +that has been quantized for the histogram method (\code{tree_method="hist"}). + +These cuts are used in order to assign observations to bins - i.e. these are ordered +boundaries which are used to determine assignment condition \verb{border_low < x < border_high}. +As such, the first and last bin will be outside of the range of the data, so as to include +all of the observations there. + +If a given column has 'n' bins, then there will be 'n+1' cuts / borders for that column, +which will be output in sorted order from lowest to highest. + +Different columns can have different numbers of bins according to their range. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars$mpg +x <- as.matrix(mtcars[, -1]) +dm <- xgb.DMatrix(x, label = y, nthread = 1) + +# DMatrix is not quantized right away, but will be once a hist model is generated +model <- xgb.train( + data = dm, + params = list( + tree_method = "hist", + max_bin = 8, + nthread = 1 + ), + nrounds = 3 +) + +# Now can get the quantile cuts +xgb.get.DMatrix.qcut(dm) +} diff --git a/R-package/src/Makevars.in b/R-package/src/Makevars.in index 8af5dbbf6..dd13983f5 100644 --- a/R-package/src/Makevars.in +++ b/R-package/src/Makevars.in @@ -63,6 +63,7 @@ OBJECTS= \ $(PKGROOT)/src/gbm/gblinear.o \ $(PKGROOT)/src/gbm/gblinear_model.o \ $(PKGROOT)/src/data/adapter.o \ + $(PKGROOT)/src/data/array_interface.o \ $(PKGROOT)/src/data/simple_dmatrix.o \ $(PKGROOT)/src/data/data.o \ $(PKGROOT)/src/data/sparse_page_raw_format.o \ diff --git a/R-package/src/Makevars.win b/R-package/src/Makevars.win index 60f754fef..46a862711 100644 --- a/R-package/src/Makevars.win +++ b/R-package/src/Makevars.win @@ -63,6 +63,7 @@ OBJECTS= \ $(PKGROOT)/src/gbm/gblinear.o \ $(PKGROOT)/src/gbm/gblinear_model.o \ $(PKGROOT)/src/data/adapter.o \ + $(PKGROOT)/src/data/array_interface.o \ $(PKGROOT)/src/data/simple_dmatrix.o \ $(PKGROOT)/src/data/data.o \ $(PKGROOT)/src/data/sparse_page_raw_format.o \ diff --git a/R-package/src/init.c b/R-package/src/init.c index f957229af..5eee8ebe6 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -45,6 +45,9 @@ extern SEXP XGDMatrixCreateFromDF_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixGetStrFeatureInfo_R(SEXP, SEXP); extern SEXP XGDMatrixNumCol_R(SEXP); extern SEXP XGDMatrixNumRow_R(SEXP); +extern SEXP XGDMatrixGetQuantileCut_R(SEXP); +extern SEXP XGDMatrixNumNonMissing_R(SEXP); +extern SEXP XGDMatrixGetDataAsCSR_R(SEXP); extern SEXP XGDMatrixSaveBinary_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixSetInfo_R(SEXP, SEXP, SEXP); extern SEXP XGDMatrixSetStrFeatureInfo_R(SEXP, SEXP, SEXP); @@ -84,6 +87,9 @@ static const R_CallMethodDef CallEntries[] = { {"XGDMatrixGetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixGetStrFeatureInfo_R, 2}, {"XGDMatrixNumCol_R", (DL_FUNC) &XGDMatrixNumCol_R, 1}, {"XGDMatrixNumRow_R", (DL_FUNC) &XGDMatrixNumRow_R, 1}, + {"XGDMatrixGetQuantileCut_R", (DL_FUNC) &XGDMatrixGetQuantileCut_R, 1}, + {"XGDMatrixNumNonMissing_R", (DL_FUNC) &XGDMatrixNumNonMissing_R, 1}, + {"XGDMatrixGetDataAsCSR_R", (DL_FUNC) &XGDMatrixGetDataAsCSR_R, 1}, {"XGDMatrixSaveBinary_R", (DL_FUNC) &XGDMatrixSaveBinary_R, 3}, {"XGDMatrixSetInfo_R", (DL_FUNC) &XGDMatrixSetInfo_R, 3}, {"XGDMatrixSetStrFeatureInfo_R", (DL_FUNC) &XGDMatrixSetStrFeatureInfo_R, 3}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index fb05c33b4..60a3fe68b 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -1,5 +1,5 @@ /** - * Copyright 2014-2023 by XGBoost Contributors + * Copyright 2014-2024, XGBoost Contributors */ #include #include @@ -9,9 +9,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -20,14 +22,14 @@ #include "../../src/c_api/c_api_error.h" #include "../../src/c_api/c_api_utils.h" // MakeSparseFromPtr #include "../../src/common/threading_utils.h" +#include "../../src/data/array_interface.h" // for ArrayInterface #include "./xgboost_R.h" // Must follow other includes. namespace { - struct ErrorWithUnwind : public std::exception {}; -void ThrowExceptionFromRError(void *unused, Rboolean jump) { +void ThrowExceptionFromRError(void *, Rboolean jump) { if (jump) { throw ErrorWithUnwind(); } @@ -49,6 +51,30 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { continuation_token); } +SEXP WrappedAllocReal(void *void_ptr) { + size_t *size = static_cast(void_ptr); + return Rf_allocVector(REALSXP, *size); +} + +SEXP SafeAllocReal(size_t size, SEXP continuation_token) { + return R_UnwindProtect( + WrappedAllocReal, static_cast(&size), + ThrowExceptionFromRError, nullptr, + continuation_token); +} + +SEXP WrappedAllocInteger(void *void_ptr) { + size_t *size = static_cast(void_ptr); + return Rf_allocVector(INTSXP, *size); +} + +SEXP SafeAllocInteger(size_t size, SEXP continuation_token) { + return R_UnwindProtect( + WrappedAllocInteger, static_cast(&size), + ThrowExceptionFromRError, nullptr, + continuation_token); +} + [[nodiscard]] std::string MakeArrayInterfaceFromRMat(SEXP R_mat) { SEXP mat_dims = Rf_getAttrib(R_mat, R_DimSymbol); if (Rf_xlength(mat_dims) > 2) { @@ -136,6 +162,37 @@ SEXP SafeMkChar(const char *c_str, SEXP continuation_token) { jconfig["nthread"] = Rf_asInteger(n_threads); return Json::Dump(jconfig); } + +// Allocate a R vector and copy an array interface encoded object to it. +[[nodiscard]] SEXP CopyArrayToR(const char *array_str, SEXP ctoken) { + xgboost::ArrayInterface<1> array{xgboost::StringView{array_str}}; + // R supports only int and double. + bool is_int = + xgboost::DispatchDType(array.type, [](auto t) { return std::is_integral_v; }); + bool is_float = xgboost::DispatchDType( + array.type, [](auto v) { return std::is_floating_point_v; }); + CHECK(is_int || is_float) << "Internal error: Invalid DType."; + CHECK(array.is_contiguous) << "Internal error: Return by XGBoost should be contiguous"; + + // Allocate memory in R + SEXP out = + Rf_protect(is_int ? SafeAllocInteger(array.n, ctoken) : SafeAllocReal(array.n, ctoken)); + + xgboost::DispatchDType(array.type, [&](auto t) { + using T = decltype(t); + auto in_ptr = static_cast(array.data); + if (is_int) { + auto out_ptr = INTEGER(out); + std::copy_n(in_ptr, array.n, out_ptr); + } else { + auto out_ptr = REAL(out); + std::copy_n(in_ptr, array.n, out_ptr); + } + }); + + Rf_unprotect(1); + return out; +} } // namespace struct RRNGStateController { @@ -540,6 +597,73 @@ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle) { return ScalarInteger(static_cast(ncol)); } +XGB_DLL SEXP XGDMatrixGetQuantileCut_R(SEXP handle) { + const char *out_names[] = {"indptr", "data", ""}; + SEXP continuation_token = Rf_protect(R_MakeUnwindCont()); + SEXP out = Rf_protect(Rf_mkNamed(VECSXP, out_names)); + R_API_BEGIN(); + const char *out_indptr; + const char *out_data; + CHECK_CALL(XGDMatrixGetQuantileCut(R_ExternalPtrAddr(handle), "{}", &out_indptr, &out_data)); + try { + SET_VECTOR_ELT(out, 0, CopyArrayToR(out_indptr, continuation_token)); + SET_VECTOR_ELT(out, 1, CopyArrayToR(out_data, continuation_token)); + } catch (ErrorWithUnwind &e) { + R_ContinueUnwind(continuation_token); + } + R_API_END(); + Rf_unprotect(2); + return out; +} + +XGB_DLL SEXP XGDMatrixNumNonMissing_R(SEXP handle) { + SEXP out = Rf_protect(Rf_allocVector(REALSXP, 1)); + R_API_BEGIN(); + bst_ulong out_; + CHECK_CALL(XGDMatrixNumNonMissing(R_ExternalPtrAddr(handle), &out_)); + REAL(out)[0] = static_cast(out_); + R_API_END(); + Rf_unprotect(1); + return out; +} + +XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle) { + const char *out_names[] = {"indptr", "indices", "data", "ncols", ""}; + SEXP out = Rf_protect(Rf_mkNamed(VECSXP, out_names)); + R_API_BEGIN(); + + bst_ulong nrows, ncols, nnz; + CHECK_CALL(XGDMatrixNumRow(R_ExternalPtrAddr(handle), &nrows)); + CHECK_CALL(XGDMatrixNumCol(R_ExternalPtrAddr(handle), &ncols)); + CHECK_CALL(XGDMatrixNumNonMissing(R_ExternalPtrAddr(handle), &nnz)); + if (std::max(nrows, ncols) > std::numeric_limits::max()) { + Rf_error("%s", "Error: resulting DMatrix data does not fit into R 'dgRMatrix'."); + } + + SET_VECTOR_ELT(out, 0, Rf_allocVector(INTSXP, nrows + 1)); + SET_VECTOR_ELT(out, 1, Rf_allocVector(INTSXP, nnz)); + SET_VECTOR_ELT(out, 2, Rf_allocVector(REALSXP, nnz)); + SET_VECTOR_ELT(out, 3, Rf_ScalarInteger(ncols)); + + std::unique_ptr indptr(new bst_ulong[nrows + 1]); + std::unique_ptr indices(new unsigned[nnz]); + std::unique_ptr data(new float[nnz]); + + CHECK_CALL(XGDMatrixGetDataAsCSR(R_ExternalPtrAddr(handle), + "{}", + indptr.get(), + indices.get(), + data.get())); + + std::copy(indptr.get(), indptr.get() + nrows + 1, INTEGER(VECTOR_ELT(out, 0))); + std::copy(indices.get(), indices.get() + nnz, INTEGER(VECTOR_ELT(out, 1))); + std::copy(data.get(), data.get() + nnz, REAL(VECTOR_ELT(out, 2))); + + R_API_END(); + Rf_unprotect(1); + return out; +} + // functions related to booster void _BoosterFinalizer(SEXP ext) { if (R_ExternalPtrAddr(ext) == NULL) return; diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 2e874e3a6..4e3458957 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -143,6 +143,31 @@ XGB_DLL SEXP XGDMatrixNumRow_R(SEXP handle); */ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle); +/*! + * \brief return the quantile cuts used for the histogram method + * \param handle an instance of data matrix + * \return A list with entries 'indptr' and 'data' + */ +XGB_DLL SEXP XGDMatrixGetQuantileCut_R(SEXP handle); + +/*! + * \brief get the number of non-missing entries in a dmatrix + * \param handle an instance of data matrix + * \return the number of non-missing entries + */ +XGB_DLL SEXP XGDMatrixNumNonMissing_R(SEXP handle); + +/*! + * \brief get the data in a dmatrix in CSR format + * \param handle an instance of data matrix + * \return R list with the following entries in this order: + * - 'indptr + * - 'indices + * - 'data' + * - 'ncol' + */ +XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle); + /*! * \brief create xgboost learner * \param dmats a list of dmatrix handles that will be cached diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 55a699687..81ac884d0 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -375,3 +375,62 @@ test_that("xgb.DMatrix: can take multi-dimensional 'base_margin'", { ) expect_equal(pred_only_x, pred_w_base - b, tolerance = 1e-5) }) + +test_that("xgb.DMatrix: number of non-missing matches data", { + x <- matrix(1:10, nrow = 5) + dm1 <- xgb.DMatrix(x) + expect_equal(xgb.get.DMatrix.num.non.missing(dm1), 10) + + x[2, 2] <- NA + x[4, 1] <- NA + dm2 <- xgb.DMatrix(x) + expect_equal(xgb.get.DMatrix.num.non.missing(dm2), 8) +}) + +test_that("xgb.DMatrix: retrieving data as CSR", { + data(mtcars) + dm <- xgb.DMatrix(as.matrix(mtcars)) + csr <- xgb.get.DMatrix.data(dm) + expect_equal(dim(csr), dim(mtcars)) + expect_equal(colnames(csr), colnames(mtcars)) + expect_equal(unname(as.matrix(csr)), unname(as.matrix(mtcars)), tolerance = 1e-6) +}) + +test_that("xgb.DMatrix: quantile cuts look correct", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y) + model <- xgb.train( + data = dm, + params = list( + tree_method = "hist", + max_bin = 8, + nthread = 1 + ), + nrounds = 3 + ) + qcut_list <- xgb.get.DMatrix.qcut(dm, "list") + qcut_arrays <- xgb.get.DMatrix.qcut(dm, "arrays") + + expect_equal(length(qcut_arrays), 2) + expect_equal(names(qcut_arrays), c("indptr", "data")) + expect_equal(length(qcut_arrays$indptr), ncol(x) + 1) + expect_true(min(diff(qcut_arrays$indptr)) > 0) + + col_min <- apply(x, 2, min) + col_max <- apply(x, 2, max) + + expect_equal(length(qcut_list), ncol(x)) + expect_equal(names(qcut_list), colnames(x)) + lapply( + seq(1, ncol(x)), + function(col) { + cuts <- qcut_list[[col]] + expect_true(min(diff(cuts)) > 0) + expect_true(col_min[col] > cuts[1]) + expect_true(col_max[col] < cuts[length(cuts)]) + expect_true(length(cuts) <= 9) + } + ) +}) diff --git a/src/data/array_interface.cc b/src/data/array_interface.cc new file mode 100644 index 000000000..06b9ed00c --- /dev/null +++ b/src/data/array_interface.cc @@ -0,0 +1,13 @@ +/** + * Copyright 2019-2024, XGBoost Contributors + */ +#include "array_interface.h" + +#include "../common/common.h" // for AssertGPUSupport + +namespace xgboost { +#if !defined(XGBOOST_USE_CUDA) +void ArrayInterfaceHandler::SyncCudaStream(int64_t) { common::AssertGPUSupport(); } +bool ArrayInterfaceHandler::IsCudaPtr(void const *) { return false; } +#endif // !defined(XGBOOST_USE_CUDA) +} // namespace xgboost diff --git a/src/data/array_interface.h b/src/data/array_interface.h index 0170e6a84..6f2438f37 100644 --- a/src/data/array_interface.h +++ b/src/data/array_interface.h @@ -375,11 +375,6 @@ struct ToDType { static constexpr ArrayInterfaceHandler::Type kType = ArrayInterfaceHandler::kI8; }; -#if !defined(XGBOOST_USE_CUDA) -inline void ArrayInterfaceHandler::SyncCudaStream(int64_t) { common::AssertGPUSupport(); } -inline bool ArrayInterfaceHandler::IsCudaPtr(void const *) { return false; } -#endif // !defined(XGBOOST_USE_CUDA) - /** * \brief A type erased view over __array_interface__ protocol defined by numpy * From 9f73127a238c19aeb95954a41519de56d267186e Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 13:15:18 +0800 Subject: [PATCH 074/109] Cleanup Python GPU tests. (#9934) * Cleanup Python GPU tests. - Remove the use of `gpu_hist` and `gpu_id` in cudf/cupy tests. - Move base margin test into the testing directory. --- demo/guide-python/individual_trees.py | 2 +- python-package/xgboost/testing/data.py | 60 ++++++- tests/ci_build/lint_python.py | 9 +- .../test_device_quantile_dmatrix.py | 5 +- tests/python-gpu/test_from_cudf.py | 163 +++++++++++------- tests/python-gpu/test_from_cupy.py | 96 +++++------ tests/python-gpu/test_gpu_basic_models.py | 18 +- tests/python-gpu/test_gpu_demos.py | 12 +- tests/python-gpu/test_gpu_linear.py | 55 +++--- .../test_gpu_training_continuation.py | 17 +- .../python-gpu/test_monotonic_constraints.py | 21 +-- tests/python/test_dmatrix.py | 54 +----- tests/python/test_with_modin.py | 4 +- tests/python/test_with_pandas.py | 6 +- 14 files changed, 282 insertions(+), 240 deletions(-) diff --git a/demo/guide-python/individual_trees.py b/demo/guide-python/individual_trees.py index d940e8521..93a9aad2b 100644 --- a/demo/guide-python/individual_trees.py +++ b/demo/guide-python/individual_trees.py @@ -58,7 +58,7 @@ def individual_tree() -> None: def model_slices() -> None: - """Inference with each individual using model slices.""" + """Inference with each individual tree using model slices.""" X_train, y_train = load_svmlight_file(train) X_test, y_test = load_svmlight_file(test) Xy_train = xgb.QuantileDMatrix(X_train, y_train) diff --git a/python-package/xgboost/testing/data.py b/python-package/xgboost/testing/data.py index b5f07d981..c0d53f914 100644 --- a/python-package/xgboost/testing/data.py +++ b/python-package/xgboost/testing/data.py @@ -3,7 +3,17 @@ import os import zipfile from dataclasses import dataclass -from typing import Any, Generator, List, NamedTuple, Optional, Tuple, Union +from typing import ( + Any, + Callable, + Generator, + List, + NamedTuple, + Optional, + Tuple, + Type, + Union, +) from urllib import request import numpy as np @@ -603,3 +613,51 @@ def sort_ltr_samples( data = X, clicks, y, qid return data + + +def run_base_margin_info( + DType: Callable, DMatrixT: Type[xgboost.DMatrix], device: str +) -> None: + """Run tests for base margin.""" + rng = np.random.default_rng() + X = DType(rng.normal(0, 1.0, size=100).astype(np.float32).reshape(50, 2)) + if hasattr(X, "iloc"): + y = X.iloc[:, 0] + else: + y = X[:, 0] + base_margin = X + # no error at set + Xy = DMatrixT(X, y, base_margin=base_margin) + # Error at train, caused by check in predictor. + with pytest.raises(ValueError, match=r".*base_margin.*"): + xgboost.train({"tree_method": "hist", "device": device}, Xy) + + if not hasattr(X, "iloc"): + # column major matrix + got = DType(Xy.get_base_margin().reshape(50, 2)) + assert (got == base_margin).all() + + assert base_margin.T.flags.c_contiguous is False + assert base_margin.T.flags.f_contiguous is True + Xy.set_info(base_margin=base_margin.T) + got = DType(Xy.get_base_margin().reshape(2, 50)) + assert (got == base_margin.T).all() + + # Row vs col vec. + base_margin = y + Xy.set_base_margin(base_margin) + bm_col = Xy.get_base_margin() + Xy.set_base_margin(base_margin.reshape(1, base_margin.size)) + bm_row = Xy.get_base_margin() + assert (bm_row == bm_col).all() + + # type + base_margin = base_margin.astype(np.float64) + Xy.set_base_margin(base_margin) + bm_f64 = Xy.get_base_margin() + assert (bm_f64 == bm_col).all() + + # too many dimensions + base_margin = X.reshape(2, 5, 2, 5) + with pytest.raises(ValueError, match=r".*base_margin.*"): + Xy.set_base_margin(base_margin) diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index fdd643da0..c9170b100 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -27,13 +27,8 @@ class LintersPaths: "tests/python/test_tree_regularization.py", "tests/python/test_shap.py", "tests/python/test_with_pandas.py", - "tests/python-gpu/test_gpu_data_iterator.py", - "tests/python-gpu/test_gpu_prediction.py", - "tests/python-gpu/load_pickle.py", - "tests/python-gpu/test_gpu_pickling.py", - "tests/python-gpu/test_gpu_eval_metrics.py", - "tests/python-gpu/test_gpu_with_sklearn.py", - "tests/python-sycl/test_sycl_prediction.py", + "tests/python-gpu/", + "tests/python-sycl/", "tests/test_distributed/test_with_spark/", "tests/test_distributed/test_gpu_with_spark/", # demo diff --git a/tests/python-gpu/test_device_quantile_dmatrix.py b/tests/python-gpu/test_device_quantile_dmatrix.py index 4cfc61321..6b8ceac4a 100644 --- a/tests/python-gpu/test_device_quantile_dmatrix.py +++ b/tests/python-gpu/test_device_quantile_dmatrix.py @@ -203,9 +203,7 @@ class TestQuantileDMatrix: np.testing.assert_equal(h_ret.indptr, d_ret.indptr) np.testing.assert_equal(h_ret.indices, d_ret.indices) - booster = xgb.train( - {"tree_method": "hist", "device": "cuda:0"}, dtrain=d_m - ) + booster = xgb.train({"tree_method": "hist", "device": "cuda:0"}, dtrain=d_m) np.testing.assert_allclose( booster.predict(d_m), @@ -215,6 +213,7 @@ class TestQuantileDMatrix: def test_ltr(self) -> None: import cupy as cp + X, y, qid, w = tm.make_ltr(100, 3, 3, 5) # make sure GPU is used to run sketching. cpX = cp.array(X) diff --git a/tests/python-gpu/test_from_cudf.py b/tests/python-gpu/test_from_cudf.py index 610c717a9..8707af0c8 100644 --- a/tests/python-gpu/test_from_cudf.py +++ b/tests/python-gpu/test_from_cudf.py @@ -1,19 +1,17 @@ import json -import sys import numpy as np import pytest import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.data import run_base_margin_info -sys.path.append("tests/python") -from test_dmatrix import set_base_margin_info +cudf = pytest.importorskip("cudf") def dmatrix_from_cudf(input_type, DMatrixT, missing=np.NAN): - '''Test constructing DMatrix from cudf''' - import cudf + """Test constructing DMatrix from cudf""" import pandas as pd kRows = 80 @@ -25,9 +23,7 @@ def dmatrix_from_cudf(input_type, DMatrixT, missing=np.NAN): na[5, 0] = missing na[3, 1] = missing - pa = pd.DataFrame({'0': na[:, 0], - '1': na[:, 1], - '2': na[:, 2].astype(np.int32)}) + pa = pd.DataFrame({"0": na[:, 0], "1": na[:, 1], "2": na[:, 2].astype(np.int32)}) np_label = np.random.randn(kRows).astype(input_type) pa_label = pd.DataFrame(np_label) @@ -41,8 +37,7 @@ def dmatrix_from_cudf(input_type, DMatrixT, missing=np.NAN): def _test_from_cudf(DMatrixT): - '''Test constructing DMatrix from cudf''' - import cudf + """Test constructing DMatrix from cudf""" dmatrix_from_cudf(np.float32, DMatrixT, np.NAN) dmatrix_from_cudf(np.float64, DMatrixT, np.NAN) @@ -50,37 +45,38 @@ def _test_from_cudf(DMatrixT): dmatrix_from_cudf(np.int32, DMatrixT, -2) dmatrix_from_cudf(np.int64, DMatrixT, -3) - cd = cudf.DataFrame({'x': [1, 2, 3], 'y': [0.1, 0.2, 0.3]}) + cd = cudf.DataFrame({"x": [1, 2, 3], "y": [0.1, 0.2, 0.3]}) dtrain = DMatrixT(cd) - assert dtrain.feature_names == ['x', 'y'] - assert dtrain.feature_types == ['int', 'float'] + assert dtrain.feature_names == ["x", "y"] + assert dtrain.feature_types == ["int", "float"] - series = cudf.DataFrame({'x': [1, 2, 3]}).iloc[:, 0] + series = cudf.DataFrame({"x": [1, 2, 3]}).iloc[:, 0] assert isinstance(series, cudf.Series) dtrain = DMatrixT(series) - assert dtrain.feature_names == ['x'] - assert dtrain.feature_types == ['int'] + assert dtrain.feature_names == ["x"] + assert dtrain.feature_types == ["int"] with pytest.raises(ValueError, match=r".*multi.*"): dtrain = DMatrixT(cd, label=cd) - xgb.train({"tree_method": "gpu_hist", "objective": "multi:softprob"}, dtrain) + xgb.train( + {"tree_method": "hist", "device": "cuda", "objective": "multi:softprob"}, + dtrain, + ) # Test when number of elements is less than 8 - X = cudf.DataFrame({'x': cudf.Series([0, 1, 2, np.NAN, 4], - dtype=np.int32)}) + X = cudf.DataFrame({"x": cudf.Series([0, 1, 2, np.NAN, 4], dtype=np.int32)}) dtrain = DMatrixT(X) assert dtrain.num_col() == 1 assert dtrain.num_row() == 5 # Boolean is not supported. - X_boolean = cudf.DataFrame({'x': cudf.Series([True, False])}) + X_boolean = cudf.DataFrame({"x": cudf.Series([True, False])}) with pytest.raises(Exception): dtrain = DMatrixT(X_boolean) - y_boolean = cudf.DataFrame({ - 'x': cudf.Series([True, False, True, True, True])}) + y_boolean = cudf.DataFrame({"x": cudf.Series([True, False, True, True, True])}) with pytest.raises(Exception): dtrain = DMatrixT(X_boolean, label=y_boolean) @@ -88,6 +84,7 @@ def _test_from_cudf(DMatrixT): def _test_cudf_training(DMatrixT): import pandas as pd from cudf import DataFrame as df + np.random.seed(1) X = pd.DataFrame(np.random.randn(50, 10)) y = pd.DataFrame(np.random.randn(50)) @@ -97,21 +94,33 @@ def _test_cudf_training(DMatrixT): cudf_base_margin = df.from_pandas(pd.DataFrame(base_margin)) evals_result_cudf = {} - dtrain_cudf = DMatrixT(df.from_pandas(X), df.from_pandas(y), weight=cudf_weights, - base_margin=cudf_base_margin) - params = {'gpu_id': 0, 'tree_method': 'gpu_hist'} - xgb.train(params, dtrain_cudf, evals=[(dtrain_cudf, "train")], - evals_result=evals_result_cudf) + dtrain_cudf = DMatrixT( + df.from_pandas(X), + df.from_pandas(y), + weight=cudf_weights, + base_margin=cudf_base_margin, + ) + params = {"device": "cuda", "tree_method": "hist"} + xgb.train( + params, + dtrain_cudf, + evals=[(dtrain_cudf, "train")], + evals_result=evals_result_cudf, + ) evals_result_np = {} dtrain_np = xgb.DMatrix(X, y, weight=weights, base_margin=base_margin) - xgb.train(params, dtrain_np, evals=[(dtrain_np, "train")], - evals_result=evals_result_np) - assert np.array_equal(evals_result_cudf["train"]["rmse"], evals_result_np["train"]["rmse"]) + xgb.train( + params, dtrain_np, evals=[(dtrain_np, "train")], evals_result=evals_result_np + ) + assert np.array_equal( + evals_result_cudf["train"]["rmse"], evals_result_np["train"]["rmse"] + ) def _test_cudf_metainfo(DMatrixT): import pandas as pd from cudf import DataFrame as df + n = 100 X = np.random.random((n, 2)) dmat_cudf = DMatrixT(df.from_pandas(pd.DataFrame(X))) @@ -120,39 +129,53 @@ def _test_cudf_metainfo(DMatrixT): uints = np.array([4, 2, 8]).astype("uint32") cudf_floats = df.from_pandas(pd.DataFrame(floats)) cudf_uints = df.from_pandas(pd.DataFrame(uints)) - dmat.set_float_info('weight', floats) - dmat.set_float_info('label', floats) - dmat.set_float_info('base_margin', floats) - dmat.set_uint_info('group', uints) + dmat.set_float_info("weight", floats) + dmat.set_float_info("label", floats) + dmat.set_float_info("base_margin", floats) + dmat.set_uint_info("group", uints) dmat_cudf.set_info(weight=cudf_floats) dmat_cudf.set_info(label=cudf_floats) dmat_cudf.set_info(base_margin=cudf_floats) dmat_cudf.set_info(group=cudf_uints) # Test setting info with cudf DataFrame - assert np.array_equal(dmat.get_float_info('weight'), dmat_cudf.get_float_info('weight')) - assert np.array_equal(dmat.get_float_info('label'), dmat_cudf.get_float_info('label')) - assert np.array_equal(dmat.get_float_info('base_margin'), - dmat_cudf.get_float_info('base_margin')) - assert np.array_equal(dmat.get_uint_info('group_ptr'), dmat_cudf.get_uint_info('group_ptr')) + assert np.array_equal( + dmat.get_float_info("weight"), dmat_cudf.get_float_info("weight") + ) + assert np.array_equal( + dmat.get_float_info("label"), dmat_cudf.get_float_info("label") + ) + assert np.array_equal( + dmat.get_float_info("base_margin"), dmat_cudf.get_float_info("base_margin") + ) + assert np.array_equal( + dmat.get_uint_info("group_ptr"), dmat_cudf.get_uint_info("group_ptr") + ) # Test setting info with cudf Series dmat_cudf.set_info(weight=cudf_floats[cudf_floats.columns[0]]) dmat_cudf.set_info(label=cudf_floats[cudf_floats.columns[0]]) dmat_cudf.set_info(base_margin=cudf_floats[cudf_floats.columns[0]]) dmat_cudf.set_info(group=cudf_uints[cudf_uints.columns[0]]) - assert np.array_equal(dmat.get_float_info('weight'), dmat_cudf.get_float_info('weight')) - assert np.array_equal(dmat.get_float_info('label'), dmat_cudf.get_float_info('label')) - assert np.array_equal(dmat.get_float_info('base_margin'), - dmat_cudf.get_float_info('base_margin')) - assert np.array_equal(dmat.get_uint_info('group_ptr'), dmat_cudf.get_uint_info('group_ptr')) + assert np.array_equal( + dmat.get_float_info("weight"), dmat_cudf.get_float_info("weight") + ) + assert np.array_equal( + dmat.get_float_info("label"), dmat_cudf.get_float_info("label") + ) + assert np.array_equal( + dmat.get_float_info("base_margin"), dmat_cudf.get_float_info("base_margin") + ) + assert np.array_equal( + dmat.get_uint_info("group_ptr"), dmat_cudf.get_uint_info("group_ptr") + ) - set_base_margin_info(df, DMatrixT, "gpu_hist") + run_base_margin_info(df, DMatrixT, "cuda") class TestFromColumnar: - '''Tests for constructing DMatrix from data structure conforming Apache -Arrow specification.''' + """Tests for constructing DMatrix from data structure conforming Apache + Arrow specification.""" @pytest.mark.skipif(**tm.no_cudf()) def test_simple_dmatrix_from_cudf(self): @@ -180,7 +203,6 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cudf()) def test_cudf_categorical(self) -> None: - import cudf n_features = 30 _X, _y = tm.make_categorical(100, n_features, 17, False) X = cudf.from_pandas(_X) @@ -251,6 +273,7 @@ def test_cudf_training_with_sklearn(): import pandas as pd from cudf import DataFrame as df from cudf import Series as ss + np.random.seed(1) X = pd.DataFrame(np.random.randn(50, 10)) y = pd.DataFrame((np.random.randn(50) > 0).astype(np.int8)) @@ -264,29 +287,34 @@ def test_cudf_training_with_sklearn(): y_cudf_series = ss(data=y.iloc[:, 0]) for y_obj in [y_cudf, y_cudf_series]: - clf = xgb.XGBClassifier(gpu_id=0, tree_method='gpu_hist') - clf.fit(X_cudf, y_obj, sample_weight=cudf_weights, base_margin=cudf_base_margin, - eval_set=[(X_cudf, y_obj)]) + clf = xgb.XGBClassifier(tree_method="hist", device="cuda:0") + clf.fit( + X_cudf, + y_obj, + sample_weight=cudf_weights, + base_margin=cudf_base_margin, + eval_set=[(X_cudf, y_obj)], + ) pred = clf.predict(X_cudf) assert np.array_equal(np.unique(pred), np.array([0, 1])) class IterForDMatrixTest(xgb.core.DataIter): - '''A data iterator for XGBoost DMatrix. + """A data iterator for XGBoost DMatrix. `reset` and `next` are required for any data iterator, other functions here are utilites for demonstration's purpose. - ''' - ROWS_PER_BATCH = 100 # data is splited by rows + """ + + ROWS_PER_BATCH = 100 # data is splited by rows BATCHES = 16 def __init__(self, categorical): - '''Generate some random data for demostration. + """Generate some random data for demostration. Actual data can be anything that is currently supported by XGBoost. - ''' - import cudf + """ self.rows = self.ROWS_PER_BATCH if categorical: @@ -300,34 +328,37 @@ class IterForDMatrixTest(xgb.core.DataIter): rng = np.random.RandomState(1994) self._data = [ cudf.DataFrame( - {'a': rng.randn(self.ROWS_PER_BATCH), - 'b': rng.randn(self.ROWS_PER_BATCH)})] * self.BATCHES + { + "a": rng.randn(self.ROWS_PER_BATCH), + "b": rng.randn(self.ROWS_PER_BATCH), + } + ) + ] * self.BATCHES self._labels = [rng.randn(self.rows)] * self.BATCHES - self.it = 0 # set iterator to 0 + self.it = 0 # set iterator to 0 super().__init__(cache_prefix=None) def as_array(self): - import cudf return cudf.concat(self._data) def as_array_labels(self): return np.concatenate(self._labels) def data(self): - '''Utility function for obtaining current batch of data.''' + """Utility function for obtaining current batch of data.""" return self._data[self.it] def labels(self): - '''Utility function for obtaining current batch of label.''' + """Utility function for obtaining current batch of label.""" return self._labels[self.it] def reset(self): - '''Reset the iterator''' + """Reset the iterator""" self.it = 0 def next(self, input_data): - '''Yield next batch of data''' + """Yield next batch of data""" if self.it == len(self._data): # Return 0 when there's no more batch. return 0 @@ -341,7 +372,7 @@ class IterForDMatrixTest(xgb.core.DataIter): def test_from_cudf_iter(enable_categorical): rounds = 100 it = IterForDMatrixTest(enable_categorical) - params = {"tree_method": "gpu_hist"} + params = {"tree_method": "hist", "device": "cuda"} # Use iterator m_it = xgb.QuantileDMatrix(it, enable_categorical=enable_categorical) diff --git a/tests/python-gpu/test_from_cupy.py b/tests/python-gpu/test_from_cupy.py index b811ba090..79814a1bb 100644 --- a/tests/python-gpu/test_from_cupy.py +++ b/tests/python-gpu/test_from_cupy.py @@ -1,31 +1,25 @@ import json -import sys import numpy as np import pytest import xgboost as xgb - -sys.path.append("tests/python") -from test_dmatrix import set_base_margin_info - from xgboost import testing as tm +from xgboost.testing.data import run_base_margin_info -cupy = pytest.importorskip("cupy") +cp = pytest.importorskip("cupy") def test_array_interface() -> None: - arr = cupy.array([[1, 2, 3, 4], [1, 2, 3, 4]]) + arr = cp.array([[1, 2, 3, 4], [1, 2, 3, 4]]) i_arr = arr.__cuda_array_interface__ i_arr = json.loads(json.dumps(i_arr)) ret = xgb.core.from_array_interface(i_arr) - np.testing.assert_equal(cupy.asnumpy(arr), cupy.asnumpy(ret)) + np.testing.assert_equal(cp.asnumpy(arr), cp.asnumpy(ret)) def dmatrix_from_cupy(input_type, DMatrixT, missing=np.NAN): - '''Test constructing DMatrix from cupy''' - import cupy as cp - + """Test constructing DMatrix from cupy""" kRows = 80 kCols = 3 @@ -51,9 +45,7 @@ def dmatrix_from_cupy(input_type, DMatrixT, missing=np.NAN): def _test_from_cupy(DMatrixT): - '''Test constructing DMatrix from cupy''' - import cupy as cp - + """Test constructing DMatrix from cupy""" dmatrix_from_cupy(np.float16, DMatrixT, np.NAN) dmatrix_from_cupy(np.float32, DMatrixT, np.NAN) dmatrix_from_cupy(np.float64, DMatrixT, np.NAN) @@ -73,7 +65,6 @@ def _test_from_cupy(DMatrixT): def _test_cupy_training(DMatrixT): - import cupy as cp np.random.seed(1) cp.random.seed(1) X = cp.random.randn(50, 10, dtype="float32") @@ -85,19 +76,23 @@ def _test_cupy_training(DMatrixT): evals_result_cupy = {} dtrain_cp = DMatrixT(X, y, weight=cupy_weights, base_margin=cupy_base_margin) - params = {'gpu_id': 0, 'nthread': 1, 'tree_method': 'gpu_hist'} - xgb.train(params, dtrain_cp, evals=[(dtrain_cp, "train")], - evals_result=evals_result_cupy) + params = {"tree_method": "hist", "device": "cuda:0"} + xgb.train( + params, dtrain_cp, evals=[(dtrain_cp, "train")], evals_result=evals_result_cupy + ) evals_result_np = {} - dtrain_np = xgb.DMatrix(cp.asnumpy(X), cp.asnumpy(y), weight=weights, - base_margin=base_margin) - xgb.train(params, dtrain_np, evals=[(dtrain_np, "train")], - evals_result=evals_result_np) - assert np.array_equal(evals_result_cupy["train"]["rmse"], evals_result_np["train"]["rmse"]) + dtrain_np = xgb.DMatrix( + cp.asnumpy(X), cp.asnumpy(y), weight=weights, base_margin=base_margin + ) + xgb.train( + params, dtrain_np, evals=[(dtrain_np, "train")], evals_result=evals_result_np + ) + assert np.array_equal( + evals_result_cupy["train"]["rmse"], evals_result_np["train"]["rmse"] + ) def _test_cupy_metainfo(DMatrixT): - import cupy as cp n = 100 X = np.random.random((n, 2)) dmat_cupy = DMatrixT(cp.array(X)) @@ -106,33 +101,35 @@ def _test_cupy_metainfo(DMatrixT): uints = np.array([4, 2, 8]).astype("uint32") cupy_floats = cp.array(floats) cupy_uints = cp.array(uints) - dmat.set_float_info('weight', floats) - dmat.set_float_info('label', floats) - dmat.set_float_info('base_margin', floats) - dmat.set_uint_info('group', uints) + dmat.set_float_info("weight", floats) + dmat.set_float_info("label", floats) + dmat.set_float_info("base_margin", floats) + dmat.set_uint_info("group", uints) dmat_cupy.set_info(weight=cupy_floats) dmat_cupy.set_info(label=cupy_floats) dmat_cupy.set_info(base_margin=cupy_floats) dmat_cupy.set_info(group=cupy_uints) # Test setting info with cupy - assert np.array_equal(dmat.get_float_info('weight'), - dmat_cupy.get_float_info('weight')) - assert np.array_equal(dmat.get_float_info('label'), - dmat_cupy.get_float_info('label')) - assert np.array_equal(dmat.get_float_info('base_margin'), - dmat_cupy.get_float_info('base_margin')) - assert np.array_equal(dmat.get_uint_info('group_ptr'), - dmat_cupy.get_uint_info('group_ptr')) + assert np.array_equal( + dmat.get_float_info("weight"), dmat_cupy.get_float_info("weight") + ) + assert np.array_equal( + dmat.get_float_info("label"), dmat_cupy.get_float_info("label") + ) + assert np.array_equal( + dmat.get_float_info("base_margin"), dmat_cupy.get_float_info("base_margin") + ) + assert np.array_equal( + dmat.get_uint_info("group_ptr"), dmat_cupy.get_uint_info("group_ptr") + ) - set_base_margin_info(cp.asarray, DMatrixT, "gpu_hist") + run_base_margin_info(cp.asarray, DMatrixT, "cuda") @pytest.mark.skipif(**tm.no_cupy()) @pytest.mark.skipif(**tm.no_sklearn()) def test_cupy_training_with_sklearn(): - import cupy as cp - np.random.seed(1) cp.random.seed(1) X = cp.random.randn(50, 10, dtype="float32") @@ -142,7 +139,7 @@ def test_cupy_training_with_sklearn(): base_margin = np.random.random(50) cupy_base_margin = cp.array(base_margin) - clf = xgb.XGBClassifier(gpu_id=0, tree_method="gpu_hist") + clf = xgb.XGBClassifier(tree_method="hist", device="cuda:0") clf.fit( X, y, @@ -155,8 +152,8 @@ def test_cupy_training_with_sklearn(): class TestFromCupy: - '''Tests for constructing DMatrix from data structure conforming Apache -Arrow specification.''' + """Tests for constructing DMatrix from data structure conforming Apache + Arrow specification.""" @pytest.mark.skipif(**tm.no_cupy()) def test_simple_dmat_from_cupy(self): @@ -184,19 +181,17 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cupy()) def test_dlpack_simple_dmat(self): - import cupy as cp n = 100 X = cp.random.random((n, 2)) xgb.DMatrix(X.toDlpack()) @pytest.mark.skipif(**tm.no_cupy()) def test_cupy_categorical(self): - import cupy as cp n_features = 10 X, y = tm.make_categorical(10, n_features, n_categories=4, onehot=False) X = cp.asarray(X.values.astype(cp.float32)) y = cp.array(y) - feature_types = ['c'] * n_features + feature_types = ["c"] * n_features assert isinstance(X, cp.ndarray) Xy = xgb.DMatrix(X, y, feature_types=feature_types) @@ -204,7 +199,6 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cupy()) def test_dlpack_device_dmat(self): - import cupy as cp n = 100 X = cp.random.random((n, 2)) m = xgb.QuantileDMatrix(X.toDlpack()) @@ -213,7 +207,6 @@ Arrow specification.''' @pytest.mark.skipif(**tm.no_cupy()) def test_qid(self): - import cupy as cp rng = cp.random.RandomState(1994) rows = 100 cols = 10 @@ -223,19 +216,16 @@ Arrow specification.''' Xy = xgb.DMatrix(X, y) Xy.set_info(qid=qid) - group_ptr = Xy.get_uint_info('group_ptr') + group_ptr = Xy.get_uint_info("group_ptr") assert group_ptr[0] == 0 assert group_ptr[-1] == rows @pytest.mark.skipif(**tm.no_cupy()) @pytest.mark.mgpu def test_specified_device(self): - import cupy as cp cp.cuda.runtime.setDevice(0) dtrain = dmatrix_from_cupy(np.float32, xgb.QuantileDMatrix, np.nan) - with pytest.raises( - xgb.core.XGBoostError, match="Invalid device ordinal" - ): + with pytest.raises(xgb.core.XGBoostError, match="Invalid device ordinal"): xgb.train( - {'tree_method': 'gpu_hist', 'gpu_id': 1}, dtrain, num_boost_round=10 + {"tree_method": "hist", "device": "cuda:1"}, dtrain, num_boost_round=10 ) diff --git a/tests/python-gpu/test_gpu_basic_models.py b/tests/python-gpu/test_gpu_basic_models.py index e97ca210e..8b74c7971 100644 --- a/tests/python-gpu/test_gpu_basic_models.py +++ b/tests/python-gpu/test_gpu_basic_models.py @@ -21,21 +21,21 @@ class TestGPUBasicModels: cpu_test_bm = test_bm.TestModels() def run_cls(self, X, y): - cls = xgb.XGBClassifier(tree_method='gpu_hist') + cls = xgb.XGBClassifier(tree_method="hist", device="cuda") cls.fit(X, y) - cls.get_booster().save_model('test_deterministic_gpu_hist-0.json') + cls.get_booster().save_model("test_deterministic_gpu_hist-0.json") - cls = xgb.XGBClassifier(tree_method='gpu_hist') + cls = xgb.XGBClassifier(tree_method="hist", device="cuda") cls.fit(X, y) - cls.get_booster().save_model('test_deterministic_gpu_hist-1.json') + cls.get_booster().save_model("test_deterministic_gpu_hist-1.json") - with open('test_deterministic_gpu_hist-0.json', 'r') as fd: + with open("test_deterministic_gpu_hist-0.json", "r") as fd: model_0 = fd.read() - with open('test_deterministic_gpu_hist-1.json', 'r') as fd: + with open("test_deterministic_gpu_hist-1.json", "r") as fd: model_1 = fd.read() - os.remove('test_deterministic_gpu_hist-0.json') - os.remove('test_deterministic_gpu_hist-1.json') + os.remove("test_deterministic_gpu_hist-0.json") + os.remove("test_deterministic_gpu_hist-1.json") return hash(model_0), hash(model_1) @@ -43,7 +43,7 @@ class TestGPUBasicModels: self.cpu_test_bm.run_custom_objective("gpu_hist") def test_eta_decay(self): - self.cpu_test_cb.run_eta_decay('gpu_hist') + self.cpu_test_cb.run_eta_decay("gpu_hist") @pytest.mark.parametrize( "objective", ["binary:logistic", "reg:absoluteerror", "reg:quantileerror"] diff --git a/tests/python-gpu/test_gpu_demos.py b/tests/python-gpu/test_gpu_demos.py index 2c4260fd1..d3b6089a3 100644 --- a/tests/python-gpu/test_gpu_demos.py +++ b/tests/python-gpu/test_gpu_demos.py @@ -12,18 +12,18 @@ import test_demos as td # noqa @pytest.mark.skipif(**tm.no_cupy()) def test_data_iterator(): - script = os.path.join(td.PYTHON_DEMO_DIR, 'quantile_data_iterator.py') - cmd = ['python', script] + script = os.path.join(td.PYTHON_DEMO_DIR, "quantile_data_iterator.py") + cmd = ["python", script] subprocess.check_call(cmd) def test_update_process_demo(): - script = os.path.join(td.PYTHON_DEMO_DIR, 'update_process.py') - cmd = ['python', script] + script = os.path.join(td.PYTHON_DEMO_DIR, "update_process.py") + cmd = ["python", script] subprocess.check_call(cmd) def test_categorical_demo(): - script = os.path.join(td.PYTHON_DEMO_DIR, 'categorical.py') - cmd = ['python', script] + script = os.path.join(td.PYTHON_DEMO_DIR, "categorical.py") + cmd = ["python", script] subprocess.check_call(cmd) diff --git a/tests/python-gpu/test_gpu_linear.py b/tests/python-gpu/test_gpu_linear.py index 04f9be256..174277c7e 100644 --- a/tests/python-gpu/test_gpu_linear.py +++ b/tests/python-gpu/test_gpu_linear.py @@ -6,22 +6,29 @@ from xgboost import testing as tm pytestmark = tm.timeout(10) -parameter_strategy = strategies.fixed_dictionaries({ - 'booster': strategies.just('gblinear'), - 'eta': strategies.floats(0.01, 0.25), - 'tolerance': strategies.floats(1e-5, 1e-2), - 'nthread': strategies.integers(1, 4), - 'feature_selector': strategies.sampled_from(['cyclic', 'shuffle', - 'greedy', 'thrifty']), - 'top_k': strategies.integers(1, 10), -}) +parameter_strategy = strategies.fixed_dictionaries( + { + "booster": strategies.just("gblinear"), + "eta": strategies.floats(0.01, 0.25), + "tolerance": strategies.floats(1e-5, 1e-2), + "nthread": strategies.integers(1, 4), + "feature_selector": strategies.sampled_from( + ["cyclic", "shuffle", "greedy", "thrifty"] + ), + "top_k": strategies.integers(1, 10), + } +) def train_result(param, dmat, num_rounds): result = {} booster = xgb.train( - param, dmat, num_rounds, [(dmat, 'train')], verbose_eval=False, - evals_result=result + param, + dmat, + num_rounds, + [(dmat, "train")], + verbose_eval=False, + evals_result=result, ) assert booster.num_boosted_rounds() == num_rounds return result @@ -32,9 +39,11 @@ class TestGPULinear: @settings(deadline=None, max_examples=20, print_blob=True) def test_gpu_coordinate(self, param, num_rounds, dataset): assume(len(dataset.y) > 0) - param['updater'] = 'gpu_coord_descent' + param["updater"] = "gpu_coord_descent" param = dataset.set_params(param) - result = train_result(param, dataset.get_dmat(), num_rounds)['train'][dataset.metric] + result = train_result(param, dataset.get_dmat(), num_rounds)["train"][ + dataset.metric + ] note(result) assert tm.non_increasing(result) @@ -46,16 +55,18 @@ class TestGPULinear: strategies.integers(10, 50), tm.make_dataset_strategy(), strategies.floats(1e-5, 0.8), - strategies.floats(1e-5, 0.8) + strategies.floats(1e-5, 0.8), ) @settings(deadline=None, max_examples=20, print_blob=True) def test_gpu_coordinate_regularised(self, param, num_rounds, dataset, alpha, lambd): assume(len(dataset.y) > 0) - param['updater'] = 'gpu_coord_descent' - param['alpha'] = alpha - param['lambda'] = lambd + param["updater"] = "gpu_coord_descent" + param["alpha"] = alpha + param["lambda"] = lambd param = dataset.set_params(param) - result = train_result(param, dataset.get_dmat(), num_rounds)['train'][dataset.metric] + result = train_result(param, dataset.get_dmat(), num_rounds)["train"][ + dataset.metric + ] note(result) assert tm.non_increasing([result[0], result[-1]]) @@ -64,8 +75,12 @@ class TestGPULinear: # Training linear model is quite expensive, so we don't include it in # test_from_cupy.py import cupy - params = {'booster': 'gblinear', 'updater': 'gpu_coord_descent', - 'n_estimators': 100} + + params = { + "booster": "gblinear", + "updater": "gpu_coord_descent", + "n_estimators": 100, + } X, y = tm.get_california_housing() cpu_model = xgb.XGBRegressor(**params) cpu_model.fit(X, y) diff --git a/tests/python-gpu/test_gpu_training_continuation.py b/tests/python-gpu/test_gpu_training_continuation.py index 6a908af27..a67d2f26b 100644 --- a/tests/python-gpu/test_gpu_training_continuation.py +++ b/tests/python-gpu/test_gpu_training_continuation.py @@ -14,14 +14,18 @@ class TestGPUTrainingContinuation: X = np.random.randn(kRows, kCols) y = np.random.randn(kRows) dtrain = xgb.DMatrix(X, y) - params = {'tree_method': 'gpu_hist', 'max_depth': '2', - 'gamma': '0.1', 'alpha': '0.01'} + params = { + "tree_method": "gpu_hist", + "max_depth": "2", + "gamma": "0.1", + "alpha": "0.01", + } bst_0 = xgb.train(params, dtrain, num_boost_round=64) - dump_0 = bst_0.get_dump(dump_format='json') + dump_0 = bst_0.get_dump(dump_format="json") bst_1 = xgb.train(params, dtrain, num_boost_round=32) bst_1 = xgb.train(params, dtrain, num_boost_round=32, xgb_model=bst_1) - dump_1 = bst_1.get_dump(dump_format='json') + dump_1 = bst_1.get_dump(dump_format="json") def recursive_compare(obj_0, obj_1): if isinstance(obj_0, float): @@ -37,9 +41,8 @@ class TestGPUTrainingContinuation: values_1 = list(obj_1.values()) for i in range(len(obj_0.items())): assert keys_0[i] == keys_1[i] - if list(obj_0.keys())[i] != 'missing': - recursive_compare(values_0[i], - values_1[i]) + if list(obj_0.keys())[i] != "missing": + recursive_compare(values_0[i], values_1[i]) else: for i in range(len(obj_0)): recursive_compare(obj_0[i], obj_1[i]) diff --git a/tests/python-gpu/test_monotonic_constraints.py b/tests/python-gpu/test_monotonic_constraints.py index 3bf4f0deb..4586b617a 100644 --- a/tests/python-gpu/test_monotonic_constraints.py +++ b/tests/python-gpu/test_monotonic_constraints.py @@ -22,12 +22,13 @@ def non_increasing(L): def assert_constraint(constraint, tree_method): from sklearn.datasets import make_regression + n = 1000 X, y = make_regression(n, random_state=rng, n_features=1, n_informative=1) dtrain = xgb.DMatrix(X, y) param = {} - param['tree_method'] = tree_method - param['monotone_constraints'] = "(" + str(constraint) + ")" + param["tree_method"] = tree_method + param["monotone_constraints"] = "(" + str(constraint) + ")" bst = xgb.train(param, dtrain) dpredict = xgb.DMatrix(X[X[:, 0].argsort()]) pred = bst.predict(dpredict) @@ -40,15 +41,15 @@ def assert_constraint(constraint, tree_method): @pytest.mark.skipif(**tm.no_sklearn()) def test_gpu_hist_basic(): - assert_constraint(1, 'gpu_hist') - assert_constraint(-1, 'gpu_hist') + assert_constraint(1, "gpu_hist") + assert_constraint(-1, "gpu_hist") def test_gpu_hist_depthwise(): params = { - 'tree_method': 'gpu_hist', - 'grow_policy': 'depthwise', - 'monotone_constraints': '(1, -1)' + "tree_method": "gpu_hist", + "grow_policy": "depthwise", + "monotone_constraints": "(1, -1)", } model = xgb.train(params, tmc.training_dset) tmc.is_correctly_constrained(model) @@ -56,9 +57,9 @@ def test_gpu_hist_depthwise(): def test_gpu_hist_lossguide(): params = { - 'tree_method': 'gpu_hist', - 'grow_policy': 'lossguide', - 'monotone_constraints': '(1, -1)' + "tree_method": "gpu_hist", + "grow_policy": "lossguide", + "monotone_constraints": "(1, -1)", } model = xgb.train(params, tmc.training_dset) tmc.is_correctly_constrained(model) diff --git a/tests/python/test_dmatrix.py b/tests/python/test_dmatrix.py index 2e8a1a2a6..c718378c5 100644 --- a/tests/python/test_dmatrix.py +++ b/tests/python/test_dmatrix.py @@ -1,6 +1,5 @@ import csv import os -import sys import tempfile import numpy as np @@ -12,59 +11,12 @@ from scipy.sparse import csr_matrix, rand import xgboost as xgb from xgboost import testing as tm from xgboost.core import DataSplitMode -from xgboost.testing.data import np_dtypes - -rng = np.random.RandomState(1) +from xgboost.testing.data import np_dtypes, run_base_margin_info dpath = "demo/data/" rng = np.random.RandomState(1994) -def set_base_margin_info(DType, DMatrixT, tm: str): - rng = np.random.default_rng() - X = DType(rng.normal(0, 1.0, size=100).astype(np.float32).reshape(50, 2)) - if hasattr(X, "iloc"): - y = X.iloc[:, 0] - else: - y = X[:, 0] - base_margin = X - # no error at set - Xy = DMatrixT(X, y, base_margin=base_margin) - # Error at train, caused by check in predictor. - with pytest.raises(ValueError, match=r".*base_margin.*"): - xgb.train({"tree_method": tm}, Xy) - - if not hasattr(X, "iloc"): - # column major matrix - got = DType(Xy.get_base_margin().reshape(50, 2)) - assert (got == base_margin).all() - - assert base_margin.T.flags.c_contiguous is False - assert base_margin.T.flags.f_contiguous is True - Xy.set_info(base_margin=base_margin.T) - got = DType(Xy.get_base_margin().reshape(2, 50)) - assert (got == base_margin.T).all() - - # Row vs col vec. - base_margin = y - Xy.set_base_margin(base_margin) - bm_col = Xy.get_base_margin() - Xy.set_base_margin(base_margin.reshape(1, base_margin.size)) - bm_row = Xy.get_base_margin() - assert (bm_row == bm_col).all() - - # type - base_margin = base_margin.astype(np.float64) - Xy.set_base_margin(base_margin) - bm_f64 = Xy.get_base_margin() - assert (bm_f64 == bm_col).all() - - # too many dimensions - base_margin = X.reshape(2, 5, 2, 5) - with pytest.raises(ValueError, match=r".*base_margin.*"): - Xy.set_base_margin(base_margin) - - class TestDMatrix: def test_warn_missing(self): from xgboost import data @@ -417,8 +369,8 @@ class TestDMatrix: ) np.testing.assert_equal(np.array(Xy.feature_types), np.array(feature_types)) - def test_base_margin(self): - set_base_margin_info(np.asarray, xgb.DMatrix, "hist") + def test_base_margin(self) -> None: + run_base_margin_info(np.asarray, xgb.DMatrix, "cpu") @given( strategies.integers(0, 1000), diff --git a/tests/python/test_with_modin.py b/tests/python/test_with_modin.py index 75dae0654..ce0dbd609 100644 --- a/tests/python/test_with_modin.py +++ b/tests/python/test_with_modin.py @@ -1,9 +1,9 @@ import numpy as np import pytest -from test_dmatrix import set_base_margin_info import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.data import run_base_margin_info try: import modin.pandas as md @@ -145,4 +145,4 @@ class TestModin: np.testing.assert_array_equal(data.get_weight(), w) def test_base_margin(self): - set_base_margin_info(md.DataFrame, xgb.DMatrix, "hist") + run_base_margin_info(md.DataFrame, xgb.DMatrix, "cpu") diff --git a/tests/python/test_with_pandas.py b/tests/python/test_with_pandas.py index c7b99a991..4dd0c640d 100644 --- a/tests/python/test_with_pandas.py +++ b/tests/python/test_with_pandas.py @@ -1,14 +1,12 @@ -import sys from typing import Type import numpy as np import pytest -from test_dmatrix import set_base_margin_info import xgboost as xgb from xgboost import testing as tm from xgboost.core import DataSplitMode -from xgboost.testing.data import pd_arrow_dtypes, pd_dtypes +from xgboost.testing.data import pd_arrow_dtypes, pd_dtypes, run_base_margin_info try: import pandas as pd @@ -336,7 +334,7 @@ class TestPandas: np.testing.assert_array_equal(data.get_weight(), w) def test_base_margin(self): - set_base_margin_info(pd.DataFrame, xgb.DMatrix, "hist") + run_base_margin_info(pd.DataFrame, xgb.DMatrix, "cpu") def test_cv_as_pandas(self): dm, _ = tm.load_agaricus(__file__) From 26a5436a652790524b508268efa79fc2a7127e02 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 14:52:19 +0800 Subject: [PATCH 075/109] [doc] Describe feature info behavior. [skip ci] (#9866) --- doc/contrib/consistency.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/contrib/consistency.rst b/doc/contrib/consistency.rst index a268820eb..b74234602 100644 --- a/doc/contrib/consistency.rst +++ b/doc/contrib/consistency.rst @@ -59,4 +59,13 @@ XGBoost has a default logger builtin that can be a wrapper over binding-specific Minimum Amount of Data Manipulation *********************************** -XGBoost is mostly a machine learning library providing boosting algorithm implementation. Some other implementations might perform some sort of data manipulation implicitly like deciding the coding of the data, and transforming the data according to some heuristic before training. We prefer to keep these operations based on necessities instead of convenience to keep the scope of the project well-defined. Whenever possible, we should leave these features to 3-party libraries and consider how a user can compose their pipeline. For instance, XGBoost itself should not perform ordinal encoding for categorical data, users will pick an encoder that fits their use cases (like out-of-core implementation, distributed implementation, known mapping, etc). If some transformations are decided to be part of the algorithm, we can have it inside the core instead of the language binding. Examples would be target-encoding or sketching the response variables. If we were to support them, we could have it inside the core implementation as part of the ML algorithm. This aligns with the same principles of default parameters, various bindings should provide similar (if not the same) results given the same set of parameters and data. \ No newline at end of file +XGBoost is mostly a machine learning library providing boosting algorithm implementation. Some other implementations might perform some sort of data manipulation implicitly like deciding the coding of the data, and transforming the data according to some heuristic before training. We prefer to keep these operations based on necessities instead of convenience to keep the scope of the project well-defined. Whenever possible, we should leave these features to 3-party libraries and consider how a user can compose their pipeline. For instance, XGBoost itself should not perform ordinal encoding for categorical data, users will pick an encoder that fits their use cases (like out-of-core implementation, distributed implementation, known mapping, etc). If some transformations are decided to be part of the algorithm, we can have it inside the core instead of the language binding. Examples would be target-encoding or sketching the response variables. If we were to support them, we could have it inside the core implementation as part of the ML algorithm. This aligns with the same principles of default parameters, various bindings should provide similar (if not the same) results given the same set of parameters and data. + +************ +Feature Info +************ + +XGBoost accepts data structures that contain meta info about predictors, including the names and types of features. Example inputs are :py:class:`pandas.DataFrame`, R `data.frame`. We have the following heuristics: +- When the input data structure contains such information, we set the `feature_names` and `feature_types` for `DMatrix` accordingly. +- When a user provides this information as explicit parameters, the user-provided version should override the one provided by the data structure. +- When both sources are missing, the `DMatrix` class contain empty info. \ No newline at end of file From 5f7b5a69213bbf82238832a277f1a11046b0d57f Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 14:52:48 +0800 Subject: [PATCH 076/109] Add tests for pickling with custom obj and metric. (#9943) --- doc/tutorials/custom_metric_obj.rst | 1 - doc/tutorials/saving_model.rst | 2 ++ python-package/xgboost/sklearn.py | 27 ++++++++++++-------- python-package/xgboost/testing/__init__.py | 7 ++++++ tests/python/test_pickling.py | 27 ++++++++++++++++++++ tests/python/test_with_sklearn.py | 29 ++++++++++------------ 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/doc/tutorials/custom_metric_obj.rst b/doc/tutorials/custom_metric_obj.rst index 76ee1b3de..118a099c1 100644 --- a/doc/tutorials/custom_metric_obj.rst +++ b/doc/tutorials/custom_metric_obj.rst @@ -279,7 +279,6 @@ available at :ref:`sphx_glr_python_examples_custom_softmax.py`. Also, see Scikit-Learn Interface ********************** - The scikit-learn interface of XGBoost has some utilities to improve the integration with standard scikit-learn functions. For instance, after XGBoost 1.6.0 users can use the cost function (not scoring functions) from scikit-learn out of the box: diff --git a/doc/tutorials/saving_model.rst b/doc/tutorials/saving_model.rst index 54c217249..34b5430df 100644 --- a/doc/tutorials/saving_model.rst +++ b/doc/tutorials/saving_model.rst @@ -101,6 +101,8 @@ snapshot generated by an earlier version of XGBoost may result in errors or unde **If a model is persisted with** ``pickle.dump`` (Python) or ``saveRDS`` (R), **then the model may not be accessible in later versions of XGBoost.** +.. _custom-obj-metric: + *************************** Custom objective and metric *************************** diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index ea8d8d041..3383ae0b7 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -192,11 +192,16 @@ __model_doc = f""" Boosting learning rate (xgb's "eta") verbosity : Optional[int] The degree of verbosity. Valid values are 0 (silent) - 3 (debug). + objective : {SklObjective} - Specify the learning task and the corresponding learning objective or - a custom objective function to be used (see note below). + + Specify the learning task and the corresponding learning objective or a custom + objective function to be used. For custom objective, see + :doc:`/tutorials/custom_metric_obj` and :ref:`custom-obj-metric` for more + information. + booster: Optional[str] - Specify which booster to use: gbtree, gblinear or dart. + Specify which booster to use: `gbtree`, `gblinear` or `dart`. tree_method: Optional[str] Specify which tree method to use. Default to auto. If this parameter is set to default, XGBoost will choose the most conservative option available. It's @@ -328,21 +333,21 @@ __model_doc = f""" Metric used for monitoring the training result and early stopping. It can be a string or list of strings as names of predefined metric in XGBoost (See - doc/parameter.rst), one of the metrics in :py:mod:`sklearn.metrics`, or any other - user defined metric that looks like `sklearn.metrics`. + doc/parameter.rst), one of the metrics in :py:mod:`sklearn.metrics`, or any + other user defined metric that looks like `sklearn.metrics`. If custom objective is also provided, then custom metric should implement the corresponding reverse link function. Unlike the `scoring` parameter commonly used in scikit-learn, when a callable - object is provided, it's assumed to be a cost function and by default XGBoost will - minimize the result during early stopping. + object is provided, it's assumed to be a cost function and by default XGBoost + will minimize the result during early stopping. - For advanced usage on Early stopping like directly choosing to maximize instead of - minimize, see :py:obj:`xgboost.callback.EarlyStopping`. + For advanced usage on Early stopping like directly choosing to maximize instead + of minimize, see :py:obj:`xgboost.callback.EarlyStopping`. - See :doc:`Custom Objective and Evaluation Metric ` - for more. + See :doc:`/tutorials/custom_metric_obj` and :ref:`custom-obj-metric` for more + information. .. note:: diff --git a/python-package/xgboost/testing/__init__.py b/python-package/xgboost/testing/__init__.py index 6b8daf561..373ad1c58 100644 --- a/python-package/xgboost/testing/__init__.py +++ b/python-package/xgboost/testing/__init__.py @@ -815,6 +815,13 @@ def softprob_obj( return objective +def ls_obj(y_true: np.ndarray, y_pred: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Least squared error.""" + grad = y_pred - y_true + hess = np.ones(len(y_true)) + return grad, hess + + class DirectoryExcursion: """Change directory. Change back and optionally cleaning up the directory when exit. diff --git a/tests/python/test_pickling.py b/tests/python/test_pickling.py index 2f4d77bf0..083a2a7fd 100644 --- a/tests/python/test_pickling.py +++ b/tests/python/test_pickling.py @@ -1,10 +1,13 @@ import json import os import pickle +import tempfile import numpy as np +import pytest import xgboost as xgb +from xgboost import testing as tm kRows = 100 kCols = 10 @@ -61,3 +64,27 @@ class TestPickling: params = {"nthread": 8, "tree_method": "exact", "subsample": 0.5} config = self.run_model_pickling(params) check(config) + + @pytest.mark.skipif(**tm.no_sklearn()) + def test_with_sklearn_obj_metric(self) -> None: + from sklearn.metrics import mean_squared_error + + X, y = tm.datasets.make_regression() + reg = xgb.XGBRegressor(objective=tm.ls_obj, eval_metric=mean_squared_error) + reg.fit(X, y) + + pkl = pickle.dumps(reg) + reg_1 = pickle.loads(pkl) + assert callable(reg_1.objective) + assert callable(reg_1.eval_metric) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "model.json") + reg.save_model(path) + + reg_2 = xgb.XGBRegressor() + reg_2.load_model(path) + + assert not callable(reg_2.objective) + assert not callable(reg_2.eval_metric) + assert reg_2.eval_metric is None diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 1e49ed053..ee0085d51 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -504,15 +504,10 @@ def test_regression_with_custom_objective(): from sklearn.metrics import mean_squared_error from sklearn.model_selection import KFold - def objective_ls(y_true, y_pred): - grad = (y_pred - y_true) - hess = np.ones(len(y_true)) - return grad, hess - X, y = fetch_california_housing(return_X_y=True) kf = KFold(n_splits=2, shuffle=True, random_state=rng) for train_index, test_index in kf.split(X, y): - xgb_model = xgb.XGBRegressor(objective=objective_ls).fit( + xgb_model = xgb.XGBRegressor(objective=tm.ls_obj).fit( X[train_index], y[train_index] ) preds = xgb_model.predict(X[test_index]) @@ -530,27 +525,29 @@ def test_regression_with_custom_objective(): np.testing.assert_raises(XGBCustomObjectiveException, xgb_model.fit, X, y) +def logregobj(y_true, y_pred): + y_pred = 1.0 / (1.0 + np.exp(-y_pred)) + grad = y_pred - y_true + hess = y_pred * (1.0 - y_pred) + return grad, hess + + def test_classification_with_custom_objective(): from sklearn.datasets import load_digits from sklearn.model_selection import KFold - def logregobj(y_true, y_pred): - y_pred = 1.0 / (1.0 + np.exp(-y_pred)) - grad = y_pred - y_true - hess = y_pred * (1.0 - y_pred) - return grad, hess - digits = load_digits(n_class=2) - y = digits['target'] - X = digits['data'] + y = digits["target"] + X = digits["data"] kf = KFold(n_splits=2, shuffle=True, random_state=rng) for train_index, test_index in kf.split(X, y): xgb_model = xgb.XGBClassifier(objective=logregobj) xgb_model.fit(X[train_index], y[train_index]) preds = xgb_model.predict(X[test_index]) labels = y[test_index] - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) assert err < 0.1 # Test that the custom objective function is actually used From 621348abb367cb012879786a9c6c60949dc40bd9 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 16:41:13 +0800 Subject: [PATCH 077/109] Fix multi-output with alternating strategies. (#9933) --------- Co-authored-by: Philip Hyunsu Cho --- python-package/xgboost/testing/updater.py | 11 +++ src/tree/updater_quantile_hist.cc | 8 +- tests/ci_build/lint_python.py | 2 + tests/python/test_basic_models.py | 6 +- tests/python/test_multi_target.py | 105 ++++++++++++++++++++++ tests/python/test_updaters.py | 64 ------------- 6 files changed, 123 insertions(+), 73 deletions(-) create mode 100644 tests/python/test_multi_target.py diff --git a/python-package/xgboost/testing/updater.py b/python-package/xgboost/testing/updater.py index af5acf428..00c982bd0 100644 --- a/python-package/xgboost/testing/updater.py +++ b/python-package/xgboost/testing/updater.py @@ -394,3 +394,14 @@ def train_result( assert booster.feature_types == dmat.feature_types return result + + +class ResetStrategy(xgb.callback.TrainingCallback): + """Callback for testing multi-output.""" + + def after_iteration(self, model: xgb.Booster, epoch: int, evals_log: dict) -> bool: + if epoch % 2 == 0: + model.set_param({"multi_strategy": "multi_output_tree"}) + else: + model.set_param({"multi_strategy": "one_output_per_tree"}) + return False diff --git a/src/tree/updater_quantile_hist.cc b/src/tree/updater_quantile_hist.cc index 2bb5b0b49..7731f505e 100644 --- a/src/tree/updater_quantile_hist.cc +++ b/src/tree/updater_quantile_hist.cc @@ -545,12 +545,12 @@ class QuantileHistMaker : public TreeUpdater { } bool UpdatePredictionCache(const DMatrix *data, linalg::MatrixView out_preds) override { - if (p_impl_) { - return p_impl_->UpdatePredictionCache(data, out_preds); - } else if (p_mtimpl_) { + if (out_preds.Shape(1) > 1) { + CHECK(p_mtimpl_); return p_mtimpl_->UpdatePredictionCache(data, out_preds); } else { - return false; + CHECK(p_impl_); + return p_impl_->UpdatePredictionCache(data, out_preds); } } diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index c9170b100..ed33a96e5 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -22,6 +22,7 @@ class LintersPaths: "tests/python/test_dmatrix.py", "tests/python/test_dt.py", "tests/python/test_demos.py", + "tests/python/test_multi_target.py", "tests/python/test_predict.py", "tests/python/test_quantile_dmatrix.py", "tests/python/test_tree_regularization.py", @@ -79,6 +80,7 @@ class LintersPaths: "tests/python/test_dt.py", "tests/python/test_demos.py", "tests/python/test_data_iterator.py", + "tests/python/test_multi_target.py", "tests/python-gpu/test_gpu_data_iterator.py", "tests/python-gpu/load_pickle.py", "tests/test_distributed/test_with_spark/test_data.py", diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index 45bef1f25..8f83e1fcc 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -8,6 +8,7 @@ import pytest import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.updater import ResetStrategy dpath = tm.data_dir(__file__) @@ -653,11 +654,6 @@ class TestModels: num_parallel_tree = 4 num_boost_round = 16 - class ResetStrategy(xgb.callback.TrainingCallback): - def after_iteration(self, model, epoch: int, evals_log) -> bool: - model.set_param({"multi_strategy": "multi_output_tree"}) - return False - booster = xgb.train( { "num_parallel_tree": num_parallel_tree, diff --git a/tests/python/test_multi_target.py b/tests/python/test_multi_target.py new file mode 100644 index 000000000..446d44062 --- /dev/null +++ b/tests/python/test_multi_target.py @@ -0,0 +1,105 @@ +from typing import Any, Dict + +from hypothesis import given, note, settings, strategies + +import xgboost as xgb +from xgboost import testing as tm +from xgboost.testing.params import ( + exact_parameter_strategy, + hist_cache_strategy, + hist_multi_parameter_strategy, + hist_parameter_strategy, +) +from xgboost.testing.updater import ResetStrategy, train_result + + +class TestTreeMethodMulti: + @given( + exact_parameter_strategy, strategies.integers(1, 20), tm.multi_dataset_strategy + ) + @settings(deadline=None, print_blob=True) + def test_exact(self, param: dict, num_rounds: int, dataset: tm.TestDataset) -> None: + if dataset.name.endswith("-l1"): + return + param["tree_method"] = "exact" + param = dataset.set_params(param) + result = train_result(param, dataset.get_dmat(), num_rounds) + assert tm.non_increasing(result["train"][dataset.metric]) + + @given( + exact_parameter_strategy, + hist_parameter_strategy, + hist_cache_strategy, + strategies.integers(1, 20), + tm.multi_dataset_strategy, + ) + @settings(deadline=None, print_blob=True) + def test_approx( + self, + param: Dict[str, Any], + hist_param: Dict[str, Any], + cache_param: Dict[str, Any], + num_rounds: int, + dataset: tm.TestDataset, + ) -> None: + param["tree_method"] = "approx" + param = dataset.set_params(param) + param.update(hist_param) + param.update(cache_param) + result = train_result(param, dataset.get_dmat(), num_rounds) + note(str(result)) + assert tm.non_increasing(result["train"][dataset.metric]) + + @given( + exact_parameter_strategy, + hist_multi_parameter_strategy, + hist_cache_strategy, + strategies.integers(1, 20), + tm.multi_dataset_strategy, + ) + @settings(deadline=None, print_blob=True) + def test_hist( + self, + param: Dict[str, Any], + hist_param: Dict[str, Any], + cache_param: Dict[str, Any], + num_rounds: int, + dataset: tm.TestDataset, + ) -> None: + if dataset.name.endswith("-l1"): + return + param["tree_method"] = "hist" + param = dataset.set_params(param) + param.update(hist_param) + param.update(cache_param) + result = train_result(param, dataset.get_dmat(), num_rounds) + note(str(result)) + assert tm.non_increasing(result["train"][dataset.metric]) + + +def test_multiclass() -> None: + X, y = tm.datasets.make_classification( + 128, n_features=12, n_informative=10, n_classes=4 + ) + clf = xgb.XGBClassifier( + multi_strategy="multi_output_tree", callbacks=[ResetStrategy()], n_estimators=10 + ) + clf.fit(X, y, eval_set=[(X, y)]) + assert clf.objective == "multi:softprob" + assert tm.non_increasing(clf.evals_result()["validation_0"]["mlogloss"]) + + proba = clf.predict_proba(X) + assert proba.shape == (y.shape[0], 4) + + +def test_multilabel() -> None: + X, y = tm.datasets.make_multilabel_classification(128) + clf = xgb.XGBClassifier( + multi_strategy="multi_output_tree", callbacks=[ResetStrategy()], n_estimators=10 + ) + clf.fit(X, y, eval_set=[(X, y)]) + assert clf.objective == "binary:logistic" + assert tm.non_increasing(clf.evals_result()["validation_0"]["logloss"]) + + proba = clf.predict_proba(X) + assert proba.shape == y.shape diff --git a/tests/python/test_updaters.py b/tests/python/test_updaters.py index c4c0de032..9a0cdecd2 100644 --- a/tests/python/test_updaters.py +++ b/tests/python/test_updaters.py @@ -12,7 +12,6 @@ from xgboost.testing.params import ( cat_parameter_strategy, exact_parameter_strategy, hist_cache_strategy, - hist_multi_parameter_strategy, hist_parameter_strategy, ) from xgboost.testing.updater import ( @@ -25,69 +24,6 @@ from xgboost.testing.updater import ( ) -class TestTreeMethodMulti: - @given( - exact_parameter_strategy, strategies.integers(1, 20), tm.multi_dataset_strategy - ) - @settings(deadline=None, print_blob=True) - def test_exact(self, param: dict, num_rounds: int, dataset: tm.TestDataset) -> None: - if dataset.name.endswith("-l1"): - return - param["tree_method"] = "exact" - param = dataset.set_params(param) - result = train_result(param, dataset.get_dmat(), num_rounds) - assert tm.non_increasing(result["train"][dataset.metric]) - - @given( - exact_parameter_strategy, - hist_parameter_strategy, - hist_cache_strategy, - strategies.integers(1, 20), - tm.multi_dataset_strategy, - ) - @settings(deadline=None, print_blob=True) - def test_approx( - self, param: Dict[str, Any], - hist_param: Dict[str, Any], - cache_param: Dict[str, Any], - num_rounds: int, - dataset: tm.TestDataset, - ) -> None: - param["tree_method"] = "approx" - param = dataset.set_params(param) - param.update(hist_param) - param.update(cache_param) - result = train_result(param, dataset.get_dmat(), num_rounds) - note(str(result)) - assert tm.non_increasing(result["train"][dataset.metric]) - - @given( - exact_parameter_strategy, - hist_multi_parameter_strategy, - hist_cache_strategy, - strategies.integers(1, 20), - tm.multi_dataset_strategy, - ) - @settings(deadline=None, print_blob=True) - def test_hist( - self, - param: Dict[str, Any], - hist_param: Dict[str, Any], - cache_param: Dict[str, Any], - num_rounds: int, - dataset: tm.TestDataset, - ) -> None: - if dataset.name.endswith("-l1"): - return - param["tree_method"] = "hist" - param = dataset.set_params(param) - param.update(hist_param) - param.update(cache_param) - result = train_result(param, dataset.get_dmat(), num_rounds) - note(str(result)) - assert tm.non_increasing(result["train"][dataset.metric]) - - class TestTreeMethod: USE_ONEHOT = np.iinfo(np.int32).max USE_PART = 1 From db396ee34046f29443cdc801903990341ea89fa8 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Thu, 4 Jan 2024 09:51:22 +0100 Subject: [PATCH 078/109] [R] make sure output fits into int32 (#9949) --- R-package/src/xgboost_R.cc | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 60a3fe68b..d7d4c49e1 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -167,21 +167,38 @@ SEXP SafeAllocInteger(size_t size, SEXP continuation_token) { [[nodiscard]] SEXP CopyArrayToR(const char *array_str, SEXP ctoken) { xgboost::ArrayInterface<1> array{xgboost::StringView{array_str}}; // R supports only int and double. - bool is_int = + bool is_int_type = xgboost::DispatchDType(array.type, [](auto t) { return std::is_integral_v; }); bool is_float = xgboost::DispatchDType( array.type, [](auto v) { return std::is_floating_point_v; }); - CHECK(is_int || is_float) << "Internal error: Invalid DType."; + CHECK(is_int_type || is_float) << "Internal error: Invalid DType."; CHECK(array.is_contiguous) << "Internal error: Return by XGBoost should be contiguous"; + // Note: the only case in which this will receive an integer type is + // for the 'indptr' part of the quantile cut outputs, which comes + // in sorted order, so the last element contains the maximum value. + bool fits_into_C_int = xgboost::DispatchDType(array.type, [&](auto t) { + using T = decltype(t); + if (!std::is_integral_v) { + return false; + } + auto ptr = static_cast(array.data); + T last_elt = ptr[array.n - 1]; + if (last_elt < 0) { + last_elt = -last_elt; // no std::abs overload for all possible types + } + return last_elt <= std::numeric_limits::max(); + }); + bool use_int = is_int_type && fits_into_C_int; + // Allocate memory in R SEXP out = - Rf_protect(is_int ? SafeAllocInteger(array.n, ctoken) : SafeAllocReal(array.n, ctoken)); + Rf_protect(use_int ? SafeAllocInteger(array.n, ctoken) : SafeAllocReal(array.n, ctoken)); xgboost::DispatchDType(array.type, [&](auto t) { using T = decltype(t); auto in_ptr = static_cast(array.data); - if (is_int) { + if (use_int) { auto out_ptr = INTEGER(out); std::copy_n(in_ptr, array.n, out_ptr); } else { From c03a4d50886ed2891bb289f5bcbf41d5ce67b61d Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Thu, 4 Jan 2024 16:51:33 +0800 Subject: [PATCH 079/109] Check support status for categorical features. (#9946) --- include/xgboost/data.h | 17 +++++++---- src/common/error_msg.h | 6 +++- src/data/data.cc | 45 +++++++++++++++++++++++------- src/data/iterative_dmatrix.h | 2 +- src/gbm/gblinear.cc | 26 ++++++++--------- src/tree/updater_colmaker.cc | 22 +++++++++------ tests/python/test_data_iterator.py | 38 +++++++++++++++++++++++++ 7 files changed, 116 insertions(+), 40 deletions(-) diff --git a/include/xgboost/data.h b/include/xgboost/data.h index 69176994b..08d3d119a 100644 --- a/include/xgboost/data.h +++ b/include/xgboost/data.h @@ -1,5 +1,5 @@ /** - * Copyright 2015-2023 by XGBoost Contributors + * Copyright 2015-2024, XGBoost Contributors * \file data.h * \brief The input data structure of xgboost. * \author Tianqi Chen @@ -158,15 +158,15 @@ class MetaInfo { void SetFeatureInfo(const char *key, const char **info, const bst_ulong size); void GetFeatureInfo(const char *field, std::vector* out_str_vecs) const; - /* - * \brief Extend with other MetaInfo. + /** + * @brief Extend with other MetaInfo. * - * \param that The other MetaInfo object. + * @param that The other MetaInfo object. * - * \param accumulate_rows Whether rows need to be accumulated in this function. If + * @param accumulate_rows Whether rows need to be accumulated in this function. If * client code knows number of rows in advance, set this * parameter to false. - * \param check_column Whether the extend method should check the consistency of + * @param check_column Whether the extend method should check the consistency of * columns. */ void Extend(MetaInfo const& that, bool accumulate_rows, bool check_column); @@ -203,6 +203,10 @@ class MetaInfo { * learning where labels are only available on worker 0. */ bool ShouldHaveLabels() const; + /** + * @brief Flag for whether the DMatrix has categorical features. + */ + bool HasCategorical() const { return has_categorical_; } private: void SetInfoFromHost(Context const& ctx, StringView key, Json arr); @@ -210,6 +214,7 @@ class MetaInfo { /*! \brief argsort of labels */ mutable std::vector label_order_cache_; + bool has_categorical_{false}; }; /*! \brief Element from a sparse vector */ diff --git a/src/common/error_msg.h b/src/common/error_msg.h index 995fe11d5..7264c3532 100644 --- a/src/common/error_msg.h +++ b/src/common/error_msg.h @@ -1,5 +1,5 @@ /** - * Copyright 2023 by XGBoost contributors + * Copyright 2023-2024, XGBoost contributors * * \brief Common error message for various checks. */ @@ -99,5 +99,9 @@ constexpr StringView InvalidCUDAOrdinal() { void MismatchedDevices(Context const* booster, Context const* data); inline auto NoFederated() { return "XGBoost is not compiled with federated learning support."; } + +inline auto NoCategorical(std::string name) { + return name + " doesn't support categorical features."; +} } // namespace xgboost::error #endif // XGBOOST_COMMON_ERROR_MSG_H_ diff --git a/src/data/data.cc b/src/data/data.cc index 96560a11f..24b41640c 100644 --- a/src/data/data.cc +++ b/src/data/data.cc @@ -1,5 +1,5 @@ /** - * Copyright 2015-2023 by XGBoost Contributors + * Copyright 2015-2024, XGBoost Contributors * \file data.cc */ #include "xgboost/data.h" @@ -260,9 +260,14 @@ void MetaInfo::SaveBinary(dmlc::Stream *fo) const { CHECK_EQ(field_cnt, kNumField) << "Wrong number of fields"; } -void LoadFeatureType(std::vectorconst& type_names, std::vector* types) { +/** + * @brief Load feature type info from names, returns whether there's categorical features. + */ +[[nodiscard]] bool LoadFeatureType(std::vector const& type_names, + std::vector* types) { types->clear(); - for (auto const &elem : type_names) { + bool has_cat{false}; + for (auto const& elem : type_names) { if (elem == "int") { types->emplace_back(FeatureType::kNumerical); } else if (elem == "float") { @@ -273,10 +278,12 @@ void LoadFeatureType(std::vectorconst& type_names, std::vectoremplace_back(FeatureType::kNumerical); } else if (elem == "c") { types->emplace_back(FeatureType::kCategorical); + has_cat = true; } else { LOG(FATAL) << "All feature_types must be one of {int, float, i, q, c}."; } } + return has_cat; } const std::vector& MetaInfo::LabelAbsSort(Context const* ctx) const { @@ -340,7 +347,8 @@ void MetaInfo::LoadBinary(dmlc::Stream *fi) { LoadVectorField(fi, u8"feature_names", DataType::kStr, &feature_names); LoadVectorField(fi, u8"feature_types", DataType::kStr, &feature_type_names); LoadVectorField(fi, u8"feature_weights", DataType::kFloat32, &feature_weights); - LoadFeatureType(feature_type_names, &feature_types.HostVector()); + + this->has_categorical_ = LoadFeatureType(feature_type_names, &feature_types.HostVector()); } template @@ -639,6 +647,7 @@ void MetaInfo::SetFeatureInfo(const char* key, const char **info, const bst_ulon CHECK_EQ(size, this->num_col_) << "Length of " << key << " must be equal to number of columns."; CHECK(info); } + if (!std::strcmp(key, "feature_type")) { feature_type_names.clear(); for (size_t i = 0; i < size; ++i) { @@ -651,7 +660,7 @@ void MetaInfo::SetFeatureInfo(const char* key, const char **info, const bst_ulon << "Length of " << key << " must be equal to number of columns."; } auto& h_feature_types = feature_types.HostVector(); - LoadFeatureType(feature_type_names, &h_feature_types); + this->has_categorical_ = LoadFeatureType(feature_type_names, &h_feature_types); } else if (!std::strcmp(key, "feature_name")) { if (IsColumnSplit()) { std::vector local_feature_names{}; @@ -674,9 +683,8 @@ void MetaInfo::SetFeatureInfo(const char* key, const char **info, const bst_ulon } } -void MetaInfo::GetFeatureInfo(const char *field, - std::vector *out_str_vecs) const { - auto &str_vecs = *out_str_vecs; +void MetaInfo::GetFeatureInfo(const char* field, std::vector* out_str_vecs) const { + auto& str_vecs = *out_str_vecs; if (!std::strcmp(field, "feature_type")) { str_vecs.resize(feature_type_names.size()); std::copy(feature_type_names.cbegin(), feature_type_names.cend(), str_vecs.begin()); @@ -689,6 +697,9 @@ void MetaInfo::GetFeatureInfo(const char *field, } void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_column) { + /** + * shape + */ if (accumulate_rows) { this->num_row_ += that.num_row_; } @@ -702,6 +713,9 @@ void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_col } this->num_col_ = that.num_col_; + /** + * info with n_samples + */ linalg::Stack(&this->labels, that.labels); this->weights_.SetDevice(that.weights_.Device()); @@ -715,6 +729,9 @@ void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_col linalg::Stack(&this->base_margin_, that.base_margin_); + /** + * group + */ if (this->group_ptr_.size() == 0) { this->group_ptr_ = that.group_ptr_; } else { @@ -727,17 +744,25 @@ void MetaInfo::Extend(MetaInfo const& that, bool accumulate_rows, bool check_col group_ptr.end()); } + /** + * info with n_features + */ if (!that.feature_names.empty()) { this->feature_names = that.feature_names; } + if (!that.feature_type_names.empty()) { this->feature_type_names = that.feature_type_names; - auto &h_feature_types = feature_types.HostVector(); - LoadFeatureType(this->feature_type_names, &h_feature_types); + auto& h_feature_types = feature_types.HostVector(); + this->has_categorical_ = LoadFeatureType(this->feature_type_names, &h_feature_types); } else if (!that.feature_types.Empty()) { + // FIXME(jiamingy): https://github.com/dmlc/xgboost/pull/9171/files#r1440188612 this->feature_types.Resize(that.feature_types.Size()); this->feature_types.Copy(that.feature_types); + auto const& ft = this->feature_types.ConstHostVector(); + this->has_categorical_ = std::any_of(ft.cbegin(), ft.cend(), common::IsCatOp{}); } + if (!that.feature_weights.Empty()) { this->feature_weights.Resize(that.feature_weights.Size()); this->feature_weights.SetDevice(that.feature_weights.Device()); diff --git a/src/data/iterative_dmatrix.h b/src/data/iterative_dmatrix.h index bcaa5b63c..760a181d1 100644 --- a/src/data/iterative_dmatrix.h +++ b/src/data/iterative_dmatrix.h @@ -93,7 +93,7 @@ class IterativeDMatrix : public DMatrix { return nullptr; } BatchSet GetRowBatches() override { - LOG(FATAL) << "Not implemented."; + LOG(FATAL) << "Not implemented for `QuantileDMatrix`."; return BatchSet(BatchIterator(nullptr)); } BatchSet GetColumnBatches(Context const *) override { diff --git a/src/gbm/gblinear.cc b/src/gbm/gblinear.cc index 4b05d55f3..71905debc 100644 --- a/src/gbm/gblinear.cc +++ b/src/gbm/gblinear.cc @@ -1,5 +1,5 @@ /** - * Copyright 2014-2023, XGBoost Contributors + * Copyright 2014-2024, XGBoost Contributors * \file gblinear.cc * \brief Implementation of Linear booster, with L1/L2 regularization: Elastic Net * the update rule is parallel coordinate descent (shotgun) @@ -8,25 +8,24 @@ #include #include -#include -#include -#include #include #include +#include +#include +#include +#include "../common/common.h" +#include "../common/error_msg.h" // NoCategorical, DeprecatedFunc +#include "../common/threading_utils.h" +#include "../common/timer.h" +#include "gblinear_model.h" #include "xgboost/gbm.h" #include "xgboost/json.h" -#include "xgboost/predictor.h" -#include "xgboost/linear_updater.h" -#include "xgboost/logging.h" #include "xgboost/learner.h" #include "xgboost/linalg.h" - -#include "gblinear_model.h" -#include "../common/timer.h" -#include "../common/common.h" -#include "../common/threading_utils.h" -#include "../common/error_msg.h" +#include "xgboost/linear_updater.h" +#include "xgboost/logging.h" +#include "xgboost/predictor.h" namespace xgboost::gbm { DMLC_REGISTRY_FILE_TAG(gblinear); @@ -145,6 +144,7 @@ class GBLinear : public GradientBooster { ObjFunction const*) override { monitor_.Start("DoBoost"); + CHECK(!p_fmat->Info().HasCategorical()) << error::NoCategorical("`gblinear`"); model_.LazyInitModel(); this->LazySumWeights(p_fmat); diff --git a/src/tree/updater_colmaker.cc b/src/tree/updater_colmaker.cc index e366811f7..ef166fae5 100644 --- a/src/tree/updater_colmaker.cc +++ b/src/tree/updater_colmaker.cc @@ -1,21 +1,22 @@ /** - * Copyright 2014-2023 by XGBoost Contributors + * Copyright 2014-2024, XGBoost Contributors * \file updater_colmaker.cc * \brief use columnwise update to construct a tree * \author Tianqi Chen */ -#include -#include #include +#include +#include +#include "../common/error_msg.h" // for NoCategorical +#include "../common/random.h" +#include "constraints.h" +#include "param.h" +#include "split_evaluator.h" +#include "xgboost/json.h" +#include "xgboost/logging.h" #include "xgboost/parameter.h" #include "xgboost/tree_updater.h" -#include "xgboost/logging.h" -#include "xgboost/json.h" -#include "param.h" -#include "constraints.h" -#include "../common/random.h" -#include "split_evaluator.h" namespace xgboost::tree { @@ -102,6 +103,9 @@ class ColMaker: public TreeUpdater { LOG(FATAL) << "Updater `grow_colmaker` or `exact` tree method doesn't " "support external memory training."; } + if (dmat->Info().HasCategorical()) { + LOG(FATAL) << error::NoCategorical("Updater `grow_colmaker` or `exact` tree method"); + } this->LazyGetColumnDensity(dmat); // rescale learning rate according to size of trees interaction_constraints_.Configure(*param, dmat->Info().num_row_); diff --git a/tests/python/test_data_iterator.py b/tests/python/test_data_iterator.py index e6bdfd2e7..174f5606c 100644 --- a/tests/python/test_data_iterator.py +++ b/tests/python/test_data_iterator.py @@ -1,3 +1,5 @@ +import os +import tempfile import weakref from typing import Any, Callable, Dict, List @@ -195,3 +197,39 @@ def test_data_cache() -> None: assert called == 1 xgb.data._proxy_transform = transform + + +def test_cat_check() -> None: + n_batches = 3 + n_features = 2 + n_samples_per_batch = 16 + + batches = [] + + for i in range(n_batches): + X, y = tm.make_categorical( + n_samples=n_samples_per_batch, + n_features=n_features, + n_categories=3, + onehot=False, + ) + batches.append((X, y)) + + X, y = list(zip(*batches)) + it = tm.IteratorForTest(X, y, None, cache=None) + Xy: xgb.DMatrix = xgb.QuantileDMatrix(it, enable_categorical=True) + + with pytest.raises(ValueError, match="categorical features"): + xgb.train({"tree_method": "exact"}, Xy) + + Xy = xgb.DMatrix(X[0], y[0], enable_categorical=True) + with pytest.raises(ValueError, match="categorical features"): + xgb.train({"tree_method": "exact"}, Xy) + + with tempfile.TemporaryDirectory() as tmpdir: + cache_path = os.path.join(tmpdir, "cache") + + it = tm.IteratorForTest(X, y, None, cache=cache_path) + Xy = xgb.DMatrix(it, enable_categorical=True) + with pytest.raises(ValueError, match="categorical features"): + xgb.train({"booster": "gblinear"}, Xy) From 38dd91f49137305e5822f459ac338a1497f2e400 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Fri, 5 Jan 2024 17:53:36 +0800 Subject: [PATCH 080/109] Save model in ubj as the default. (#9947) --- .../spark/ml/util/XGBoostReadWrite.scala | 3 - .../scala/spark/XGBoostClassifierSuite.scala | 17 +- .../scala/spark/XGBoostRegressorSuite.scala | 29 +- .../java/ml/dmlc/xgboost4j/java/Booster.java | 5 +- .../ml/dmlc/xgboost4j/scala/Booster.scala | 3 +- python-package/xgboost/core.py | 2 +- python-package/xgboost/testing/__init__.py | 5 +- src/c_api/c_api.cc | 16 +- tests/ci_build/lint_python.py | 2 + tests/python/test_basic.py | 182 ++++---- tests/python/test_basic_models.py | 194 +-------- tests/python/test_callback.py | 32 +- tests/python/test_config.py | 10 +- tests/python/test_dmatrix.py | 39 +- tests/python/test_early_stopping.py | 6 +- tests/python/test_eval_metrics.py | 24 +- tests/python/test_linear.py | 10 +- tests/python/test_model_io.py | 406 ++++++++++++++++++ tests/python/test_pickling.py | 27 -- tests/python/test_shap.py | 2 +- tests/python/test_updaters.py | 4 +- tests/python/test_with_pandas.py | 7 - tests/python/test_with_sklearn.py | 123 ------ 23 files changed, 598 insertions(+), 550 deletions(-) create mode 100644 tests/python/test_model_io.py diff --git a/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala b/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala index 672241be1..ff732b78c 100644 --- a/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala +++ b/jvm-packages/xgboost4j-spark/src/main/scala/org/apache/spark/ml/util/XGBoostReadWrite.scala @@ -30,9 +30,6 @@ import org.apache.spark.ml.param.Params import org.apache.spark.ml.util.DefaultParamsReader.Metadata abstract class XGBoostWriter extends MLWriter { - - /** Currently it's using the "deprecated" format as - * default, which will be changed into `ubj` in future releases. */ def getModelFormat(): String = { optionMap.getOrElse("format", JBooster.DEFAULT_FORMAT) } diff --git a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala index 9b53c7642..48e7dae52 100644 --- a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala +++ b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostClassifierSuite.scala @@ -1,5 +1,5 @@ /* - Copyright (c) 2014-2022 by Contributors + Copyright (c) 2014-2024 by Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -432,6 +432,7 @@ class XGBoostClassifierSuite extends AnyFunSuite with PerTest with TmpFolderPerS val xgb = new XGBoostClassifier(paramMap) val model = xgb.fit(trainingDF) + // test json val modelPath = new File(tempDir.toFile, "xgbc").getPath model.write.option("format", "json").save(modelPath) val nativeJsonModelPath = new File(tempDir.toFile, "nativeModel.json").getPath @@ -439,21 +440,21 @@ class XGBoostClassifierSuite extends AnyFunSuite with PerTest with TmpFolderPerS assert(compareTwoFiles(new File(modelPath, "data/XGBoostClassificationModel").getPath, nativeJsonModelPath)) - // test default "deprecated" + // test ubj val modelUbjPath = new File(tempDir.toFile, "xgbcUbj").getPath model.write.save(modelUbjPath) - val nativeDeprecatedModelPath = new File(tempDir.toFile, "nativeModel").getPath - model.nativeBooster.saveModel(nativeDeprecatedModelPath) + val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel.ubj").getPath + model.nativeBooster.saveModel(nativeUbjModelPath) assert(compareTwoFiles(new File(modelUbjPath, "data/XGBoostClassificationModel").getPath, - nativeDeprecatedModelPath)) + nativeUbjModelPath)) // json file should be indifferent with ubj file val modelJsonPath = new File(tempDir.toFile, "xgbcJson").getPath model.write.option("format", "json").save(modelJsonPath) - val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel1.ubj").getPath - model.nativeBooster.saveModel(nativeUbjModelPath) + val nativeUbjModelPath1 = new File(tempDir.toFile, "nativeModel1.ubj").getPath + model.nativeBooster.saveModel(nativeUbjModelPath1) assert(!compareTwoFiles(new File(modelJsonPath, "data/XGBoostClassificationModel").getPath, - nativeUbjModelPath)) + nativeUbjModelPath1)) } test("native json model file should store feature_name and feature_type") { diff --git a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala index 1bdea7a82..0698541c7 100644 --- a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala +++ b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/XGBoostRegressorSuite.scala @@ -1,5 +1,5 @@ /* - Copyright (c) 2014-2022 by Contributors + Copyright (c) 2014-2024 by Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -333,21 +333,24 @@ class XGBoostRegressorSuite extends AnyFunSuite with PerTest with TmpFolderPerSu assert(compareTwoFiles(new File(modelPath, "data/XGBoostRegressionModel").getPath, nativeJsonModelPath)) - // test default "deprecated" + // test default "ubj" val modelUbjPath = new File(tempDir.toFile, "xgbcUbj").getPath model.write.save(modelUbjPath) - val nativeDeprecatedModelPath = new File(tempDir.toFile, "nativeModel").getPath - model.nativeBooster.saveModel(nativeDeprecatedModelPath) - assert(compareTwoFiles(new File(modelUbjPath, "data/XGBoostRegressionModel").getPath, - nativeDeprecatedModelPath)) - // json file should be indifferent with ubj file - val modelJsonPath = new File(tempDir.toFile, "xgbcJson").getPath - model.write.option("format", "json").save(modelJsonPath) - val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel1.ubj").getPath + val nativeUbjModelPath = new File(tempDir.toFile, "nativeModel.ubj").getPath model.nativeBooster.saveModel(nativeUbjModelPath) - assert(!compareTwoFiles(new File(modelJsonPath, "data/XGBoostRegressionModel").getPath, - nativeUbjModelPath)) - } + assert(compareTwoFiles(new File(modelUbjPath, "data/XGBoostRegressionModel").getPath, + nativeUbjModelPath)) + + // test the deprecated format + val modelDeprecatedPath = new File(tempDir.toFile, "modelDeprecated").getPath + model.write.option("format", "deprecated").save(modelDeprecatedPath) + + val nativeDeprecatedModelPath = new File(tempDir.toFile, "nativeModel.deprecated").getPath + model.nativeBooster.saveModel(nativeDeprecatedModelPath) + + assert(compareTwoFiles(new File(modelDeprecatedPath, "data/XGBoostRegressionModel").getPath, + nativeDeprecatedModelPath)) + } } diff --git a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java index 51959ce0c..22ed6dc82 100644 --- a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java +++ b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/Booster.java @@ -34,7 +34,7 @@ import org.apache.commons.logging.LogFactory; * Booster for xgboost, this is a model API that support interactive build of a XGBoost Model */ public class Booster implements Serializable, KryoSerializable { - public static final String DEFAULT_FORMAT = "deprecated"; + public static final String DEFAULT_FORMAT = "ubj"; private static final Log logger = LogFactory.getLog(Booster.class); // handle to the booster. private long handle = 0; @@ -788,8 +788,7 @@ public class Booster implements Serializable, KryoSerializable { } /** - * Save model into raw byte array. Currently it's using the deprecated format as - * default, which will be changed into `ubj` in future releases. + * Save model into raw byte array in the UBJSON ("ubj") format. * * @return the saved byte array * @throws XGBoostError native error diff --git a/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala b/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala index c288bfab1..57c3b9a5d 100644 --- a/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala +++ b/jvm-packages/xgboost4j/src/main/scala/ml/dmlc/xgboost4j/scala/Booster.scala @@ -337,8 +337,7 @@ class Booster private[xgboost4j](private[xgboost4j] var booster: JBooster) } /** - * Save model into a raw byte array. Currently it's using the deprecated format as - * default, which will be changed into `ubj` in future releases. + * Save model into a raw byte array in the UBJSON ("ubj") format. */ @throws(classOf[XGBoostError]) def toByteArray: Array[Byte] = { diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 097fb0935..d554a8d3a 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -2613,7 +2613,7 @@ class Booster: else: raise TypeError("fname must be a string or os PathLike") - def save_raw(self, raw_format: str = "deprecated") -> bytearray: + def save_raw(self, raw_format: str = "ubj") -> bytearray: """Save the model to a in memory buffer representation instead of file. Parameters diff --git a/python-package/xgboost/testing/__init__.py b/python-package/xgboost/testing/__init__.py index 373ad1c58..46bbf8800 100644 --- a/python-package/xgboost/testing/__init__.py +++ b/python-package/xgboost/testing/__init__.py @@ -630,7 +630,7 @@ sparse_datasets_strategy = strategies.sampled_from( def make_datasets_with_margin( unweighted_strategy: strategies.SearchStrategy, -) -> Callable: +) -> Callable[[], strategies.SearchStrategy[TestDataset]]: """Factory function for creating strategies that generates datasets with weight and base margin. @@ -668,8 +668,7 @@ def make_datasets_with_margin( # A strategy for drawing from a set of example datasets. May add random weights to the # dataset -@memory.cache -def make_dataset_strategy() -> Callable: +def make_dataset_strategy() -> strategies.SearchStrategy[TestDataset]: _unweighted_datasets_strategy = strategies.sampled_from( [ TestDataset( diff --git a/src/c_api/c_api.cc b/src/c_api/c_api.cc index 9aedcef2e..d4cc217d1 100644 --- a/src/c_api/c_api.cc +++ b/src/c_api/c_api.cc @@ -1313,10 +1313,8 @@ XGB_DLL int XGBoosterLoadModel(BoosterHandle handle, const char* fname) { namespace { void WarnOldModel() { - if (XGBOOST_VER_MAJOR >= 2) { - LOG(WARNING) << "Saving into deprecated binary model format, please consider using `json` or " - "`ubj`. Model format will default to JSON in XGBoost 2.2 if not specified."; - } + LOG(WARNING) << "Saving into deprecated binary model format, please consider using `json` or " + "`ubj`. Model format is default to UBJSON in XGBoost 2.1 if not specified."; } } // anonymous namespace @@ -1339,14 +1337,14 @@ XGB_DLL int XGBoosterSaveModel(BoosterHandle handle, const char *fname) { save_json(std::ios::out); } else if (common::FileExtension(fname) == "ubj") { save_json(std::ios::binary); - } else if (XGBOOST_VER_MAJOR == 2 && XGBOOST_VER_MINOR >= 2) { - LOG(WARNING) << "Saving model to JSON as default. You can use file extension `json`, `ubj` or " - "`deprecated` to choose between formats."; - save_json(std::ios::out); - } else { + } else if (common::FileExtension(fname) == "deprecated") { WarnOldModel(); auto *bst = static_cast(handle); bst->SaveModel(fo.get()); + } else { + LOG(WARNING) << "Saving model in the UBJSON format as default. You can use file extension:" + " `json`, `ubj` or `deprecated` to choose between formats."; + save_json(std::ios::binary); } API_END(); } diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index ed33a96e5..1e414f3b5 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -27,6 +27,7 @@ class LintersPaths: "tests/python/test_quantile_dmatrix.py", "tests/python/test_tree_regularization.py", "tests/python/test_shap.py", + "tests/python/test_model_io.py", "tests/python/test_with_pandas.py", "tests/python-gpu/", "tests/python-sycl/", @@ -83,6 +84,7 @@ class LintersPaths: "tests/python/test_multi_target.py", "tests/python-gpu/test_gpu_data_iterator.py", "tests/python-gpu/load_pickle.py", + "tests/python/test_model_io.py", "tests/test_distributed/test_with_spark/test_data.py", "tests/test_distributed/test_gpu_with_spark/test_data.py", "tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py", diff --git a/tests/python/test_basic.py b/tests/python/test_basic.py index b99351c7f..cdc571a91 100644 --- a/tests/python/test_basic.py +++ b/tests/python/test_basic.py @@ -10,46 +10,48 @@ import pytest import xgboost as xgb from xgboost import testing as tm -dpath = 'demo/data/' +dpath = "demo/data/" rng = np.random.RandomState(1994) class TestBasic: def test_compat(self): from xgboost.compat import lazy_isinstance + a = np.array([1, 2, 3]) - assert lazy_isinstance(a, 'numpy', 'ndarray') - assert not lazy_isinstance(a, 'numpy', 'dataframe') + assert lazy_isinstance(a, "numpy", "ndarray") + assert not lazy_isinstance(a, "numpy", "dataframe") def test_basic(self): dtrain, dtest = tm.load_agaricus(__file__) - param = {'max_depth': 2, 'eta': 1, - 'objective': 'binary:logistic'} + param = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} # specify validations set to watch performance - watchlist = [(dtrain, 'train')] + watchlist = [(dtrain, "train")] num_round = 2 - bst = xgb.train(param, dtrain, num_round, watchlist, verbose_eval=True) + bst = xgb.train(param, dtrain, num_round, evals=watchlist, verbose_eval=True) preds = bst.predict(dtrain) labels = dtrain.get_label() - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) # error must be smaller than 10% assert err < 0.1 preds = bst.predict(dtest) labels = dtest.get_label() - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) # error must be smaller than 10% assert err < 0.1 with tempfile.TemporaryDirectory() as tmpdir: - dtest_path = os.path.join(tmpdir, 'dtest.dmatrix') + dtest_path = os.path.join(tmpdir, "dtest.dmatrix") # save dmatrix into binary buffer dtest.save_binary(dtest_path) # save model - model_path = os.path.join(tmpdir, 'model.booster') + model_path = os.path.join(tmpdir, "model.ubj") bst.save_model(model_path) # load model and data in bst2 = xgb.Booster(model_file=model_path) @@ -59,17 +61,21 @@ class TestBasic: assert np.sum(np.abs(preds2 - preds)) == 0 def test_metric_config(self): - # Make sure that the metric configuration happens in booster so the - # string `['error', 'auc']` doesn't get passed down to core. + # Make sure that the metric configuration happens in booster so the string + # `['error', 'auc']` doesn't get passed down to core. dtrain, dtest = tm.load_agaricus(__file__) - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': ['error', 'auc']} - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + param = { + "max_depth": 2, + "eta": 1, + "objective": "binary:logistic", + "eval_metric": ["error", "auc"], + } + watchlist = [(dtest, "eval"), (dtrain, "train")] num_round = 2 - booster = xgb.train(param, dtrain, num_round, watchlist) + booster = xgb.train(param, dtrain, num_round, evals=watchlist) predt_0 = booster.predict(dtrain) with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, 'model.json') + path = os.path.join(tmpdir, "model.json") booster.save_model(path) booster = xgb.Booster(params=param, model_file=path) @@ -78,22 +84,23 @@ class TestBasic: def test_multiclass(self): dtrain, dtest = tm.load_agaricus(__file__) - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, 'num_class': 2} + param = {"max_depth": 2, "eta": 1, "num_class": 2} # specify validations set to watch performance - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + watchlist = [(dtest, "eval"), (dtrain, "train")] num_round = 2 - bst = xgb.train(param, dtrain, num_round, watchlist) + bst = xgb.train(param, dtrain, num_round, evals=watchlist) # this is prediction preds = bst.predict(dtest) labels = dtest.get_label() - err = sum(1 for i in range(len(preds)) - if preds[i] != labels[i]) / float(len(preds)) + err = sum(1 for i in range(len(preds)) if preds[i] != labels[i]) / float( + len(preds) + ) # error must be smaller than 10% assert err < 0.1 with tempfile.TemporaryDirectory() as tmpdir: - dtest_path = os.path.join(tmpdir, 'dtest.buffer') - model_path = os.path.join(tmpdir, 'xgb.model') + dtest_path = os.path.join(tmpdir, "dtest.buffer") + model_path = os.path.join(tmpdir, "model.ubj") # save dmatrix into binary buffer dtest.save_binary(dtest_path) # save model @@ -108,33 +115,39 @@ class TestBasic: def test_dump(self): data = np.random.randn(100, 2) target = np.array([0, 1] * 50) - features = ['Feature1', 'Feature2'] + features = ["Feature1", "Feature2"] dm = xgb.DMatrix(data, label=target, feature_names=features) - params = {'objective': 'binary:logistic', - 'eval_metric': 'logloss', - 'eta': 0.3, - 'max_depth': 1} + params = { + "objective": "binary:logistic", + "eval_metric": "logloss", + "eta": 0.3, + "max_depth": 1, + } bst = xgb.train(params, dm, num_boost_round=1) # number of feature importances should == number of features dump1 = bst.get_dump() - assert len(dump1) == 1, 'Expected only 1 tree to be dumped.' - len(dump1[0].splitlines()) == 3, 'Expected 1 root and 2 leaves - 3 lines in dump.' + assert len(dump1) == 1, "Expected only 1 tree to be dumped." + len( + dump1[0].splitlines() + ) == 3, "Expected 1 root and 2 leaves - 3 lines in dump." dump2 = bst.get_dump(with_stats=True) - assert dump2[0].count('\n') == 3, 'Expected 1 root and 2 leaves - 3 lines in dump.' - msg = 'Expected more info when with_stats=True is given.' - assert dump2[0].find('\n') > dump1[0].find('\n'), msg + assert ( + dump2[0].count("\n") == 3 + ), "Expected 1 root and 2 leaves - 3 lines in dump." + msg = "Expected more info when with_stats=True is given." + assert dump2[0].find("\n") > dump1[0].find("\n"), msg dump3 = bst.get_dump(dump_format="json") dump3j = json.loads(dump3[0]) - assert dump3j['nodeid'] == 0, 'Expected the root node on top.' + assert dump3j["nodeid"] == 0, "Expected the root node on top." dump4 = bst.get_dump(dump_format="json", with_stats=True) dump4j = json.loads(dump4[0]) - assert 'gain' in dump4j, "Expected 'gain' to be dumped in JSON." + assert "gain" in dump4j, "Expected 'gain' to be dumped in JSON." with pytest.raises(ValueError): bst.get_dump(fmap="foo") @@ -163,12 +176,14 @@ class TestBasic: def test_load_file_invalid(self): with pytest.raises(xgb.core.XGBoostError): - xgb.Booster(model_file='incorrect_path') + xgb.Booster(model_file="incorrect_path") with pytest.raises(xgb.core.XGBoostError): - xgb.Booster(model_file=u'不正なパス') + xgb.Booster(model_file="不正なパス") - @pytest.mark.parametrize("path", ["모델.ubj", "がうる・ぐら.json"], ids=["path-0", "path-1"]) + @pytest.mark.parametrize( + "path", ["모델.ubj", "がうる・ぐら.json"], ids=["path-0", "path-1"] + ) def test_unicode_path(self, tmpdir, path): model_path = pathlib.Path(tmpdir) / path dtrain, _ = tm.load_agaricus(__file__) @@ -180,12 +195,11 @@ class TestBasic: assert bst.get_dump(dump_format="text") == bst2.get_dump(dump_format="text") def test_dmatrix_numpy_init_omp(self): - rows = [1000, 11326, 15000] cols = 50 for row in rows: X = np.random.randn(row, cols) - y = np.random.randn(row).astype('f') + y = np.random.randn(row).astype("f") dm = xgb.DMatrix(X, y, nthread=0) np.testing.assert_array_equal(dm.get_label(), y) assert dm.num_row() == row @@ -198,8 +212,7 @@ class TestBasic: def test_cv(self): dm, _ = tm.load_agaricus(__file__) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + params = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} # return np.ndarray cv = xgb.cv(params, dm, num_boost_round=10, nfold=10, as_pandas=False) @@ -208,19 +221,18 @@ class TestBasic: def test_cv_no_shuffle(self): dm, _ = tm.load_agaricus(__file__) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + params = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} # return np.ndarray - cv = xgb.cv(params, dm, num_boost_round=10, shuffle=False, nfold=10, - as_pandas=False) + cv = xgb.cv( + params, dm, num_boost_round=10, shuffle=False, nfold=10, as_pandas=False + ) assert isinstance(cv, dict) assert len(cv) == (4) def test_cv_explicit_fold_indices(self): dm, _ = tm.load_agaricus(__file__) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, 'objective': - 'binary:logistic'} + params = {"max_depth": 2, "eta": 1, "objective": "binary:logistic"} folds = [ # Train Test ([1, 3], [5, 8]), @@ -228,15 +240,13 @@ class TestBasic: ] # return np.ndarray - cv = xgb.cv(params, dm, num_boost_round=10, folds=folds, - as_pandas=False) + cv = xgb.cv(params, dm, num_boost_round=10, folds=folds, as_pandas=False) assert isinstance(cv, dict) assert len(cv) == (4) @pytest.mark.skipif(**tm.skip_s390x()) def test_cv_explicit_fold_indices_labels(self): - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, 'objective': - 'reg:squarederror'} + params = {"max_depth": 2, "eta": 1, "objective": "reg:squarederror"} N = 100 F = 3 dm = xgb.DMatrix(data=np.random.randn(N, F), label=np.arange(N)) @@ -252,9 +262,10 @@ class TestBasic: super().__init__() def after_iteration( - self, model, + self, + model, epoch: int, - evals_log: xgb.callback.TrainingCallback.EvalsLog + evals_log: xgb.callback.TrainingCallback.EvalsLog, ): print([fold.dtest.get_label() for fold in model.cvfolds]) @@ -263,12 +274,18 @@ class TestBasic: # Run cross validation and capture standard out to test callback result with tm.captured_output() as (out, err): xgb.cv( - params, dm, num_boost_round=1, folds=folds, callbacks=[cb], - as_pandas=False + params, + dm, + num_boost_round=1, + folds=folds, + callbacks=[cb], + as_pandas=False, ) output = out.getvalue().strip() - solution = ('[array([5., 8.], dtype=float32), array([23., 43., 11.],' + - ' dtype=float32)]') + solution = ( + "[array([5., 8.], dtype=float32), array([23., 43., 11.]," + + " dtype=float32)]" + ) assert output == solution @@ -285,7 +302,7 @@ class TestBasicPathLike: """Saving to a binary file using pathlib from a DMatrix.""" data = np.random.randn(100, 2) target = np.array([0, 1] * 50) - features = ['Feature1', 'Feature2'] + features = ["Feature1", "Feature2"] dm = xgb.DMatrix(data, label=target, feature_names=features) @@ -299,42 +316,3 @@ class TestBasicPathLike: """An invalid model_file path should raise XGBoostError.""" with pytest.raises(xgb.core.XGBoostError): xgb.Booster(model_file=Path("invalidpath")) - - def test_Booster_save_and_load(self): - """Saving and loading model files from paths.""" - save_path = Path("saveload.model") - - data = np.random.randn(100, 2) - target = np.array([0, 1] * 50) - features = ['Feature1', 'Feature2'] - - dm = xgb.DMatrix(data, label=target, feature_names=features) - params = {'objective': 'binary:logistic', - 'eval_metric': 'logloss', - 'eta': 0.3, - 'max_depth': 1} - - bst = xgb.train(params, dm, num_boost_round=1) - - # save, assert exists - bst.save_model(save_path) - assert save_path.exists() - - def dump_assertions(dump): - """Assertions for the expected dump from Booster""" - assert len(dump) == 1, 'Exepcted only 1 tree to be dumped.' - assert len(dump[0].splitlines()) == 3, 'Expected 1 root and 2 leaves - 3 lines.' - - # load the model again using Path - bst2 = xgb.Booster(model_file=save_path) - dump2 = bst2.get_dump() - dump_assertions(dump2) - - # load again using load_model - bst3 = xgb.Booster() - bst3.load_model(save_path) - dump3 = bst3.get_dump() - dump_assertions(dump3) - - # remove file - Path.unlink(save_path) diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index 8f83e1fcc..ca35c4e91 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -15,33 +15,9 @@ dpath = tm.data_dir(__file__) rng = np.random.RandomState(1994) -def json_model(model_path: str, parameters: dict) -> dict: - datasets = pytest.importorskip("sklearn.datasets") - - X, y = datasets.make_classification(64, n_features=8, n_classes=3, n_informative=6) - if parameters.get("objective", None) == "multi:softmax": - parameters["num_class"] = 3 - - dm1 = xgb.DMatrix(X, y) - - bst = xgb.train(parameters, dm1) - bst.save_model(model_path) - - if model_path.endswith("ubj"): - import ubjson - - with open(model_path, "rb") as ubjfd: - model = ubjson.load(ubjfd) - else: - with open(model_path, "r") as fd: - model = json.load(fd) - - return model - - class TestModels: def test_glm(self): - param = {'verbosity': 0, 'objective': 'binary:logistic', + param = {'objective': 'binary:logistic', 'booster': 'gblinear', 'alpha': 0.0001, 'lambda': 1, 'nthread': 1} dtrain, dtest = tm.load_agaricus(__file__) @@ -73,7 +49,7 @@ class TestModels: with tempfile.TemporaryDirectory() as tmpdir: dtest_path = os.path.join(tmpdir, 'dtest.dmatrix') - model_path = os.path.join(tmpdir, 'xgboost.model.dart') + model_path = os.path.join(tmpdir, "xgboost.model.dart.ubj") # save dmatrix into binary buffer dtest.save_binary(dtest_path) model_path = model_path @@ -101,7 +77,6 @@ class TestModels: # check whether sample_type and normalize_type work num_round = 50 - param['verbosity'] = 0 param['learning_rate'] = 0.1 param['rate_drop'] = 0.1 preds_list = [] @@ -214,8 +189,7 @@ class TestModels: assert set(evals_result['eval'].keys()) == {'auc', 'error', 'logloss'} def test_fpreproc(self): - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + param = {'max_depth': 2, 'eta': 1, 'objective': 'binary:logistic'} num_round = 2 def fpreproc(dtrain, dtest, param): @@ -229,8 +203,7 @@ class TestModels: metrics={'auc'}, seed=0, fpreproc=fpreproc) def test_show_stdv(self): - param = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic'} + param = {'max_depth': 2, 'eta': 1, 'objective': 'binary:logistic'} num_round = 2 dtrain, _ = tm.load_agaricus(__file__) xgb.cv(param, dtrain, num_round, nfold=5, @@ -273,142 +246,6 @@ class TestModels: bst = xgb.train([], dm2) bst.predict(dm2) # success - def test_model_binary_io(self): - model_path = 'test_model_binary_io.bin' - parameters = {'tree_method': 'hist', 'booster': 'gbtree', - 'scale_pos_weight': '0.5'} - X = np.random.random((10, 3)) - y = np.random.random((10,)) - dtrain = xgb.DMatrix(X, y) - bst = xgb.train(parameters, dtrain, num_boost_round=2) - bst.save_model(model_path) - bst = xgb.Booster(model_file=model_path) - os.remove(model_path) - config = json.loads(bst.save_config()) - assert float(config['learner']['objective'][ - 'reg_loss_param']['scale_pos_weight']) == 0.5 - - buf = bst.save_raw() - from_raw = xgb.Booster() - from_raw.load_model(buf) - - buf_from_raw = from_raw.save_raw() - assert buf == buf_from_raw - - def run_model_json_io(self, parameters: dict, ext: str) -> None: - if ext == "ubj" and tm.no_ubjson()["condition"]: - pytest.skip(tm.no_ubjson()["reason"]) - - loc = locale.getpreferredencoding(False) - model_path = 'test_model_json_io.' + ext - j_model = json_model(model_path, parameters) - assert isinstance(j_model['learner'], dict) - - bst = xgb.Booster(model_file=model_path) - - bst.save_model(fname=model_path) - if ext == "ubj": - import ubjson - with open(model_path, "rb") as ubjfd: - j_model = ubjson.load(ubjfd) - else: - with open(model_path, 'r') as fd: - j_model = json.load(fd) - - assert isinstance(j_model['learner'], dict) - - os.remove(model_path) - assert locale.getpreferredencoding(False) == loc - - json_raw = bst.save_raw(raw_format="json") - from_jraw = xgb.Booster() - from_jraw.load_model(json_raw) - - ubj_raw = bst.save_raw(raw_format="ubj") - from_ubjraw = xgb.Booster() - from_ubjraw.load_model(ubj_raw) - - if parameters.get("multi_strategy", None) != "multi_output_tree": - # old binary model is not supported. - old_from_json = from_jraw.save_raw(raw_format="deprecated") - old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") - - assert old_from_json == old_from_ubj - - raw_json = bst.save_raw(raw_format="json") - pretty = json.dumps(json.loads(raw_json), indent=2) + "\n\n" - bst.load_model(bytearray(pretty, encoding="ascii")) - - if parameters.get("multi_strategy", None) != "multi_output_tree": - # old binary model is not supported. - old_from_json = from_jraw.save_raw(raw_format="deprecated") - old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") - - assert old_from_json == old_from_ubj - - rng = np.random.default_rng() - X = rng.random(size=from_jraw.num_features() * 10).reshape( - (10, from_jraw.num_features()) - ) - predt_from_jraw = from_jraw.predict(xgb.DMatrix(X)) - predt_from_bst = bst.predict(xgb.DMatrix(X)) - np.testing.assert_allclose(predt_from_jraw, predt_from_bst) - - @pytest.mark.parametrize("ext", ["json", "ubj"]) - def test_model_json_io(self, ext: str) -> None: - parameters = {"booster": "gbtree", "tree_method": "hist"} - self.run_model_json_io(parameters, ext) - parameters = { - "booster": "gbtree", - "tree_method": "hist", - "multi_strategy": "multi_output_tree", - "objective": "multi:softmax", - } - self.run_model_json_io(parameters, ext) - parameters = {"booster": "gblinear"} - self.run_model_json_io(parameters, ext) - parameters = {"booster": "dart", "tree_method": "hist"} - self.run_model_json_io(parameters, ext) - - @pytest.mark.skipif(**tm.no_json_schema()) - def test_json_io_schema(self): - import jsonschema - model_path = 'test_json_schema.json' - path = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - doc = os.path.join(path, 'doc', 'model.schema') - with open(doc, 'r') as fd: - schema = json.load(fd) - parameters = {'tree_method': 'hist', 'booster': 'gbtree'} - jsonschema.validate(instance=json_model(model_path, parameters), - schema=schema) - os.remove(model_path) - - parameters = {'tree_method': 'hist', 'booster': 'dart'} - jsonschema.validate(instance=json_model(model_path, parameters), - schema=schema) - os.remove(model_path) - - try: - dtrain, _ = tm.load_agaricus(__file__) - xgb.train({'objective': 'foo'}, dtrain, num_boost_round=1) - except ValueError as e: - e_str = str(e) - beg = e_str.find('Objective candidate') - end = e_str.find('Stack trace') - e_str = e_str[beg: end] - e_str = e_str.strip() - splited = e_str.splitlines() - objectives = [s.split(': ')[1] for s in splited] - j_objectives = schema['properties']['learner']['properties'][ - 'objective']['oneOf'] - objectives_from_schema = set() - for j_obj in j_objectives: - objectives_from_schema.add( - j_obj['properties']['name']['const']) - objectives = set(objectives) - assert objectives == objectives_from_schema - @pytest.mark.skipif(**tm.no_json_schema()) def test_json_dump_schema(self): import jsonschema @@ -470,29 +307,6 @@ class TestModels: for d in text_dump: assert d.find(r"feature \"2\"") != -1 - def test_categorical_model_io(self): - X, y = tm.make_categorical(256, 16, 71, False) - Xy = xgb.DMatrix(X, y, enable_categorical=True) - booster = xgb.train({"tree_method": "approx"}, Xy, num_boost_round=16) - predt_0 = booster.predict(Xy) - - with tempfile.TemporaryDirectory() as tempdir: - path = os.path.join(tempdir, "model.binary") - with pytest.raises(ValueError, match=r".*JSON/UBJSON.*"): - booster.save_model(path) - - path = os.path.join(tempdir, "model.json") - booster.save_model(path) - booster = xgb.Booster(model_file=path) - predt_1 = booster.predict(Xy) - np.testing.assert_allclose(predt_0, predt_1) - - path = os.path.join(tempdir, "model.ubj") - booster.save_model(path) - booster = xgb.Booster(model_file=path) - predt_1 = booster.predict(Xy) - np.testing.assert_allclose(predt_0, predt_1) - @pytest.mark.skipif(**tm.no_sklearn()) def test_attributes(self): from sklearn.datasets import load_iris diff --git a/tests/python/test_callback.py b/tests/python/test_callback.py index 262c09c99..4893ad074 100644 --- a/tests/python/test_callback.py +++ b/tests/python/test_callback.py @@ -278,14 +278,18 @@ class TestCallbacks: dtrain, dtest = tm.load_agaricus(__file__) - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + watchlist = [(dtest, "eval"), (dtrain, "train")] num_round = 4 # learning_rates as a list # init eta with 0 to check whether learning_rates work - param = {'max_depth': 2, 'eta': 0, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': 'error', - 'tree_method': tree_method} + param = { + "max_depth": 2, + "eta": 0, + "objective": "binary:logistic", + "eval_metric": "error", + "tree_method": tree_method, + } evals_result = {} bst = xgb.train( param, @@ -295,15 +299,19 @@ class TestCallbacks: callbacks=[scheduler([0.8, 0.7, 0.6, 0.5])], evals_result=evals_result, ) - eval_errors_0 = list(map(float, evals_result['eval']['error'])) + eval_errors_0 = list(map(float, evals_result["eval"]["error"])) assert isinstance(bst, xgb.core.Booster) # validation error should decrease, if eta > 0 assert eval_errors_0[0] > eval_errors_0[-1] # init learning_rate with 0 to check whether learning_rates work - param = {'max_depth': 2, 'learning_rate': 0, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': 'error', - 'tree_method': tree_method} + param = { + "max_depth": 2, + "learning_rate": 0, + "objective": "binary:logistic", + "eval_metric": "error", + "tree_method": tree_method, + } evals_result = {} bst = xgb.train( @@ -314,15 +322,17 @@ class TestCallbacks: callbacks=[scheduler([0.8, 0.7, 0.6, 0.5])], evals_result=evals_result, ) - eval_errors_1 = list(map(float, evals_result['eval']['error'])) + eval_errors_1 = list(map(float, evals_result["eval"]["error"])) assert isinstance(bst, xgb.core.Booster) # validation error should decrease, if learning_rate > 0 assert eval_errors_1[0] > eval_errors_1[-1] # check if learning_rates override default value of eta/learning_rate param = { - 'max_depth': 2, 'verbosity': 0, 'objective': 'binary:logistic', - 'eval_metric': 'error', 'tree_method': tree_method + "max_depth": 2, + "objective": "binary:logistic", + "eval_metric": "error", + "tree_method": tree_method, } evals_result = {} bst = xgb.train( diff --git a/tests/python/test_config.py b/tests/python/test_config.py index 01b5c2d99..3f741c25d 100644 --- a/tests/python/test_config.py +++ b/tests/python/test_config.py @@ -12,6 +12,7 @@ def test_global_config_verbosity(verbosity_level): return xgb.get_config()["verbosity"] old_verbosity = get_current_verbosity() + assert old_verbosity == 1 with xgb.config_context(verbosity=verbosity_level): new_verbosity = get_current_verbosity() assert new_verbosity == verbosity_level @@ -30,7 +31,10 @@ def test_global_config_use_rmm(use_rmm): assert old_use_rmm_flag == get_current_use_rmm_flag() -def test_nested_config(): +def test_nested_config() -> None: + verbosity = xgb.get_config()["verbosity"] + assert verbosity == 1 + with xgb.config_context(verbosity=3): assert xgb.get_config()["verbosity"] == 3 with xgb.config_context(verbosity=2): @@ -45,13 +49,15 @@ def test_nested_config(): with xgb.config_context(verbosity=None): assert xgb.get_config()["verbosity"] == 3 # None has no effect - verbosity = xgb.get_config()["verbosity"] xgb.set_config(verbosity=2) assert xgb.get_config()["verbosity"] == 2 with xgb.config_context(verbosity=3): assert xgb.get_config()["verbosity"] == 3 xgb.set_config(verbosity=verbosity) # reset + verbosity = xgb.get_config()["verbosity"] + assert verbosity == 1 + def test_thread_safty(): n_threads = multiprocessing.cpu_count() diff --git a/tests/python/test_dmatrix.py b/tests/python/test_dmatrix.py index c718378c5..9d123ddb9 100644 --- a/tests/python/test_dmatrix.py +++ b/tests/python/test_dmatrix.py @@ -1,6 +1,7 @@ import csv import os import tempfile +import warnings import numpy as np import pytest @@ -24,20 +25,18 @@ class TestDMatrix: with pytest.warns(UserWarning): data._warn_unused_missing("uri", 4) - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") data._warn_unused_missing("uri", None) data._warn_unused_missing("uri", np.nan) - assert len(record) == 0 - - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") x = rng.randn(10, 10) y = rng.randn(10) xgb.DMatrix(x, y, missing=4) - assert len(record) == 0 - def test_dmatrix_numpy_init(self): data = np.random.randn(5, 5) dm = xgb.DMatrix(data) @@ -264,7 +263,7 @@ class TestDMatrix: dtrain = xgb.DMatrix(x, label=rng.binomial(1, 0.3, nrow)) assert (dtrain.num_row(), dtrain.num_col()) == (nrow, ncol) watchlist = [(dtrain, "train")] - param = {"max_depth": 3, "objective": "binary:logistic", "verbosity": 0} + param = {"max_depth": 3, "objective": "binary:logistic"} bst = xgb.train(param, dtrain, 5, watchlist) bst.predict(dtrain) @@ -302,7 +301,7 @@ class TestDMatrix: dtrain = xgb.DMatrix(x, label=rng.binomial(1, 0.3, nrow)) assert (dtrain.num_row(), dtrain.num_col()) == (nrow, ncol) watchlist = [(dtrain, "train")] - param = {"max_depth": 3, "objective": "binary:logistic", "verbosity": 0} + param = {"max_depth": 3, "objective": "binary:logistic"} bst = xgb.train(param, dtrain, 5, watchlist) bst.predict(dtrain) @@ -475,17 +474,19 @@ class TestDMatrixColumnSplit: def test_uri(self): def verify_uri(): rank = xgb.collective.get_rank() - data = np.random.rand(5, 5) - filename = f"test_data_{rank}.csv" - with open(filename, mode="w", newline="") as file: - writer = csv.writer(file) - for row in data: - writer.writerow(row) - dtrain = xgb.DMatrix( - f"{filename}?format=csv", data_split_mode=DataSplitMode.COL - ) - assert dtrain.num_row() == 5 - assert dtrain.num_col() == 5 * xgb.collective.get_world_size() + with tempfile.TemporaryDirectory() as tmpdir: + filename = os.path.join(tmpdir, f"test_data_{rank}.csv") + + data = np.random.rand(5, 5) + with open(filename, mode="w", newline="") as file: + writer = csv.writer(file) + for row in data: + writer.writerow(row) + dtrain = xgb.DMatrix( + f"{filename}?format=csv", data_split_mode=DataSplitMode.COL + ) + assert dtrain.num_row() == 5 + assert dtrain.num_col() == 5 * xgb.collective.get_world_size() tm.run_with_rabit(world_size=3, test_fn=verify_uri) diff --git a/tests/python/test_early_stopping.py b/tests/python/test_early_stopping.py index 47f58cbd6..7695c6861 100644 --- a/tests/python/test_early_stopping.py +++ b/tests/python/test_early_stopping.py @@ -67,8 +67,10 @@ class TestEarlyStopping: X = digits['data'] y = digits['target'] dm = xgb.DMatrix(X, label=y) - params = {'max_depth': 2, 'eta': 1, 'verbosity': 0, - 'objective': 'binary:logistic', 'eval_metric': 'error'} + params = { + 'max_depth': 2, 'eta': 1, 'objective': 'binary:logistic', + 'eval_metric': 'error' + } cv = xgb.cv(params, dm, num_boost_round=10, nfold=10, early_stopping_rounds=10) diff --git a/tests/python/test_eval_metrics.py b/tests/python/test_eval_metrics.py index 147c87a27..92726014b 100644 --- a/tests/python/test_eval_metrics.py +++ b/tests/python/test_eval_metrics.py @@ -9,29 +9,13 @@ rng = np.random.RandomState(1337) class TestEvalMetrics: - xgb_params_01 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': 'error' - } + xgb_params_01 = {'nthread': 1, 'eval_metric': 'error'} - xgb_params_02 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': ['error'] - } + xgb_params_02 = {'nthread': 1, 'eval_metric': ['error']} - xgb_params_03 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': ['rmse', 'error'] - } + xgb_params_03 = {'nthread': 1, 'eval_metric': ['rmse', 'error']} - xgb_params_04 = { - 'verbosity': 0, - 'nthread': 1, - 'eval_metric': ['error', 'rmse'] - } + xgb_params_04 = {'nthread': 1, 'eval_metric': ['error', 'rmse']} def evalerror_01(self, preds, dtrain): labels = dtrain.get_label() diff --git a/tests/python/test_linear.py b/tests/python/test_linear.py index 0a198a036..5d281d415 100644 --- a/tests/python/test_linear.py +++ b/tests/python/test_linear.py @@ -22,8 +22,14 @@ coord_strategy = strategies.fixed_dictionaries({ def train_result(param, dmat, num_rounds): result = {} - xgb.train(param, dmat, num_rounds, [(dmat, 'train')], verbose_eval=False, - evals_result=result) + xgb.train( + param, + dmat, + num_rounds, + evals=[(dmat, "train")], + verbose_eval=False, + evals_result=result, + ) return result diff --git a/tests/python/test_model_io.py b/tests/python/test_model_io.py new file mode 100644 index 000000000..dc843a7f4 --- /dev/null +++ b/tests/python/test_model_io.py @@ -0,0 +1,406 @@ +import json +import locale +import os +import pickle +import tempfile +from pathlib import Path +from typing import List + +import numpy as np +import pytest + +import xgboost as xgb +from xgboost import testing as tm + + +def json_model(model_path: str, parameters: dict) -> dict: + datasets = pytest.importorskip("sklearn.datasets") + + X, y = datasets.make_classification(64, n_features=8, n_classes=3, n_informative=6) + if parameters.get("objective", None) == "multi:softmax": + parameters["num_class"] = 3 + + dm1 = xgb.DMatrix(X, y) + + bst = xgb.train(parameters, dm1) + bst.save_model(model_path) + + if model_path.endswith("ubj"): + import ubjson + + with open(model_path, "rb") as ubjfd: + model = ubjson.load(ubjfd) + else: + with open(model_path, "r") as fd: + model = json.load(fd) + + return model + + +class TestBoosterIO: + def run_model_json_io(self, parameters: dict, ext: str) -> None: + config = xgb.config.get_config() + assert config["verbosity"] == 1 + + if ext == "ubj" and tm.no_ubjson()["condition"]: + pytest.skip(tm.no_ubjson()["reason"]) + + loc = locale.getpreferredencoding(False) + model_path = "test_model_json_io." + ext + j_model = json_model(model_path, parameters) + assert isinstance(j_model["learner"], dict) + + bst = xgb.Booster(model_file=model_path) + + bst.save_model(fname=model_path) + if ext == "ubj": + import ubjson + + with open(model_path, "rb") as ubjfd: + j_model = ubjson.load(ubjfd) + else: + with open(model_path, "r") as fd: + j_model = json.load(fd) + + assert isinstance(j_model["learner"], dict) + + os.remove(model_path) + assert locale.getpreferredencoding(False) == loc + + json_raw = bst.save_raw(raw_format="json") + from_jraw = xgb.Booster() + from_jraw.load_model(json_raw) + + ubj_raw = bst.save_raw(raw_format="ubj") + from_ubjraw = xgb.Booster() + from_ubjraw.load_model(ubj_raw) + + if parameters.get("multi_strategy", None) != "multi_output_tree": + # Old binary model is not supported for vector leaf. + with pytest.warns(Warning, match="Model format is default to UBJSON"): + old_from_json = from_jraw.save_raw(raw_format="deprecated") + old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") + + assert old_from_json == old_from_ubj + + raw_json = bst.save_raw(raw_format="json") + pretty = json.dumps(json.loads(raw_json), indent=2) + "\n\n" + bst.load_model(bytearray(pretty, encoding="ascii")) + + if parameters.get("multi_strategy", None) != "multi_output_tree": + # old binary model is not supported. + with pytest.warns(Warning, match="Model format is default to UBJSON"): + old_from_json = from_jraw.save_raw(raw_format="deprecated") + old_from_ubj = from_ubjraw.save_raw(raw_format="deprecated") + + assert old_from_json == old_from_ubj + + rng = np.random.default_rng() + X = rng.random(size=from_jraw.num_features() * 10).reshape( + (10, from_jraw.num_features()) + ) + predt_from_jraw = from_jraw.predict(xgb.DMatrix(X)) + predt_from_bst = bst.predict(xgb.DMatrix(X)) + np.testing.assert_allclose(predt_from_jraw, predt_from_bst) + + @pytest.mark.parametrize("ext", ["json", "ubj"]) + def test_model_json_io(self, ext: str) -> None: + parameters = {"booster": "gbtree", "tree_method": "hist"} + self.run_model_json_io(parameters, ext) + parameters = { + "booster": "gbtree", + "tree_method": "hist", + "multi_strategy": "multi_output_tree", + "objective": "multi:softmax", + } + self.run_model_json_io(parameters, ext) + parameters = {"booster": "gblinear"} + self.run_model_json_io(parameters, ext) + parameters = {"booster": "dart", "tree_method": "hist"} + self.run_model_json_io(parameters, ext) + + def test_categorical_model_io(self) -> None: + X, y = tm.make_categorical(256, 16, 71, False) + Xy = xgb.DMatrix(X, y, enable_categorical=True) + booster = xgb.train({"tree_method": "approx"}, Xy, num_boost_round=16) + predt_0 = booster.predict(Xy) + + with tempfile.TemporaryDirectory() as tempdir: + path = os.path.join(tempdir, "model.deprecated") + with pytest.raises(ValueError, match=r".*JSON/UBJSON.*"): + with pytest.warns(Warning, match="Model format is default to UBJSON"): + booster.save_model(path) + + path = os.path.join(tempdir, "model.json") + booster.save_model(path) + booster = xgb.Booster(model_file=path) + predt_1 = booster.predict(Xy) + np.testing.assert_allclose(predt_0, predt_1) + + path = os.path.join(tempdir, "model.ubj") + booster.save_model(path) + booster = xgb.Booster(model_file=path) + predt_1 = booster.predict(Xy) + np.testing.assert_allclose(predt_0, predt_1) + + @pytest.mark.skipif(**tm.no_json_schema()) + def test_json_io_schema(self) -> None: + import jsonschema + + model_path = "test_json_schema.json" + path = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + doc = os.path.join(path, "doc", "model.schema") + with open(doc, "r") as fd: + schema = json.load(fd) + parameters = {"tree_method": "hist", "booster": "gbtree"} + jsonschema.validate(instance=json_model(model_path, parameters), schema=schema) + os.remove(model_path) + + parameters = {"tree_method": "hist", "booster": "dart"} + jsonschema.validate(instance=json_model(model_path, parameters), schema=schema) + os.remove(model_path) + + try: + dtrain, _ = tm.load_agaricus(__file__) + xgb.train({"objective": "foo"}, dtrain, num_boost_round=1) + except ValueError as e: + e_str = str(e) + beg = e_str.find("Objective candidate") + end = e_str.find("Stack trace") + e_str = e_str[beg:end] + e_str = e_str.strip() + splited = e_str.splitlines() + objectives = [s.split(": ")[1] for s in splited] + j_objectives = schema["properties"]["learner"]["properties"]["objective"][ + "oneOf" + ] + objectives_from_schema = set() + for j_obj in j_objectives: + objectives_from_schema.add(j_obj["properties"]["name"]["const"]) + assert set(objectives) == objectives_from_schema + + def test_model_binary_io(self) -> None: + model_path = "test_model_binary_io.deprecated" + parameters = { + "tree_method": "hist", + "booster": "gbtree", + "scale_pos_weight": "0.5", + } + X = np.random.random((10, 3)) + y = np.random.random((10,)) + dtrain = xgb.DMatrix(X, y) + bst = xgb.train(parameters, dtrain, num_boost_round=2) + with pytest.warns(Warning, match="Model format is default to UBJSON"): + bst.save_model(model_path) + bst = xgb.Booster(model_file=model_path) + os.remove(model_path) + config = json.loads(bst.save_config()) + assert ( + float(config["learner"]["objective"]["reg_loss_param"]["scale_pos_weight"]) + == 0.5 + ) + + buf = bst.save_raw() + from_raw = xgb.Booster() + from_raw.load_model(buf) + + buf_from_raw = from_raw.save_raw() + assert buf == buf_from_raw + + def test_with_pathlib(self) -> None: + """Saving and loading model files from paths.""" + save_path = Path("model.ubj") + + rng = np.random.default_rng(1994) + + data = rng.normal(size=(100, 2)) + target = np.array([0, 1] * 50) + features = ["Feature1", "Feature2"] + + dm = xgb.DMatrix(data, label=target, feature_names=features) + params = { + "objective": "binary:logistic", + "eval_metric": "logloss", + "eta": 0.3, + "max_depth": 1, + } + + bst = xgb.train(params, dm, num_boost_round=1) + + # save, assert exists + bst.save_model(save_path) + assert save_path.exists() + + def dump_assertions(dump: List[str]) -> None: + """Assertions for the expected dump from Booster""" + assert len(dump) == 1, "Exepcted only 1 tree to be dumped." + assert ( + len(dump[0].splitlines()) == 3 + ), "Expected 1 root and 2 leaves - 3 lines." + + # load the model again using Path + bst2 = xgb.Booster(model_file=save_path) + dump2 = bst2.get_dump() + dump_assertions(dump2) + + # load again using load_model + bst3 = xgb.Booster() + bst3.load_model(save_path) + dump3 = bst3.get_dump() + dump_assertions(dump3) + + # remove file + Path.unlink(save_path) + + +def save_load_model(model_path: str) -> None: + from sklearn.datasets import load_digits + from sklearn.model_selection import KFold + + rng = np.random.RandomState(1994) + + digits = load_digits(n_class=2) + y = digits["target"] + X = digits["data"] + kf = KFold(n_splits=2, shuffle=True, random_state=rng) + for train_index, test_index in kf.split(X, y): + xgb_model = xgb.XGBClassifier().fit(X[train_index], y[train_index]) + xgb_model.save_model(model_path) + + xgb_model = xgb.XGBClassifier() + xgb_model.load_model(model_path) + + assert isinstance(xgb_model.classes_, np.ndarray) + np.testing.assert_equal(xgb_model.classes_, np.array([0, 1])) + assert isinstance(xgb_model._Booster, xgb.Booster) + + preds = xgb_model.predict(X[test_index]) + labels = y[test_index] + err = sum( + 1 for i in range(len(preds)) if int(preds[i] > 0.5) != labels[i] + ) / float(len(preds)) + assert err < 0.1 + assert xgb_model.get_booster().attr("scikit_learn") is None + + # test native booster + preds = xgb_model.predict(X[test_index], output_margin=True) + booster = xgb.Booster(model_file=model_path) + predt_1 = booster.predict(xgb.DMatrix(X[test_index]), output_margin=True) + assert np.allclose(preds, predt_1) + + with pytest.raises(TypeError): + xgb_model = xgb.XGBModel() + xgb_model.load_model(model_path) + + clf = xgb.XGBClassifier(booster="gblinear", early_stopping_rounds=1) + clf.fit(X, y, eval_set=[(X, y)]) + best_iteration = clf.best_iteration + best_score = clf.best_score + predt_0 = clf.predict(X) + clf.save_model(model_path) + clf.load_model(model_path) + assert clf.booster == "gblinear" + predt_1 = clf.predict(X) + np.testing.assert_allclose(predt_0, predt_1) + assert clf.best_iteration == best_iteration + assert clf.best_score == best_score + + clfpkl = pickle.dumps(clf) + clf = pickle.loads(clfpkl) + predt_2 = clf.predict(X) + np.testing.assert_allclose(predt_0, predt_2) + assert clf.best_iteration == best_iteration + assert clf.best_score == best_score + + +@pytest.mark.skipif(**tm.no_sklearn()) +def test_sklearn_model() -> None: + from sklearn.datasets import load_digits + from sklearn.model_selection import train_test_split + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "digits.deprecated") + with pytest.warns(Warning, match="Model format is default to UBJSON"): + save_load_model(model_path) + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "digits.model.json") + save_load_model(model_path) + + with tempfile.TemporaryDirectory() as tempdir: + model_path = os.path.join(tempdir, "digits.model.ubj") + digits = load_digits(n_class=2) + y = digits["target"] + X = digits["data"] + booster = xgb.train( + {"tree_method": "hist", "objective": "binary:logistic"}, + dtrain=xgb.DMatrix(X, y), + num_boost_round=4, + ) + predt_0 = booster.predict(xgb.DMatrix(X)) + booster.save_model(model_path) + cls = xgb.XGBClassifier() + cls.load_model(model_path) + + proba = cls.predict_proba(X) + assert proba.shape[0] == X.shape[0] + assert proba.shape[1] == 2 # binary + + predt_1 = cls.predict_proba(X)[:, 1] + assert np.allclose(predt_0, predt_1) + + cls = xgb.XGBModel() + cls.load_model(model_path) + predt_1 = cls.predict(X) + assert np.allclose(predt_0, predt_1) + + # mclass + X, y = load_digits(n_class=10, return_X_y=True) + # small test_size to force early stop + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.01, random_state=1 + ) + clf = xgb.XGBClassifier( + n_estimators=64, tree_method="hist", early_stopping_rounds=2 + ) + clf.fit(X_train, y_train, eval_set=[(X_test, y_test)]) + score = clf.best_score + clf.save_model(model_path) + + clf = xgb.XGBClassifier() + clf.load_model(model_path) + assert clf.classes_.size == 10 + assert clf.objective == "multi:softprob" + + np.testing.assert_equal(clf.classes_, np.arange(10)) + assert clf.n_classes_ == 10 + + assert clf.best_iteration == 27 + assert clf.best_score == score + + +@pytest.mark.skipif(**tm.no_sklearn()) +def test_with_sklearn_obj_metric() -> None: + from sklearn.metrics import mean_squared_error + + X, y = tm.datasets.make_regression() + reg = xgb.XGBRegressor(objective=tm.ls_obj, eval_metric=mean_squared_error) + reg.fit(X, y) + + pkl = pickle.dumps(reg) + reg_1 = pickle.loads(pkl) + assert callable(reg_1.objective) + assert callable(reg_1.eval_metric) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "model.json") + reg.save_model(path) + + reg_2 = xgb.XGBRegressor() + reg_2.load_model(path) + + assert not callable(reg_2.objective) + assert not callable(reg_2.eval_metric) + assert reg_2.eval_metric is None diff --git a/tests/python/test_pickling.py b/tests/python/test_pickling.py index 083a2a7fd..2f4d77bf0 100644 --- a/tests/python/test_pickling.py +++ b/tests/python/test_pickling.py @@ -1,13 +1,10 @@ import json import os import pickle -import tempfile import numpy as np -import pytest import xgboost as xgb -from xgboost import testing as tm kRows = 100 kCols = 10 @@ -64,27 +61,3 @@ class TestPickling: params = {"nthread": 8, "tree_method": "exact", "subsample": 0.5} config = self.run_model_pickling(params) check(config) - - @pytest.mark.skipif(**tm.no_sklearn()) - def test_with_sklearn_obj_metric(self) -> None: - from sklearn.metrics import mean_squared_error - - X, y = tm.datasets.make_regression() - reg = xgb.XGBRegressor(objective=tm.ls_obj, eval_metric=mean_squared_error) - reg.fit(X, y) - - pkl = pickle.dumps(reg) - reg_1 = pickle.loads(pkl) - assert callable(reg_1.objective) - assert callable(reg_1.eval_metric) - - with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, "model.json") - reg.save_model(path) - - reg_2 = xgb.XGBRegressor() - reg_2.load_model(path) - - assert not callable(reg_2.objective) - assert not callable(reg_2.eval_metric) - assert reg_2.eval_metric is None diff --git a/tests/python/test_shap.py b/tests/python/test_shap.py index bbbdcedc0..88149c054 100644 --- a/tests/python/test_shap.py +++ b/tests/python/test_shap.py @@ -49,7 +49,7 @@ class TestSHAP: def fn(max_depth: int, num_rounds: int) -> None: # train - params = {"max_depth": max_depth, "eta": 1, "verbosity": 0} + params = {"max_depth": max_depth, "eta": 1} bst = xgb.train(params, dtrain, num_boost_round=num_rounds) # predict diff --git a/tests/python/test_updaters.py b/tests/python/test_updaters.py index 9a0cdecd2..e7641348d 100644 --- a/tests/python/test_updaters.py +++ b/tests/python/test_updaters.py @@ -117,7 +117,6 @@ class TestTreeMethod: ag_param = {'max_depth': 2, 'tree_method': 'hist', 'eta': 1, - 'verbosity': 0, 'objective': 'binary:logistic', 'eval_metric': 'auc'} hist_res = {} @@ -340,7 +339,8 @@ class TestTreeMethod: assert get_score(config_0) == get_score(config_1) - raw_booster = booster_1.save_raw(raw_format="deprecated") + with pytest.warns(Warning, match="Model format is default to UBJSON"): + raw_booster = booster_1.save_raw(raw_format="deprecated") booster_2 = xgb.Booster(model_file=raw_booster) config_2 = json.loads(booster_2.save_config()) assert get_score(config_1) == get_score(config_2) diff --git a/tests/python/test_with_pandas.py b/tests/python/test_with_pandas.py index 4dd0c640d..e53e7adcc 100644 --- a/tests/python/test_with_pandas.py +++ b/tests/python/test_with_pandas.py @@ -341,7 +341,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": "error", } @@ -372,7 +371,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": "auc", } @@ -383,7 +381,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } @@ -394,7 +391,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } @@ -413,7 +409,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", } cv = xgb.cv( @@ -424,7 +419,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", } cv = xgb.cv( @@ -435,7 +429,6 @@ class TestPandas: params = { "max_depth": 2, "eta": 1, - "verbosity": 0, "objective": "binary:logistic", "eval_metric": ["auc"], } diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index ee0085d51..47f1778d6 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -678,7 +678,6 @@ def test_split_value_histograms(): params = { "max_depth": 6, "eta": 0.01, - "verbosity": 0, "objective": "binary:logistic", "base_score": 0.5, } @@ -897,128 +896,6 @@ def test_validation_weights(): run_validation_weights(xgb.XGBClassifier) -def save_load_model(model_path): - from sklearn.datasets import load_digits - from sklearn.model_selection import KFold - - digits = load_digits(n_class=2) - y = digits['target'] - X = digits['data'] - kf = KFold(n_splits=2, shuffle=True, random_state=rng) - for train_index, test_index in kf.split(X, y): - xgb_model = xgb.XGBClassifier().fit(X[train_index], y[train_index]) - xgb_model.save_model(model_path) - - xgb_model = xgb.XGBClassifier() - xgb_model.load_model(model_path) - - assert isinstance(xgb_model.classes_, np.ndarray) - np.testing.assert_equal(xgb_model.classes_, np.array([0, 1])) - assert isinstance(xgb_model._Booster, xgb.Booster) - - preds = xgb_model.predict(X[test_index]) - labels = y[test_index] - err = sum(1 for i in range(len(preds)) - if int(preds[i] > 0.5) != labels[i]) / float(len(preds)) - assert err < 0.1 - assert xgb_model.get_booster().attr('scikit_learn') is None - - # test native booster - preds = xgb_model.predict(X[test_index], output_margin=True) - booster = xgb.Booster(model_file=model_path) - predt_1 = booster.predict(xgb.DMatrix(X[test_index]), - output_margin=True) - assert np.allclose(preds, predt_1) - - with pytest.raises(TypeError): - xgb_model = xgb.XGBModel() - xgb_model.load_model(model_path) - - clf = xgb.XGBClassifier(booster="gblinear", early_stopping_rounds=1) - clf.fit(X, y, eval_set=[(X, y)]) - best_iteration = clf.best_iteration - best_score = clf.best_score - predt_0 = clf.predict(X) - clf.save_model(model_path) - clf.load_model(model_path) - assert clf.booster == "gblinear" - predt_1 = clf.predict(X) - np.testing.assert_allclose(predt_0, predt_1) - assert clf.best_iteration == best_iteration - assert clf.best_score == best_score - - clfpkl = pickle.dumps(clf) - clf = pickle.loads(clfpkl) - predt_2 = clf.predict(X) - np.testing.assert_allclose(predt_0, predt_2) - assert clf.best_iteration == best_iteration - assert clf.best_score == best_score - - -def test_save_load_model(): - with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, "digits.model") - save_load_model(model_path) - - with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, "digits.model.json") - save_load_model(model_path) - - from sklearn.datasets import load_digits - from sklearn.model_selection import train_test_split - - with tempfile.TemporaryDirectory() as tempdir: - model_path = os.path.join(tempdir, "digits.model.ubj") - digits = load_digits(n_class=2) - y = digits["target"] - X = digits["data"] - booster = xgb.train( - {"tree_method": "hist", "objective": "binary:logistic"}, - dtrain=xgb.DMatrix(X, y), - num_boost_round=4, - ) - predt_0 = booster.predict(xgb.DMatrix(X)) - booster.save_model(model_path) - cls = xgb.XGBClassifier() - cls.load_model(model_path) - - proba = cls.predict_proba(X) - assert proba.shape[0] == X.shape[0] - assert proba.shape[1] == 2 # binary - - predt_1 = cls.predict_proba(X)[:, 1] - assert np.allclose(predt_0, predt_1) - - cls = xgb.XGBModel() - cls.load_model(model_path) - predt_1 = cls.predict(X) - assert np.allclose(predt_0, predt_1) - - # mclass - X, y = load_digits(n_class=10, return_X_y=True) - # small test_size to force early stop - X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.01, random_state=1 - ) - clf = xgb.XGBClassifier( - n_estimators=64, tree_method="hist", early_stopping_rounds=2 - ) - clf.fit(X_train, y_train, eval_set=[(X_test, y_test)]) - score = clf.best_score - clf.save_model(model_path) - - clf = xgb.XGBClassifier() - clf.load_model(model_path) - assert clf.classes_.size == 10 - assert clf.objective == "multi:softprob" - - np.testing.assert_equal(clf.classes_, np.arange(10)) - assert clf.n_classes_ == 10 - - assert clf.best_iteration == 27 - assert clf.best_score == score - - def test_RFECV(): from sklearn.datasets import load_breast_cancer, load_diabetes, load_iris from sklearn.feature_selection import RFECV From 3976455af9a2a7b72338ff805742d15b32d5fd79 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Mon, 8 Jan 2024 13:26:12 +0800 Subject: [PATCH 081/109] [jvm-packages] Use UBJ for checkpoints. (#9954) --- .../scala/spark/ExternalCheckpointManagerSuite.scala | 12 ++++++------ .../xgboost4j/java/ExternalCheckpointManager.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala index e6835158d..729bd9c77 100755 --- a/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala +++ b/jvm-packages/xgboost4j-spark/src/test/scala/ml/dmlc/xgboost4j/scala/spark/ExternalCheckpointManagerSuite.scala @@ -50,13 +50,13 @@ class ExternalCheckpointManagerSuite extends AnyFunSuite with TmpFolderPerSuite manager.updateCheckpoint(model2._booster.booster) var files = FileSystem.get(sc.hadoopConfiguration).listStatus(new Path(tmpPath)) assert(files.length == 1) - assert(files.head.getPath.getName == "1.model") + assert(files.head.getPath.getName == "1.ubj") assert(manager.loadCheckpointAsScalaBooster().getNumBoostedRound == 2) manager.updateCheckpoint(model4._booster) files = FileSystem.get(sc.hadoopConfiguration).listStatus(new Path(tmpPath)) assert(files.length == 1) - assert(files.head.getPath.getName == "3.model") + assert(files.head.getPath.getName == "3.ubj") assert(manager.loadCheckpointAsScalaBooster().getNumBoostedRound == 4) } @@ -66,10 +66,10 @@ class ExternalCheckpointManagerSuite extends AnyFunSuite with TmpFolderPerSuite val manager = new ExternalCheckpointManager(tmpPath, FileSystem.get(sc.hadoopConfiguration)) manager.updateCheckpoint(model4._booster) manager.cleanUpHigherVersions(3) - assert(new File(s"$tmpPath/3.model").exists()) + assert(new File(s"$tmpPath/3.ubj").exists()) manager.cleanUpHigherVersions(2) - assert(!new File(s"$tmpPath/3.model").exists()) + assert(!new File(s"$tmpPath/3.ubj").exists()) } test("test checkpoint rounds") { @@ -105,8 +105,8 @@ class ExternalCheckpointManagerSuite extends AnyFunSuite with TmpFolderPerSuite // Check only one model is kept after training val files = FileSystem.get(sc.hadoopConfiguration).listStatus(new Path(tmpPath)) assert(files.length == 1) - assert(files.head.getPath.getName == "4.model") - val tmpModel = SXGBoost.loadModel(s"$tmpPath/4.model") + assert(files.head.getPath.getName == "4.ubj") + val tmpModel = SXGBoost.loadModel(s"$tmpPath/4.ubj") // Train next model based on prev model val nextModel = new XGBoostClassifier(paramMap ++ Seq("num_round" -> 8)).fit(training) assert(error(tmpModel) >= error(prevModel._booster)) diff --git a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java index 3d794756d..d5b8e8b9c 100644 --- a/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java +++ b/jvm-packages/xgboost4j/src/main/java/ml/dmlc/xgboost4j/java/ExternalCheckpointManager.java @@ -29,7 +29,7 @@ import org.apache.hadoop.fs.Path; public class ExternalCheckpointManager { private Log logger = LogFactory.getLog("ExternalCheckpointManager"); - private String modelSuffix = ".model"; + private String modelSuffix = ".ubj"; private Path checkpointPath; // directory for checkpoints private FileSystem fs; From 3ff3a5f1ed35503feeaec4ae3f159746159623e6 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Mon, 8 Jan 2024 17:30:49 +0800 Subject: [PATCH 082/109] [jvm-packages] support jdk 17 for test (#9959) --- jvm-packages/pom.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index de778a995..1acff2240 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -48,6 +48,22 @@ cuda11 3.2.17 2.11.0 + + + + -XX:+IgnoreUnrecognizedVMOptions + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + --add-opens=java.base/java.nio=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED + --add-opens=java.base/sun.nio.cs=ALL-UNNAMED + --add-opens=java.base/sun.security.action=ALL-UNNAMED + --add-opens=java.base/sun.util.calendar=ALL-UNNAMED + @@ -337,6 +353,9 @@ org.scalatest scalatest-maven-plugin 2.2.0 + + -ea -Xmx4g -Xss4m ${extraJavaTestArgs} + test From 9a30bdd3137a23669b9df9851cf7bd6230cf846d Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Mon, 8 Jan 2024 19:26:24 +0800 Subject: [PATCH 083/109] Test loading models with invalid file extensions. (#9955) --- doc/tutorials/saving_model.rst | 30 ++++++++++------ tests/python/test_model_io.py | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/doc/tutorials/saving_model.rst b/doc/tutorials/saving_model.rst index 34b5430df..326834cd4 100644 --- a/doc/tutorials/saving_model.rst +++ b/doc/tutorials/saving_model.rst @@ -2,14 +2,20 @@ Introduction to Model IO ######################## +Since 2.1.0, the default model format for XGBoost is the UBJSON format, the option is +enabled for serializing models to file, serializing models to buffer, and for memory +snapshot (pickle and alike). + In XGBoost 1.0.0, we introduced support of using `JSON `_ for saving/loading XGBoost models and related hyper-parameters for training, aiming to replace the old binary internal format with an open format that can be easily reused. Later in XGBoost 1.6.0, additional support for `Universal Binary JSON `__ is added as an optimization for more -efficient model IO. They have the same document structure with different representations, -and we will refer them collectively as the JSON format. This tutorial aims to share some -basic insights into the JSON serialisation method used in XGBoost. Without explicitly +efficient model IO, which is set to default in 2.1. + +JSON and UBJSON have the same document structure with different representations, and we +will refer them collectively as the JSON format. This tutorial aims to share some basic +insights into the JSON serialisation method used in XGBoost. Without explicitly mentioned, the following sections assume you are using the one of the 2 outputs formats, which can be enabled by providing the file name with ``.json`` (or ``.ubj`` for binary JSON) as file extension when saving/loading model: ``booster.save_model('model.json')``. @@ -25,12 +31,13 @@ If you come from Deep Learning community, then it should be clear to you that there are differences between the neural network structures composed of weights with fixed tensor operations, and the optimizers (like RMSprop) used to train them. -So when one calls ``booster.save_model`` (``xgb.save`` in R), XGBoost saves the trees, some model -parameters like number of input columns in trained trees, and the objective function, which combined -to represent the concept of "model" in XGBoost. As for why are we saving the objective as -part of model, that's because objective controls transformation of global bias (called -``base_score`` in XGBoost). Users can share this model with others for prediction, -evaluation or continue the training with a different set of hyper-parameters etc. +So when one calls ``booster.save_model`` (``xgb.save`` in R), XGBoost saves the trees, +some model parameters like number of input columns in trained trees, and the objective +function, which combined to represent the concept of "model" in XGBoost. As for why are +we saving the objective as part of model, that's because objective controls transformation +of global bias (called ``base_score`` in XGBoost) and task-specific information. Users +can share this model with others for prediction, evaluation or continue the training with +a different set of hyper-parameters etc. However, this is not the end of story. There are cases where we need to save something more than just the model itself. For example, in distributed training, XGBoost performs @@ -81,7 +88,10 @@ a filename with ``.json`` or ``.ubj`` as file extension, the latter is the exten JSON files that were produced by an external source may lead to undefined behaviors and crashes. -While for memory snapshot, UBJSON is the default starting with xgboost 1.6. +While for memory snapshot, UBJSON is the default starting with xgboost 1.6. When loading +the model back, XGBoost recognizes the file extensions ``.json`` and ``.ubj``, and can +dispatch accordingly. If the extension is not specified, XGBoost tries to guess the right +one. *************************************************************** A note on backward compatibility of models and memory snapshots diff --git a/tests/python/test_model_io.py b/tests/python/test_model_io.py index dc843a7f4..884bba08f 100644 --- a/tests/python/test_model_io.py +++ b/tests/python/test_model_io.py @@ -254,6 +254,68 @@ class TestBoosterIO: # remove file Path.unlink(save_path) + def test_invalid_postfix(self) -> None: + """Test mis-specified model format, no special hanlding is expected, the + JSON/UBJ parser can emit parsing errors. + + """ + X, y, w = tm.make_regression(64, 16, False) + booster = xgb.train({}, xgb.QuantileDMatrix(X, y, weight=w), num_boost_round=3) + + def rename(src: str, dst: str) -> None: + if os.path.exists(dst): + # Windows cannot overwrite an existing file. + os.remove(dst) + os.rename(src, dst) + + with tempfile.TemporaryDirectory() as tmpdir: + path_dep = os.path.join(tmpdir, "model.deprecated") + # save into deprecated format + with pytest.warns(UserWarning, match="UBJSON"): + booster.save_model(path_dep) + + path_ubj = os.path.join(tmpdir, "model.ubj") + rename(path_dep, path_ubj) + + with pytest.raises(ValueError, match="{"): + xgb.Booster(model_file=path_ubj) + + path_json = os.path.join(tmpdir, "model.json") + rename(path_ubj, path_json) + + with pytest.raises(ValueError, match="{"): + xgb.Booster(model_file=path_json) + + # save into ubj format + booster.save_model(path_ubj) + rename(path_ubj, path_dep) + # deprecated is not a recognized format internally, XGBoost can guess the + # right format + xgb.Booster(model_file=path_dep) + rename(path_dep, path_json) + with pytest.raises(ValueError, match="Expecting"): + xgb.Booster(model_file=path_json) + + # save into JSON format + booster.save_model(path_json) + rename(path_json, path_dep) + # deprecated is not a recognized format internally, XGBoost can guess the + # right format + xgb.Booster(model_file=path_dep) + rename(path_dep, path_ubj) + with pytest.raises(ValueError, match="Expecting"): + xgb.Booster(model_file=path_ubj) + + # save model without file extension + path_no = os.path.join(tmpdir, "model") + with pytest.warns(UserWarning, match="UBJSON"): + booster.save_model(path_no) + + booster_1 = xgb.Booster(model_file=path_no) + r0 = booster.save_raw(raw_format="json") + r1 = booster_1.save_raw(raw_format="json") + assert r0 == r1 + def save_load_model(model_path: str) -> None: from sklearn.datasets import load_digits From fa5e2f6c45175ea1ab197852012af2a6a6f11da3 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 00:54:23 +0800 Subject: [PATCH 084/109] Synthesize the AMES housing dataset for tests. (#9963) --- python-package/xgboost/testing/data.py | 210 +++++++++++++++++++++---- 1 file changed, 178 insertions(+), 32 deletions(-) diff --git a/python-package/xgboost/testing/data.py b/python-package/xgboost/testing/data.py index c0d53f914..0c4f29008 100644 --- a/python-package/xgboost/testing/data.py +++ b/python-package/xgboost/testing/data.py @@ -4,10 +4,11 @@ import os import zipfile from dataclasses import dataclass from typing import ( + TYPE_CHECKING, Any, Callable, + Dict, Generator, - List, NamedTuple, Optional, Tuple, @@ -25,6 +26,11 @@ from scipy import sparse import xgboost from xgboost.data import pandas_pyarrow_mapper +if TYPE_CHECKING: + from ..compat import DataFrame as DataFrameT +else: + DataFrameT = Any + joblib = pytest.importorskip("joblib") memory = joblib.Memory("./cachedir", verbose=0) @@ -256,46 +262,186 @@ def get_sparse() -> Tuple[np.ndarray, np.ndarray]: return X, y +# pylint: disable=too-many-statements @memory.cache -def get_ames_housing() -> Tuple[np.ndarray, np.ndarray]: - """ +def get_ames_housing() -> Tuple[DataFrameT, np.ndarray]: + """Get a synthetic version of the amse housing dataset. + + The real one can be obtained via: + + .. code-block:: + + from sklearn import datasets + + datasets.fetch_openml(data_id=42165, as_frame=True, return_X_y=True) + Number of samples: 1460 Number of features: 20 Number of categorical features: 10 Number of numerical features: 10 """ - datasets = pytest.importorskip("sklearn.datasets") - X, y = datasets.fetch_openml(data_id=42165, as_frame=True, return_X_y=True) + pytest.importorskip("pandas") + import pandas as pd - categorical_columns_subset: List[str] = [ - "BldgType", # 5 cats, no nan - "GarageFinish", # 3 cats, nan - "LotConfig", # 5 cats, no nan - "Functional", # 7 cats, no nan - "MasVnrType", # 4 cats, nan - "HouseStyle", # 8 cats, no nan - "FireplaceQu", # 5 cats, nan - "ExterCond", # 5 cats, no nan - "ExterQual", # 4 cats, no nan - "PoolQC", # 3 cats, nan - ] + rng = np.random.default_rng(1994) + n_samples = 1460 + df = pd.DataFrame() - numerical_columns_subset: List[str] = [ - "3SsnPorch", - "Fireplaces", - "BsmtHalfBath", - "HalfBath", - "GarageCars", - "TotRmsAbvGrd", - "BsmtFinSF1", - "BsmtFinSF2", - "GrLivArea", - "ScreenPorch", - ] + def synth_cat( + name_proba: Dict[Union[str, float], float], density: float + ) -> pd.Series: + n_nulls = int(n_samples * (1 - density)) + has_nan = np.abs(1.0 - density) > 1e-6 and n_nulls > 0 + if has_nan: + sparsity = 1.0 - density + name_proba[np.nan] = sparsity - X = X[categorical_columns_subset + numerical_columns_subset] - X[categorical_columns_subset] = X[categorical_columns_subset].astype("category") - return X, y + keys = list(name_proba.keys()) + p = list(name_proba.values()) + p[-1] += 1.0 - np.sum(p) # Fix floating point error + x = rng.choice(keys, size=n_samples, p=p) + + series = pd.Series( + x, + dtype=pd.CategoricalDtype( + # not NA + filter(lambda x: isinstance(x, str), keys) + ), + ) + return series + + df["BldgType"] = synth_cat( + { + "1Fam": 0.835616, + "2fmCon": 0.078082, + "Duplex": 0.035616, + "Twnhs": 0.029452, + "TwnhsE": 0.021233, + }, + 1.0, + ) + df["GarageFinish"] = synth_cat( + {"Unf": 0.414384, "RFn": 0.289041, "Fin": 0.241096}, 0.94452 + ) + df["LotConfig"] = synth_cat( + { + "Corner": 0.180137, + "CulDSac": 0.064384, + "FR2": 0.032192, + "FR3": 0.002740, + }, + 1.0, + ) + df["Functional"] = synth_cat( + { + "Typ": 0.931506, + "Min2": 0.023287, + "Min1": 0.021232, + "Mod": 0.010273, + "Maj1": 0.009589, + "Maj2": 0.003424, + "Sev": 0.000684, + }, + 1.0, + ) + df["MasVnrType"] = synth_cat( + { + "None": 0.591780, + "BrkFace": 0.304794, + "Stone": 0.087671, + "BrkCmn": 0.010273, + }, + 0.99452, + ) + df["HouseStyle"] = synth_cat( + { + "1Story": 0.497260, + "2Story": 0.304794, + "1.5Fin": 0.105479, + "SLvl": 0.044520, + "SFoyer": 0.025342, + "1.5Unf": 0.009589, + "2.5Unf": 0.007534, + "2.5Fin": 0.005479, + }, + 1.0, + ) + df["FireplaceQu"] = synth_cat( + { + "Gd": 0.260273, + "TA": 0.214383, + "Fa": 0.022602, + "Ex": 0.016438, + "Po": 0.013698, + }, + 0.527397, + ) + df["ExterCond"] = synth_cat( + { + "TA": 0.878082, + "Gd": 0.1, + "Fa": 0.019178, + "Ex": 0.002054, + "Po": 0.000684, + }, + 1.0, + ) + df["ExterQual"] = synth_cat( + { + "TA": 0.620547, + "Gd": 0.334246, + "Ex": 0.035616, + "Fa": 0.009589, + }, + 1.0, + ) + df["PoolQC"] = synth_cat( + { + "Gd": 0.002054, + "Ex": 0.001369, + "Fa": 0.001369, + }, + 0.004794, + ) + + # We focus on the cateogircal values here, for numerical features, simple normal + # distribution is used, which doesn't match the original data. + def synth_num(loc: float, std: float, density: float) -> pd.Series: + x = rng.normal(loc=loc, scale=std, size=n_samples) + n_nulls = int(n_samples * (1 - density)) + if np.abs(1.0 - density) > 1e-6 and n_nulls > 0: + null_idx = rng.choice(n_samples, size=n_nulls, replace=False) + x[null_idx] = np.nan + return pd.Series(x, dtype=np.float64) + + df["3SsnPorch"] = synth_num(3.4095890410958902, 29.31733055678188, 1.0) + df["Fireplaces"] = synth_num(0.613013698630137, 0.6446663863122295, 1.0) + df["BsmtHalfBath"] = synth_num(0.057534246575342465, 0.23875264627921178, 1.0) + df["HalfBath"] = synth_num(0.38287671232876713, 0.5028853810928914, 1.0) + df["GarageCars"] = synth_num(1.7671232876712328, 0.7473150101111095, 1.0) + df["TotRmsAbvGrd"] = synth_num(6.517808219178082, 1.6253932905840505, 1.0) + df["BsmtFinSF1"] = synth_num(443.6397260273973, 456.0980908409277, 1.0) + df["BsmtFinSF2"] = synth_num(46.54931506849315, 161.31927280654173, 1.0) + df["GrLivArea"] = synth_num(1515.463698630137, 525.4803834232025, 1.0) + df["ScreenPorch"] = synth_num(15.060958904109588, 55.757415281874174, 1.0) + + columns = list(df.columns) + rng.shuffle(columns) + df = df[columns] + + # linear interaction for testing purposes. + y = np.zeros(shape=(n_samples,)) + for c in df.columns: + if isinstance(df[c].dtype, pd.CategoricalDtype): + y += df[c].cat.codes.astype(np.float64) + else: + y += df[c].values + + # Shift and scale to match the original y. + y *= 79442.50288288662 / y.std() + y += 180921.19589041095 - y.mean() + + return df, y @memory.cache From b3eb5d09452fccc26f95684a1786d91a5f1aeb8d Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 03:22:15 +0800 Subject: [PATCH 085/109] Use UBJ in Python checkpoint. (#9958) --- demo/guide-python/callbacks.py | 33 ++++++++++++------ doc/python/python_api.rst | 4 --- python-package/xgboost/callback.py | 34 ++++++++++++++++--- python-package/xgboost/core.py | 29 ++++++++-------- tests/ci_build/lint_python.py | 3 ++ tests/python/test_callback.py | 17 ++++++---- .../test_with_dask/test_with_dask.py | 30 ++++++++++++---- 7 files changed, 104 insertions(+), 46 deletions(-) diff --git a/demo/guide-python/callbacks.py b/demo/guide-python/callbacks.py index 9c12f70de..0676c732e 100644 --- a/demo/guide-python/callbacks.py +++ b/demo/guide-python/callbacks.py @@ -7,6 +7,7 @@ Demo for using and defining callback functions import argparse import os import tempfile +from typing import Dict import numpy as np from matplotlib import pyplot as plt @@ -17,24 +18,26 @@ import xgboost as xgb class Plotting(xgb.callback.TrainingCallback): - """Plot evaluation result during training. Only for demonstration purpose as it's quite - slow to draw. + """Plot evaluation result during training. Only for demonstration purpose as it's + quite slow to draw using matplotlib. """ - def __init__(self, rounds): + def __init__(self, rounds: int) -> None: self.fig = plt.figure() self.ax = self.fig.add_subplot(111) self.rounds = rounds - self.lines = {} + self.lines: Dict[str, plt.Line2D] = {} self.fig.show() self.x = np.linspace(0, self.rounds, self.rounds) plt.ion() - def _get_key(self, data, metric): + def _get_key(self, data: str, metric: str) -> str: return f"{data}-{metric}" - def after_iteration(self, model, epoch, evals_log): + def after_iteration( + self, model: xgb.Booster, epoch: int, evals_log: Dict[str, dict] + ) -> bool: """Update the plot.""" if not self.lines: for data, metric in evals_log.items(): @@ -55,7 +58,7 @@ class Plotting(xgb.callback.TrainingCallback): return False -def custom_callback(): +def custom_callback() -> None: """Demo for defining a custom callback function that plots evaluation result during training.""" X, y = load_breast_cancer(return_X_y=True) @@ -82,19 +85,27 @@ def custom_callback(): ) -def check_point_callback(): - # only for demo, set a larger value (like 100) in practice as checkpointing is quite +def check_point_callback() -> None: + """Demo for using the checkpoint callback. Custom logic for handling output is + usually required and users are encouraged to define their own callback for + checkpointing operations. The builtin one can be used as a starting point. + + """ + # Only for demo, set a larger value (like 100) in practice as checkpointing is quite # slow. rounds = 2 - def check(as_pickle): + def check(as_pickle: bool) -> None: for i in range(0, 10, rounds): if i == 0: continue if as_pickle: path = os.path.join(tmpdir, "model_" + str(i) + ".pkl") else: - path = os.path.join(tmpdir, "model_" + str(i) + ".json") + path = os.path.join( + tmpdir, + f"model_{i}.{xgb.callback.TrainingCheckPoint.default_format}", + ) assert os.path.exists(path) X, y = load_breast_cancer(return_X_y=True) diff --git a/doc/python/python_api.rst b/doc/python/python_api.rst index 38b22a994..4ba520fe4 100644 --- a/doc/python/python_api.rst +++ b/doc/python/python_api.rst @@ -88,22 +88,18 @@ Callback API .. autoclass:: xgboost.callback.EvaluationMonitor :members: - :inherited-members: :show-inheritance: .. autoclass:: xgboost.callback.EarlyStopping :members: - :inherited-members: :show-inheritance: .. autoclass:: xgboost.callback.LearningRateScheduler :members: - :inherited-members: :show-inheritance: .. autoclass:: xgboost.callback.TrainingCheckPoint :members: - :inherited-members: :show-inheritance: .. _dask_api: diff --git a/python-package/xgboost/callback.py b/python-package/xgboost/callback.py index 29d880539..b8d5a751e 100644 --- a/python-package/xgboost/callback.py +++ b/python-package/xgboost/callback.py @@ -62,11 +62,31 @@ class TrainingCallback(ABC): return model def before_iteration(self, model: _Model, epoch: int, evals_log: EvalsLog) -> bool: - """Run before each iteration. Return True when training should stop.""" + """Run before each iteration. Returns True when training should stop. See + :py:meth:`after_iteration` for details. + + """ return False def after_iteration(self, model: _Model, epoch: int, evals_log: EvalsLog) -> bool: - """Run after each iteration. Return True when training should stop.""" + """Run after each iteration. Returns `True` when training should stop. + + Parameters + ---------- + + model : + Eeither a :py:class:`~xgboost.Booster` object or a CVPack if the cv function + in xgboost is being used. + epoch : + The current training iteration. + evals_log : + A dictionary containing the evaluation history: + + .. code-block:: python + + {"data_name": {"metric_name": [0.5, ...]}} + + """ return False @@ -547,14 +567,16 @@ class TrainingCheckPoint(TrainingCallback): .. versionadded:: 1.3.0 + Since XGBoost 2.1.0, the default format is changed to UBJSON. + Parameters ---------- directory : Output model directory. name : - pattern of output model file. Models will be saved as name_0.json, name_1.json, - name_2.json .... + pattern of output model file. Models will be saved as name_0.ubj, name_1.ubj, + name_2.ubj .... as_pickle : When set to True, all training parameters will be saved in pickle format, instead of saving only the model. @@ -564,6 +586,8 @@ class TrainingCheckPoint(TrainingCallback): """ + default_format = "ubj" + def __init__( self, directory: Union[str, os.PathLike], @@ -592,7 +616,7 @@ class TrainingCheckPoint(TrainingCallback): self._name + "_" + (str(epoch + self._start)) - + (".pkl" if self._as_pickle else ".json"), + + (".pkl" if self._as_pickle else f".{self.default_format}"), ) self._epoch = 0 # reset counter if collective.get_rank() == 0: diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index d554a8d3a..4d093293d 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -2591,9 +2591,8 @@ class Booster: The model is saved in an XGBoost internal format which is universal among the various XGBoost interfaces. Auxiliary attributes of the Python Booster object - (such as feature_names) will not be saved when using binary format. To save - those attributes, use JSON/UBJ instead. See :doc:`Model IO - ` for more info. + (such as feature_names) are only saved when using JSON or UBJSON (default) + format. See :doc:`Model IO ` for more info. .. code-block:: python @@ -2616,12 +2615,15 @@ class Booster: def save_raw(self, raw_format: str = "ubj") -> bytearray: """Save the model to a in memory buffer representation instead of file. + The model is saved in an XGBoost internal format which is universal among the + various XGBoost interfaces. Auxiliary attributes of the Python Booster object + (such as feature_names) are only saved when using JSON or UBJSON (default) + format. See :doc:`Model IO ` for more info. + Parameters ---------- raw_format : - Format of output buffer. Can be `json`, `ubj` or `deprecated`. Right now - the default is `deprecated` but it will be changed to `ubj` (univeral binary - json) in the future. + Format of output buffer. Can be `json`, `ubj` or `deprecated`. Returns ------- @@ -2640,11 +2642,10 @@ class Booster: def load_model(self, fname: ModelIn) -> None: """Load the model from a file or a bytearray. - The model is loaded from XGBoost format which is universal among the various - XGBoost interfaces. Auxiliary attributes of the Python Booster object (such as - feature_names) will not be loaded when using binary format. To save those - attributes, use JSON/UBJ instead. See :doc:`Model IO ` - for more info. + The model is saved in an XGBoost internal format which is universal among the + various XGBoost interfaces. Auxiliary attributes of the Python Booster object + (such as feature_names) are only saved when using JSON or UBJSON (default) + format. See :doc:`Model IO ` for more info. .. code-block:: python @@ -2769,9 +2770,9 @@ class Booster: with_stats: bool = False, dump_format: str = "text", ) -> List[str]: - """Returns the model dump as a list of strings. Unlike :py:meth:`save_model`, the output - format is primarily used for visualization or interpretation, hence it's more - human readable but cannot be loaded back to XGBoost. + """Returns the model dump as a list of strings. Unlike :py:meth:`save_model`, + the output format is primarily used for visualization or interpretation, hence + it's more human readable but cannot be loaded back to XGBoost. Parameters ---------- diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index 1e414f3b5..87d76607f 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -31,6 +31,8 @@ class LintersPaths: "tests/python/test_with_pandas.py", "tests/python-gpu/", "tests/python-sycl/", + "tests/test_distributed/test_with_dask/", + "tests/test_distributed/test_gpu_with_dask/", "tests/test_distributed/test_with_spark/", "tests/test_distributed/test_gpu_with_spark/", # demo @@ -91,6 +93,7 @@ class LintersPaths: # demo "demo/json-model/json_parser.py", "demo/guide-python/external_memory.py", + "demo/guide-python/callbacks.py", "demo/guide-python/cat_in_the_dat.py", "demo/guide-python/categorical.py", "demo/guide-python/cat_pipeline.py", diff --git a/tests/python/test_callback.py b/tests/python/test_callback.py index 4893ad074..3a7501e48 100644 --- a/tests/python/test_callback.py +++ b/tests/python/test_callback.py @@ -244,7 +244,7 @@ class TestCallbacks: assert booster.num_boosted_rounds() == booster.best_iteration + 1 with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, 'model.json') + path = os.path.join(tmpdir, "model.json") cls.save_model(path) cls = xgb.XGBClassifier() cls.load_model(path) @@ -378,7 +378,7 @@ class TestCallbacks: scheduler = xgb.callback.LearningRateScheduler dtrain, dtest = tm.load_agaricus(__file__) - watchlist = [(dtest, 'eval'), (dtrain, 'train')] + watchlist = [(dtest, "eval"), (dtrain, "train")] param = { "max_depth": 2, @@ -429,7 +429,7 @@ class TestCallbacks: assert tree_3th_0["split_conditions"] != tree_3th_1["split_conditions"] @pytest.mark.parametrize("tree_method", ["hist", "approx", "approx"]) - def test_eta_decay(self, tree_method): + def test_eta_decay(self, tree_method: str) -> None: self.run_eta_decay(tree_method) @pytest.mark.parametrize( @@ -446,7 +446,7 @@ class TestCallbacks: def test_eta_decay_leaf_output(self, tree_method: str, objective: str) -> None: self.run_eta_decay_leaf_output(tree_method, objective) - def test_check_point(self): + def test_check_point(self) -> None: from sklearn.datasets import load_breast_cancer X, y = load_breast_cancer(return_X_y=True) @@ -463,7 +463,12 @@ class TestCallbacks: callbacks=[check_point], ) for i in range(1, 10): - assert os.path.exists(os.path.join(tmpdir, "model_" + str(i) + ".json")) + assert os.path.exists( + os.path.join( + tmpdir, + f"model_{i}.{xgb.callback.TrainingCheckPoint.default_format}", + ) + ) check_point = xgb.callback.TrainingCheckPoint( directory=tmpdir, interval=1, as_pickle=True, name="model" @@ -478,7 +483,7 @@ class TestCallbacks: for i in range(1, 10): assert os.path.exists(os.path.join(tmpdir, "model_" + str(i) + ".pkl")) - def test_callback_list(self): + def test_callback_list(self) -> None: X, y = tm.data.get_california_housing() m = xgb.DMatrix(X, y) callbacks = [xgb.callback.EarlyStopping(rounds=10)] diff --git a/tests/test_distributed/test_with_dask/test_with_dask.py b/tests/test_distributed/test_with_dask/test_with_dask.py index d380f0dee..79df025fe 100644 --- a/tests/test_distributed/test_with_dask/test_with_dask.py +++ b/tests/test_distributed/test_with_dask/test_with_dask.py @@ -1590,7 +1590,7 @@ class TestWithDask: @given( params=hist_parameter_strategy, cache_param=hist_cache_strategy, - dataset=tm.make_dataset_strategy() + dataset=tm.make_dataset_strategy(), ) @settings( deadline=None, max_examples=10, suppress_health_check=suppress, print_blob=True @@ -2250,16 +2250,27 @@ class TestDaskCallbacks: ], ) for i in range(1, 10): - assert os.path.exists(os.path.join(tmpdir, "model_" + str(i) + ".json")) + assert os.path.exists( + os.path.join( + tmpdir, + f"model_{i}.{xgb.callback.TrainingCheckPoint.default_format}", + ) + ) -@gen_cluster(client=True, clean_kwargs={"processes": False, "threads": False}, allow_unclosed=True) +@gen_cluster( + client=True, + clean_kwargs={"processes": False, "threads": False}, + allow_unclosed=True, +) async def test_worker_left(c, s, a, b): async with Worker(s.address): dx = da.random.random((1000, 10)).rechunk(chunks=(10, None)) dy = da.random.random((1000,)).rechunk(chunks=(10,)) d_train = await xgb.dask.DaskDMatrix( - c, dx, dy, + c, + dx, + dy, ) await async_poll_for(lambda: len(s.workers) == 2, timeout=5) with pytest.raises(RuntimeError, match="Missing"): @@ -2271,12 +2282,19 @@ async def test_worker_left(c, s, a, b): ) -@gen_cluster(client=True, Worker=Nanny, clean_kwargs={"processes": False, "threads": False}, allow_unclosed=True) +@gen_cluster( + client=True, + Worker=Nanny, + clean_kwargs={"processes": False, "threads": False}, + allow_unclosed=True, +) async def test_worker_restarted(c, s, a, b): dx = da.random.random((1000, 10)).rechunk(chunks=(10, None)) dy = da.random.random((1000,)).rechunk(chunks=(10,)) d_train = await xgb.dask.DaskDMatrix( - c, dx, dy, + c, + dx, + dy, ) await c.restart_workers([a.worker_address]) with pytest.raises(RuntimeError, match="Missing"): From 7ff6d44efaf5dfe09320f2a67a1c8ae99370c62b Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 8 Jan 2024 20:43:21 +0100 Subject: [PATCH 086/109] [R] Use R's error stream for printing warnings (#9965) --- R-package/src/xgboost_custom.cc | 6 +++++- R-package/tests/testthat/test_basic.R | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/R-package/src/xgboost_custom.cc b/R-package/src/xgboost_custom.cc index f196297ec..6aaa3696a 100644 --- a/R-package/src/xgboost_custom.cc +++ b/R-package/src/xgboost_custom.cc @@ -17,7 +17,11 @@ namespace xgboost { ConsoleLogger::~ConsoleLogger() { if (cur_verbosity_ == LogVerbosity::kIgnore || cur_verbosity_ <= GlobalVerbosity()) { - dmlc::CustomLogMessage::Log(log_stream_.str()); + if (cur_verbosity_ == LogVerbosity::kWarning) { + REprintf("%s\n", log_stream_.str().c_str()); + } else { + dmlc::CustomLogMessage::Log(log_stream_.str()); + } } } TrackerLogger::~TrackerLogger() { diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index d4b3a6be3..1f6cebbca 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -83,7 +83,8 @@ test_that("parameter validation works", { bar = "foo" ) output <- capture.output( - xgb.train(params = params, data = dtrain, nrounds = nrounds) + xgb.train(params = params, data = dtrain, nrounds = nrounds), + type = "message" ) print(output) } From bed0349954d6acc77de88905e188b744019b2d33 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 8 Jan 2024 20:43:48 +0100 Subject: [PATCH 087/109] [R] Don't write files to user's directory (#9966) --- R-package/R/utils.R | 17 ++++----- R-package/R/xgb.Booster.R | 12 +++---- R-package/R/xgb.DMatrix.R | 6 ++-- R-package/R/xgb.DMatrix.save.R | 6 ++-- R-package/R/xgb.load.R | 6 ++-- R-package/R/xgb.save.R | 6 ++-- R-package/inst/make-r-def.R | 2 +- .../a-compatibility-note-for-saveRDS-save.Rd | 17 ++++----- R-package/man/xgb.Booster.complete.Rd | 6 ++-- R-package/man/xgb.DMatrix.Rd | 6 ++-- R-package/man/xgb.DMatrix.save.Rd | 6 ++-- R-package/man/xgb.attr.Rd | 6 ++-- R-package/man/xgb.load.Rd | 6 ++-- R-package/man/xgb.save.Rd | 6 ++-- R-package/tests/testthat/test_basic.R | 8 ++--- R-package/tests/testthat/test_callbacks.R | 24 ++++++------- R-package/tests/testthat/test_dmatrix.R | 36 ++++++++++--------- R-package/tests/testthat/test_helpers.R | 18 +++++----- R-package/vignettes/xgboostPresentation.Rmd | 14 ++++---- 19 files changed, 108 insertions(+), 100 deletions(-) diff --git a/R-package/R/utils.R b/R-package/R/utils.R index 1798e4ad1..c011ab8ed 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -388,13 +388,14 @@ NULL #' objective = "binary:logistic") #' #' # Save as a stand-alone file; load it with xgb.load() -#' xgb.save(bst, 'xgb.model') -#' bst2 <- xgb.load('xgb.model') +#' fname <- file.path(tempdir(), "xgb_model.ubj") +#' xgb.save(bst, fname) +#' bst2 <- xgb.load(fname) #' #' # Save as a stand-alone file (JSON); load it with xgb.load() -#' xgb.save(bst, 'xgb.model.json') -#' bst2 <- xgb.load('xgb.model.json') -#' if (file.exists('xgb.model.json')) file.remove('xgb.model.json') +#' fname <- file.path(tempdir(), "xgb_model.json") +#' xgb.save(bst, fname) +#' bst2 <- xgb.load(fname) #' #' # Save as a raw byte vector; load it with xgb.load.raw() #' xgb_bytes <- xgb.save.raw(bst) @@ -405,12 +406,12 @@ NULL #' # Persist the R object. Here, saveRDS() is okay, since it doesn't persist #' # xgb.Booster directly. What's being persisted is the future-proof byte representation #' # as given by xgb.save.raw(). -#' saveRDS(obj, 'my_object.rds') +#' fname <- file.path(tempdir(), "my_object.Rds") +#' saveRDS(obj, fname) #' # Read back the R object -#' obj2 <- readRDS('my_object.rds') +#' obj2 <- readRDS(fname) #' # Re-construct xgb.Booster object from the bytes #' bst2 <- xgb.load.raw(obj2$xgb_model_bytes) -#' if (file.exists('my_object.rds')) file.remove('my_object.rds') #' #' @name a-compatibility-note-for-saveRDS-save NULL diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 371f3d129..9fdfc9dd6 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -118,12 +118,12 @@ xgb.get.handle <- function(object) { #' objective = "binary:logistic" #' ) #' -#' saveRDS(bst, "xgb.model.rds") +#' fname <- file.path(tempdir(), "xgb_model.Rds") +#' saveRDS(bst, fname) #' #' # Warning: The resulting RDS file is only compatible with the current XGBoost version. #' # Refer to the section titled "a-compatibility-note-for-saveRDS-save". -#' bst1 <- readRDS("xgb.model.rds") -#' if (file.exists("xgb.model.rds")) file.remove("xgb.model.rds") +#' bst1 <- readRDS(fname) #' # the handle is invalid: #' print(bst1$handle) #' @@ -580,9 +580,9 @@ predict.xgb.Booster.handle <- function(object, ...) { #' print(xgb.attr(bst, "my_attribute")) #' xgb.attributes(bst) <- list(a = 123, b = "abc") #' -#' xgb.save(bst, "xgb.model") -#' bst1 <- xgb.load("xgb.model") -#' if (file.exists("xgb.model")) file.remove("xgb.model") +#' fname <- file.path(tempdir(), "xgb.ubj") +#' xgb.save(bst, fname) +#' bst1 <- xgb.load(fname) #' print(xgb.attr(bst1, "my_attribute")) #' print(xgb.attributes(bst1)) #' diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index cc16e18da..f9a810c85 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -56,9 +56,9 @@ #' dtrain <- with( #' agaricus.train, xgb.DMatrix(data, label = label, nthread = nthread) #' ) -#' xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -#' dtrain <- xgb.DMatrix('xgb.DMatrix.data') -#' if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +#' fname <- file.path(tempdir(), "xgb.DMatrix.data") +#' xgb.DMatrix.save(dtrain, fname) +#' dtrain <- xgb.DMatrix(fname) #' @export xgb.DMatrix <- function( data, diff --git a/R-package/R/xgb.DMatrix.save.R b/R-package/R/xgb.DMatrix.save.R index 91edb32d0..ef4599d0e 100644 --- a/R-package/R/xgb.DMatrix.save.R +++ b/R-package/R/xgb.DMatrix.save.R @@ -8,9 +8,9 @@ #' @examples #' data(agaricus.train, package='xgboost') #' dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) -#' xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -#' dtrain <- xgb.DMatrix('xgb.DMatrix.data') -#' if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +#' fname <- file.path(tempdir(), "xgb.DMatrix.data") +#' xgb.DMatrix.save(dtrain, fname) +#' dtrain <- xgb.DMatrix(fname) #' @export xgb.DMatrix.save <- function(dmatrix, fname) { if (typeof(fname) != "character") diff --git a/R-package/R/xgb.load.R b/R-package/R/xgb.load.R index e8f9e0023..27844ed8c 100644 --- a/R-package/R/xgb.load.R +++ b/R-package/R/xgb.load.R @@ -38,9 +38,9 @@ #' objective = "binary:logistic" #' ) #' -#' xgb.save(bst, 'xgb.model') -#' bst <- xgb.load('xgb.model') -#' if (file.exists('xgb.model')) file.remove('xgb.model') +#' fname <- file.path(tempdir(), "xgb.ubj") +#' xgb.save(bst, fname) +#' bst <- xgb.load(fname) #' @export xgb.load <- function(modelfile) { if (is.null(modelfile)) diff --git a/R-package/R/xgb.save.R b/R-package/R/xgb.save.R index 32b7d9618..474153bda 100644 --- a/R-package/R/xgb.save.R +++ b/R-package/R/xgb.save.R @@ -40,9 +40,9 @@ #' nrounds = 2, #' objective = "binary:logistic" #' ) -#' xgb.save(bst, 'xgb.model') -#' bst <- xgb.load('xgb.model') -#' if (file.exists('xgb.model')) file.remove('xgb.model') +#' fname <- file.path(tempdir(), "xgb.ubj") +#' xgb.save(bst, fname) +#' bst <- xgb.load(fname) #' @export xgb.save <- function(model, fname) { if (typeof(fname) != "character") diff --git a/R-package/inst/make-r-def.R b/R-package/inst/make-r-def.R index f40e54c56..c187b10a0 100644 --- a/R-package/inst/make-r-def.R +++ b/R-package/inst/make-r-def.R @@ -55,7 +55,7 @@ message(sprintf("Creating '%s' from '%s'", OUT_DEF_FILE, IN_DLL_FILE)) } # use objdump to dump all the symbols -OBJDUMP_FILE <- "objdump-out.txt" +OBJDUMP_FILE <- file.path(tempdir(), "objdump-out.txt") .pipe_shell_command_to_stdout( command = "objdump" , args = c("-p", IN_DLL_FILE) diff --git a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd index 023cff9fd..a8f46547e 100644 --- a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd +++ b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd @@ -38,13 +38,14 @@ bst <- xgb.train(data = xgb.DMatrix(agaricus.train$data, label = agaricus.train$ objective = "binary:logistic") # Save as a stand-alone file; load it with xgb.load() -xgb.save(bst, 'xgb.model') -bst2 <- xgb.load('xgb.model') +fname <- file.path(tempdir(), "xgb_model.ubj") +xgb.save(bst, fname) +bst2 <- xgb.load(fname) # Save as a stand-alone file (JSON); load it with xgb.load() -xgb.save(bst, 'xgb.model.json') -bst2 <- xgb.load('xgb.model.json') -if (file.exists('xgb.model.json')) file.remove('xgb.model.json') +fname <- file.path(tempdir(), "xgb_model.json") +xgb.save(bst, fname) +bst2 <- xgb.load(fname) # Save as a raw byte vector; load it with xgb.load.raw() xgb_bytes <- xgb.save.raw(bst) @@ -55,11 +56,11 @@ obj <- list(xgb_model_bytes = xgb.save.raw(bst), description = "My first XGBoost # Persist the R object. Here, saveRDS() is okay, since it doesn't persist # xgb.Booster directly. What's being persisted is the future-proof byte representation # as given by xgb.save.raw(). -saveRDS(obj, 'my_object.rds') +fname <- file.path(tempdir(), "my_object.Rds") +saveRDS(obj, fname) # Read back the R object -obj2 <- readRDS('my_object.rds') +obj2 <- readRDS(fname) # Re-construct xgb.Booster object from the bytes bst2 <- xgb.load.raw(obj2$xgb_model_bytes) -if (file.exists('my_object.rds')) file.remove('my_object.rds') } diff --git a/R-package/man/xgb.Booster.complete.Rd b/R-package/man/xgb.Booster.complete.Rd index 0adb1a69f..102224a8f 100644 --- a/R-package/man/xgb.Booster.complete.Rd +++ b/R-package/man/xgb.Booster.complete.Rd @@ -45,12 +45,12 @@ bst <- xgboost( objective = "binary:logistic" ) -saveRDS(bst, "xgb.model.rds") +fname <- file.path(tempdir(), "xgb_model.Rds") +saveRDS(bst, fname) # Warning: The resulting RDS file is only compatible with the current XGBoost version. # Refer to the section titled "a-compatibility-note-for-saveRDS-save". -bst1 <- readRDS("xgb.model.rds") -if (file.exists("xgb.model.rds")) file.remove("xgb.model.rds") +bst1 <- readRDS(fname) # the handle is invalid: print(bst1$handle) diff --git a/R-package/man/xgb.DMatrix.Rd b/R-package/man/xgb.DMatrix.Rd index 01436ba14..eb667377f 100644 --- a/R-package/man/xgb.DMatrix.Rd +++ b/R-package/man/xgb.DMatrix.Rd @@ -94,7 +94,7 @@ data.table::setDTthreads(nthread) dtrain <- with( agaricus.train, xgb.DMatrix(data, label = label, nthread = nthread) ) -xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -dtrain <- xgb.DMatrix('xgb.DMatrix.data') -if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +fname <- file.path(tempdir(), "xgb.DMatrix.data") +xgb.DMatrix.save(dtrain, fname) +dtrain <- xgb.DMatrix(fname) } diff --git a/R-package/man/xgb.DMatrix.save.Rd b/R-package/man/xgb.DMatrix.save.Rd index 401516059..d5c0563b3 100644 --- a/R-package/man/xgb.DMatrix.save.Rd +++ b/R-package/man/xgb.DMatrix.save.Rd @@ -17,7 +17,7 @@ Save xgb.DMatrix object to binary file \examples{ data(agaricus.train, package='xgboost') dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) -xgb.DMatrix.save(dtrain, 'xgb.DMatrix.data') -dtrain <- xgb.DMatrix('xgb.DMatrix.data') -if (file.exists('xgb.DMatrix.data')) file.remove('xgb.DMatrix.data') +fname <- file.path(tempdir(), "xgb.DMatrix.data") +xgb.DMatrix.save(dtrain, fname) +dtrain <- xgb.DMatrix(fname) } diff --git a/R-package/man/xgb.attr.Rd b/R-package/man/xgb.attr.Rd index 9203e6281..2aab62812 100644 --- a/R-package/man/xgb.attr.Rd +++ b/R-package/man/xgb.attr.Rd @@ -79,9 +79,9 @@ xgb.attr(bst, "my_attribute") <- "my attribute value" print(xgb.attr(bst, "my_attribute")) xgb.attributes(bst) <- list(a = 123, b = "abc") -xgb.save(bst, "xgb.model") -bst1 <- xgb.load("xgb.model") -if (file.exists("xgb.model")) file.remove("xgb.model") +fname <- file.path(tempdir(), "xgb.ubj") +xgb.save(bst, fname) +bst1 <- xgb.load(fname) print(xgb.attr(bst1, "my_attribute")) print(xgb.attributes(bst1)) diff --git a/R-package/man/xgb.load.Rd b/R-package/man/xgb.load.Rd index 63f551d7a..e6a2d6cdd 100644 --- a/R-package/man/xgb.load.Rd +++ b/R-package/man/xgb.load.Rd @@ -43,9 +43,9 @@ bst <- xgb.train( objective = "binary:logistic" ) -xgb.save(bst, 'xgb.model') -bst <- xgb.load('xgb.model') -if (file.exists('xgb.model')) file.remove('xgb.model') +fname <- file.path(tempdir(), "xgb.ubj") +xgb.save(bst, fname) +bst <- xgb.load(fname) } \seealso{ \code{\link{xgb.save}}, \code{\link{xgb.Booster.complete}}. diff --git a/R-package/man/xgb.save.Rd b/R-package/man/xgb.save.Rd index 22c6c8fa3..ee4b799c5 100644 --- a/R-package/man/xgb.save.Rd +++ b/R-package/man/xgb.save.Rd @@ -46,9 +46,9 @@ bst <- xgb.train( nrounds = 2, objective = "binary:logistic" ) -xgb.save(bst, 'xgb.model') -bst <- xgb.load('xgb.model') -if (file.exists('xgb.model')) file.remove('xgb.model') +fname <- file.path(tempdir(), "xgb.ubj") +xgb.save(bst, fname) +bst <- xgb.load(fname) } \seealso{ \code{\link{xgb.load}}, \code{\link{xgb.Booster.complete}}. diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 1f6cebbca..44c530c90 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -330,17 +330,17 @@ test_that("training continuation works", { } expect_equal(dim(bst2$evaluation_log), c(2, 2)) # test continuing from a model in file - xgb.save(bst1, "xgboost.json") - bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = "xgboost.json") + fname <- file.path(tempdir(), "xgboost.json") + xgb.save(bst1, fname) + bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = fname) if (!windows_flag && !solaris_flag) { expect_equal(bst$raw, bst2$raw) } expect_equal(dim(bst2$evaluation_log), c(2, 2)) - file.remove("xgboost.json") }) test_that("model serialization works", { - out_path <- "model_serialization" + out_path <- file.path(tempdir(), "model_serialization") dtrain <- xgb.DMatrix(train$data, label = train$label, nthread = n_threads) watchlist <- list(train = dtrain) param <- list(objective = "binary:logistic", nthread = n_threads) diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index de5150380..bee98f688 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -174,16 +174,17 @@ test_that("cb.reset.parameters works as expected", { test_that("cb.save.model works as expected", { files <- c('xgboost_01.json', 'xgboost_02.json', 'xgboost.json') + files <- unname(sapply(files, function(f) file.path(tempdir(), f))) for (f in files) if (file.exists(f)) file.remove(f) bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 1, verbose = 0, - save_period = 1, save_name = "xgboost_%02d.json") - expect_true(file.exists('xgboost_01.json')) - expect_true(file.exists('xgboost_02.json')) - b1 <- xgb.load('xgboost_01.json') + save_period = 1, save_name = file.path(tempdir(), "xgboost_%02d.json")) + expect_true(file.exists(files[1])) + expect_true(file.exists(files[2])) + b1 <- xgb.load(files[1]) xgb.parameters(b1) <- list(nthread = 2) expect_equal(xgb.ntree(b1), 1) - b2 <- xgb.load('xgboost_02.json') + b2 <- xgb.load(files[2]) xgb.parameters(b2) <- list(nthread = 2) expect_equal(xgb.ntree(b2), 2) @@ -193,9 +194,9 @@ test_that("cb.save.model works as expected", { # save_period = 0 saves the last iteration's model bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 1, verbose = 0, - save_period = 0, save_name = 'xgboost.json') - expect_true(file.exists('xgboost.json')) - b2 <- xgb.load('xgboost.json') + save_period = 0, save_name = file.path(tempdir(), 'xgboost.json')) + expect_true(file.exists(files[3])) + b2 <- xgb.load(files[3]) xgb.config(b2) <- xgb.config(bst) expect_equal(bst$raw, b2$raw) @@ -225,14 +226,13 @@ test_that("early stopping xgb.train works", { ) expect_equal(bst$evaluation_log, bst0$evaluation_log) - xgb.save(bst, "model.bin") - loaded <- xgb.load("model.bin") + fname <- file.path(tempdir(), "model.bin") + xgb.save(bst, fname) + loaded <- xgb.load(fname) expect_false(is.null(loaded$best_iteration)) expect_equal(loaded$best_iteration, bst$best_ntreelimit) expect_equal(loaded$best_ntreelimit, bst$best_ntreelimit) - - file.remove("model.bin") }) test_that("early stopping using a specific metric works", { diff --git a/R-package/tests/testthat/test_dmatrix.R b/R-package/tests/testthat/test_dmatrix.R index 81ac884d0..568aaa3bd 100644 --- a/R-package/tests/testthat/test_dmatrix.R +++ b/R-package/tests/testthat/test_dmatrix.R @@ -67,20 +67,22 @@ test_that("xgb.DMatrix: NA", { x[1, "x1"] <- NA m <- xgb.DMatrix(x, nthread = n_threads) - xgb.DMatrix.save(m, "int.dmatrix") + fname_int <- file.path(tempdir(), "int.dmatrix") + xgb.DMatrix.save(m, fname_int) x <- matrix(as.numeric(x), nrow = n_samples, ncol = 2) colnames(x) <- c("x1", "x2") m <- xgb.DMatrix(x, nthread = n_threads) - xgb.DMatrix.save(m, "float.dmatrix") + fname_float <- file.path(tempdir(), "float.dmatrix") + xgb.DMatrix.save(m, fname_float) - iconn <- file("int.dmatrix", "rb") - fconn <- file("float.dmatrix", "rb") + iconn <- file(fname_int, "rb") + fconn <- file(fname_float, "rb") - expect_equal(file.size("int.dmatrix"), file.size("float.dmatrix")) + expect_equal(file.size(fname_int), file.size(fname_float)) - bytes <- file.size("int.dmatrix") + bytes <- file.size(fname_int) idmatrix <- readBin(iconn, "raw", n = bytes) fdmatrix <- readBin(fconn, "raw", n = bytes) @@ -90,8 +92,8 @@ test_that("xgb.DMatrix: NA", { close(iconn) close(fconn) - file.remove("int.dmatrix") - file.remove("float.dmatrix") + file.remove(fname_int) + file.remove(fname_float) }) test_that("xgb.DMatrix: saving, loading", { @@ -274,17 +276,19 @@ test_that("xgb.DMatrix: Inf as missing", { x_nan[2, 1] <- NA_real_ m_inf <- xgb.DMatrix(x_inf, nthread = n_threads, missing = Inf) - xgb.DMatrix.save(m_inf, "inf.dmatrix") + fname_inf <- file.path(tempdir(), "inf.dmatrix") + xgb.DMatrix.save(m_inf, fname_inf) m_nan <- xgb.DMatrix(x_nan, nthread = n_threads, missing = NA_real_) - xgb.DMatrix.save(m_nan, "nan.dmatrix") + fname_nan <- file.path(tempdir(), "nan.dmatrix") + xgb.DMatrix.save(m_nan, fname_nan) - infconn <- file("inf.dmatrix", "rb") - nanconn <- file("nan.dmatrix", "rb") + infconn <- file(fname_inf, "rb") + nanconn <- file(fname_nan, "rb") - expect_equal(file.size("inf.dmatrix"), file.size("nan.dmatrix")) + expect_equal(file.size(fname_inf), file.size(fname_nan)) - bytes <- file.size("inf.dmatrix") + bytes <- file.size(fname_inf) infdmatrix <- readBin(infconn, "raw", n = bytes) nandmatrix <- readBin(nanconn, "raw", n = bytes) @@ -294,8 +298,8 @@ test_that("xgb.DMatrix: Inf as missing", { close(infconn) close(nanconn) - file.remove("inf.dmatrix") - file.remove("nan.dmatrix") + file.remove(fname_inf) + file.remove(fname_nan) }) test_that("xgb.DMatrix: error on three-dimensional array", { diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index fd1fffbac..62a8c44bc 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -217,9 +217,9 @@ test_that("xgb-attribute functionality", { xgb.attributes(bst.Tree) <- list.val expect_equal(xgb.attributes(bst.Tree), list.ch) # serializing: - xgb.save(bst.Tree, 'xgb.model') - bst <- xgb.load('xgb.model') - if (file.exists('xgb.model')) file.remove('xgb.model') + fname <- file.path(tempdir(), "xgb.model") + xgb.save(bst.Tree, fname) + bst <- xgb.load(fname) expect_equal(xgb.attr(bst, "my_attr"), val) expect_equal(xgb.attributes(bst), list.ch) # deletion: @@ -256,15 +256,15 @@ if (grepl('Windows', Sys.info()[['sysname']], fixed = TRUE) || test_that("xgb.Booster serializing as R object works", { .skip_if_vcd_not_available() - saveRDS(bst.Tree, 'xgb.model.rds') - bst <- readRDS('xgb.model.rds') + fname_rds <- file.path(tempdir(), "xgb.model.rds") + saveRDS(bst.Tree, fname_rds) + bst <- readRDS(fname_rds) dtrain <- xgb.DMatrix(sparse_matrix, label = label, nthread = 2) expect_equal(predict(bst.Tree, dtrain), predict(bst, dtrain), tolerance = float_tolerance) expect_equal(xgb.dump(bst.Tree), xgb.dump(bst)) - xgb.save(bst, 'xgb.model') - if (file.exists('xgb.model')) file.remove('xgb.model') - bst <- readRDS('xgb.model.rds') - if (file.exists('xgb.model.rds')) file.remove('xgb.model.rds') + fname_bin <- file.path(tempdir(), "xgb.model") + xgb.save(bst, fname_bin) + bst <- readRDS(fname_rds) nil_ptr <- new("externalptr") class(nil_ptr) <- "xgb.Booster.handle" expect_true(identical(bst$handle, nil_ptr)) diff --git a/R-package/vignettes/xgboostPresentation.Rmd b/R-package/vignettes/xgboostPresentation.Rmd index 1b015fab8..90393060f 100644 --- a/R-package/vignettes/xgboostPresentation.Rmd +++ b/R-package/vignettes/xgboostPresentation.Rmd @@ -400,9 +400,10 @@ In simple cases, it will happen because there is nothing better than a linear al Like saving models, `xgb.DMatrix` object (which groups both dataset and outcome) can also be saved using `xgb.DMatrix.save` function. ```{r DMatrixSave, message=F, warning=F} -xgb.DMatrix.save(dtrain, "dtrain.buffer") +fname <- file.path(tempdir(), "dtrain.buffer") +xgb.DMatrix.save(dtrain, fname) # to load it in, simply call xgb.DMatrix -dtrain2 <- xgb.DMatrix("dtrain.buffer") +dtrain2 <- xgb.DMatrix(fname) bst <- xgb.train( data = dtrain2 , max_depth = 2 @@ -415,7 +416,7 @@ bst <- xgb.train( ``` ```{r DMatrixDel, include=FALSE} -file.remove("dtrain.buffer") +file.remove(fname) ``` #### Information extraction @@ -466,7 +467,8 @@ Hopefully for you, **XGBoost** implements such functions. ```{r saveModel, message=F, warning=F} # save model to binary local file -xgb.save(bst, "xgboost.model") +fname <- file.path(tempdir(), "xgb_model.ubj") +xgb.save(bst, fname) ``` > `xgb.save` function should return `r TRUE` if everything goes well and crashes otherwise. @@ -475,7 +477,7 @@ An interesting test to see how identical our saved model is to the original one ```{r loadModel, message=F, warning=F} # load binary model to R -bst2 <- xgb.load("xgboost.model") +bst2 <- xgb.load(fname) xgb.parameters(bst2) <- list(nthread = 2) pred2 <- predict(bst2, test$data) @@ -485,7 +487,7 @@ print(paste("sum(abs(pred2-pred))=", sum(abs(pred2 - pred)))) ```{r clean, include=FALSE} # delete the created model -file.remove("./xgboost.model") +file.remove(fname) ``` > result is `0`? We are good! From 2f57bbde3c5feeb04c26fb2bb1391c63568662cf Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 09:54:39 +0800 Subject: [PATCH 088/109] Additional tests for attributes and model booosted rounds. (#9962) --- src/learner.cc | 8 ++- tests/python/test_basic_models.py | 82 ++++++++++++++++++++----------- tests/python/test_model_io.py | 30 +++++++++++ 3 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/learner.cc b/src/learner.cc index 6b0fd7e4b..db72f7164 100644 --- a/src/learner.cc +++ b/src/learner.cc @@ -535,8 +535,7 @@ class LearnerConfiguration : public Learner { tparam_.booster = get(gradient_booster["name"]); if (!gbm_) { - gbm_.reset(GradientBooster::Create(tparam_.booster, - &ctx_, &learner_model_param_)); + gbm_.reset(GradientBooster::Create(tparam_.booster, &ctx_, &learner_model_param_)); } gbm_->LoadConfig(gradient_booster); @@ -1095,6 +1094,11 @@ class LearnerIO : public LearnerConfiguration { std::vector > extra_attr; mparam.contain_extra_attrs = 1; + if (!this->feature_names_.empty() || !this->feature_types_.empty()) { + LOG(WARNING) << "feature names and feature types are being disregarded, use JSON/UBJSON " + "format instead."; + } + { // Similar to JSON model IO, we save the objective. Json j_obj { Object() }; diff --git a/tests/python/test_basic_models.py b/tests/python/test_basic_models.py index ca35c4e91..828c24862 100644 --- a/tests/python/test_basic_models.py +++ b/tests/python/test_basic_models.py @@ -1,5 +1,4 @@ import json -import locale import os import tempfile @@ -110,20 +109,39 @@ class TestModels: predt_2 = bst.predict(dtrain) assert np.all(np.abs(predt_2 - predt_1) < 1e-6) - def test_boost_from_existing_model(self): + def test_boost_from_existing_model(self) -> None: X, _ = tm.load_agaricus(__file__) - booster = xgb.train({'tree_method': 'hist'}, X, num_boost_round=4) + booster = xgb.train({"tree_method": "hist"}, X, num_boost_round=4) assert booster.num_boosted_rounds() == 4 - booster = xgb.train({'tree_method': 'hist'}, X, num_boost_round=4, - xgb_model=booster) + booster.set_param({"tree_method": "approx"}) + assert booster.num_boosted_rounds() == 4 + booster = xgb.train( + {"tree_method": "hist"}, X, num_boost_round=4, xgb_model=booster + ) assert booster.num_boosted_rounds() == 8 - booster = xgb.train({'updater': 'prune', 'process_type': 'update'}, X, - num_boost_round=4, xgb_model=booster) + with pytest.warns(UserWarning, match="`updater`"): + booster = xgb.train( + {"updater": "prune", "process_type": "update"}, + X, + num_boost_round=4, + xgb_model=booster, + ) # Trees are moved for update, the rounds is reduced. This test is # written for being compatible with current code (1.0.0). If the # behaviour is considered sub-optimal, feel free to change. assert booster.num_boosted_rounds() == 4 + booster = xgb.train({"booster": "gblinear"}, X, num_boost_round=4) + assert booster.num_boosted_rounds() == 4 + booster.set_param({"updater": "coord_descent"}) + assert booster.num_boosted_rounds() == 4 + booster.set_param({"updater": "shotgun"}) + assert booster.num_boosted_rounds() == 4 + booster = xgb.train( + {"booster": "gblinear"}, X, num_boost_round=4, xgb_model=booster + ) + assert booster.num_boosted_rounds() == 8 + def run_custom_objective(self, tree_method=None): param = { 'max_depth': 2, @@ -307,25 +325,6 @@ class TestModels: for d in text_dump: assert d.find(r"feature \"2\"") != -1 - @pytest.mark.skipif(**tm.no_sklearn()) - def test_attributes(self): - from sklearn.datasets import load_iris - - X, y = load_iris(return_X_y=True) - cls = xgb.XGBClassifier(n_estimators=2) - cls.fit(X, y, early_stopping_rounds=1, eval_set=[(X, y)]) - assert cls.get_booster().best_iteration == cls.n_estimators - 1 - assert cls.best_iteration == cls.get_booster().best_iteration - - with tempfile.TemporaryDirectory() as tmpdir: - path = os.path.join(tmpdir, "cls.json") - cls.save_model(path) - - cls = xgb.XGBClassifier(n_estimators=2) - cls.load_model(path) - assert cls.get_booster().best_iteration == cls.n_estimators - 1 - assert cls.best_iteration == cls.get_booster().best_iteration - def run_slice( self, booster: xgb.Booster, @@ -493,18 +492,23 @@ class TestModels: np.testing.assert_allclose(predt0, predt1, atol=1e-5) @pytest.mark.skipif(**tm.no_pandas()) - def test_feature_info(self): + @pytest.mark.parametrize("ext", ["json", "ubj"]) + def test_feature_info(self, ext: str) -> None: import pandas as pd + # make data rows = 100 cols = 10 X = rng.randn(rows, cols) y = rng.randn(rows) + + # Test with pandas, which has feature info. feature_names = ["test_feature_" + str(i) for i in range(cols)] X_pd = pd.DataFrame(X, columns=feature_names) X_pd[f"test_feature_{3}"] = X_pd.iloc[:, 3].astype(np.int32) Xy = xgb.DMatrix(X_pd, y) + assert Xy.feature_types is not None assert Xy.feature_types[3] == "int" booster = xgb.train({}, dtrain=Xy, num_boost_round=1) @@ -513,10 +517,32 @@ class TestModels: assert booster.feature_types == Xy.feature_types with tempfile.TemporaryDirectory() as tmpdir: - path = tmpdir + "model.json" + path = tmpdir + f"model.{ext}" booster.save_model(path) booster = xgb.Booster() booster.load_model(path) assert booster.feature_names == Xy.feature_names assert booster.feature_types == Xy.feature_types + + # Test with numpy, no feature info is set + Xy = xgb.DMatrix(X, y) + assert Xy.feature_names is None + assert Xy.feature_types is None + + booster = xgb.train({}, dtrain=Xy, num_boost_round=1) + assert booster.feature_names is None + assert booster.feature_types is None + + # test explicitly set + fns = [str(i) for i in range(cols)] + booster.feature_names = fns + + assert booster.feature_names == fns + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, f"model.{ext}") + booster.save_model(path) + + booster = xgb.Booster(model_file=path) + assert booster.feature_names == fns diff --git a/tests/python/test_model_io.py b/tests/python/test_model_io.py index 884bba08f..df0fff22e 100644 --- a/tests/python/test_model_io.py +++ b/tests/python/test_model_io.py @@ -466,3 +466,33 @@ def test_with_sklearn_obj_metric() -> None: assert not callable(reg_2.objective) assert not callable(reg_2.eval_metric) assert reg_2.eval_metric is None + + +@pytest.mark.skipif(**tm.no_sklearn()) +def test_attributes() -> None: + from sklearn.datasets import load_iris + + X, y = load_iris(return_X_y=True) + clf = xgb.XGBClassifier(n_estimators=2, early_stopping_rounds=1) + clf.fit(X, y, eval_set=[(X, y)]) + best_iteration = clf.get_booster().best_iteration + assert best_iteration is not None + assert clf.n_estimators is not None + assert best_iteration == clf.n_estimators - 1 + + best_iteration = clf.best_iteration + assert best_iteration == clf.get_booster().best_iteration + + clf.get_booster().set_attr(foo="bar") + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "clf.json") + clf.save_model(path) + + clf = xgb.XGBClassifier(n_estimators=2) + clf.load_model(path) + assert clf.n_estimators is not None + assert clf.get_booster().best_iteration == clf.n_estimators - 1 + assert clf.best_iteration == clf.get_booster().best_iteration + + assert clf.get_booster().attributes()["foo"] == "bar" From 01c471155627f9d81c230d0a16a7655c9fdd16c4 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Tue, 9 Jan 2024 19:59:01 +0800 Subject: [PATCH 089/109] Check `__cuda_array_interface__` instead of cupy class. (#9971) * Now XGBoost can directly consume CUDA data from torch. --- doc/python/python_intro.rst | 2 ++ python-package/xgboost/compat.py | 4 ++-- python-package/xgboost/core.py | 19 +++++++++++-------- python-package/xgboost/dask/__init__.py | 4 ++-- python-package/xgboost/data.py | 25 +++++++++++-------------- python-package/xgboost/sklearn.py | 6 +++--- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/doc/python/python_intro.rst b/doc/python/python_intro.rst index cc0e461e0..0d26a5253 100644 --- a/doc/python/python_intro.rst +++ b/doc/python/python_intro.rst @@ -162,6 +162,8 @@ Support Matrix +-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ | cupy.ndarray | T | T | T | T | T | T | +-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ +| torch.Tensor | T | T | T | T | T | T | ++-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ | dlpack | CPA | CPA | | CPA | FF | FF | +-------------------------+-----------+-------------------+-----------+-----------+--------------------+-------------+ | datatable.Frame | T | FF | | NPA | FF | | diff --git a/python-package/xgboost/compat.py b/python-package/xgboost/compat.py index 7c11495f7..729750f1f 100644 --- a/python-package/xgboost/compat.py +++ b/python-package/xgboost/compat.py @@ -138,9 +138,9 @@ def concat(value: Sequence[_T]) -> _T: # pylint: disable=too-many-return-statem from cudf import concat as CUDF_concat # pylint: disable=import-error return CUDF_concat(value, axis=0) - from .data import _is_cupy_array + from .data import _is_cupy_alike - if _is_cupy_array(value[0]): + if _is_cupy_alike(value[0]): import cupy # pylint: disable=import-error # pylint: disable=c-extension-no-member,no-member diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 4d093293d..36be766b1 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -357,10 +357,13 @@ def _numpy2ctypes_type(dtype: Type[np.number]) -> Type[CNumeric]: return _NUMPY_TO_CTYPES_MAPPING[dtype] +def _array_hasobject(data: DataType) -> bool: + return hasattr(data.dtype, "hasobject") and data.dtype.hasobject + + def _cuda_array_interface(data: DataType) -> bytes: - assert ( - data.dtype.hasobject is False - ), "Input data contains `object` dtype. Expecting numeric data." + if _array_hasobject(data): + raise ValueError("Input data contains `object` dtype. Expecting numeric data.") interface = data.__cuda_array_interface__ if "mask" in interface: interface["mask"] = interface["mask"].__cuda_array_interface__ @@ -2102,7 +2105,7 @@ class Booster: _array_interface, _cuda_array_interface, _ensure_np_dtype, - _is_cupy_array, + _is_cupy_alike, ) self._assign_dmatrix_features(dtrain) @@ -2116,7 +2119,7 @@ class Booster: "Expecting `np.ndarray` or `cupy.ndarray` for gradient and hessian." f" Got: {type(array)}" ) - if not isinstance(array, np.ndarray) and not _is_cupy_array(array): + if not isinstance(array, np.ndarray) and not _is_cupy_alike(array): raise TypeError(msg) n_samples = dtrain.num_row() @@ -2131,7 +2134,7 @@ class Booster: if isinstance(array, np.ndarray): array, _ = _ensure_np_dtype(array, array.dtype) interface = _array_interface(array) - elif _is_cupy_array(array): + elif _is_cupy_alike(array): interface = _cuda_array_interface(array) else: raise TypeError(msg) @@ -2461,7 +2464,7 @@ class Booster: _arrow_transform, _is_arrow, _is_cudf_df, - _is_cupy_array, + _is_cupy_alike, _is_list, _is_np_array_like, _is_pandas_df, @@ -2543,7 +2546,7 @@ class Booster: ) ) return _prediction_output(shape, dims, preds, False) - if _is_cupy_array(data): + if _is_cupy_alike(data): from .data import _transform_cupy_array data = _transform_cupy_array(data) diff --git a/python-package/xgboost/dask/__init__.py b/python-package/xgboost/dask/__init__.py index 046a2c982..6b4ae5b07 100644 --- a/python-package/xgboost/dask/__init__.py +++ b/python-package/xgboost/dask/__init__.py @@ -75,7 +75,7 @@ from xgboost.core import ( _deprecate_positional_args, _expect, ) -from xgboost.data import _is_cudf_ser, _is_cupy_array +from xgboost.data import _is_cudf_ser, _is_cupy_alike from xgboost.sklearn import ( XGBClassifier, XGBClassifierBase, @@ -1909,7 +1909,7 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): self.classes_ = await self.client.compute(y.drop_duplicates()) if _is_cudf_ser(self.classes_): self.classes_ = self.classes_.to_cupy() - if _is_cupy_array(self.classes_): + if _is_cupy_alike(self.classes_): self.classes_ = self.classes_.get() self.classes_ = numpy.array(self.classes_) self.n_classes_ = len(self.classes_) diff --git a/python-package/xgboost/data.py b/python-package/xgboost/data.py index 05337e788..49a0f43b7 100644 --- a/python-package/xgboost/data.py +++ b/python-package/xgboost/data.py @@ -26,6 +26,7 @@ from .core import ( DataIter, DataSplitMode, DMatrix, + _array_hasobject, _check_call, _cuda_array_interface, _ProxyDMatrix, @@ -77,9 +78,8 @@ def is_scipy_csr(data: DataType) -> bool: def _array_interface_dict(data: np.ndarray) -> dict: - assert ( - data.dtype.hasobject is False - ), "Input data contains `object` dtype. Expecting numeric data." + if _array_hasobject(data): + raise ValueError("Input data contains `object` dtype. Expecting numeric data.") interface = data.__array_interface__ if "mask" in interface: interface["mask"] = interface["mask"].__array_interface__ @@ -219,7 +219,7 @@ def _is_np_array_like(data: DataType) -> bool: def _ensure_np_dtype( data: DataType, dtype: Optional[NumpyDType] ) -> Tuple[np.ndarray, Optional[NumpyDType]]: - if data.dtype.hasobject or data.dtype in [np.float16, np.bool_]: + if _array_hasobject(data) or data.dtype in [np.float16, np.bool_]: dtype = np.float32 data = data.astype(dtype, copy=False) if not data.flags.aligned: @@ -998,11 +998,8 @@ def _is_cudf_ser(data: DataType) -> bool: return lazy_isinstance(data, "cudf.core.series", "Series") -def _is_cupy_array(data: DataType) -> bool: - return any( - lazy_isinstance(data, n, "ndarray") - for n in ("cupy.core.core", "cupy", "cupy._core.core") - ) +def _is_cupy_alike(data: DataType) -> bool: + return hasattr(data, "__cuda_array_interface__") def _transform_cupy_array(data: DataType) -> CupyT: @@ -1010,7 +1007,7 @@ def _transform_cupy_array(data: DataType) -> CupyT: if not hasattr(data, "__cuda_array_interface__") and hasattr(data, "__array__"): data = cupy.array(data, copy=False) - if data.dtype.hasobject or data.dtype in [cupy.bool_]: + if _array_hasobject(data) or data.dtype in [cupy.bool_]: data = data.astype(cupy.float32, copy=False) return data @@ -1222,7 +1219,7 @@ def dispatch_data_backend( return _from_cudf_df( data, missing, threads, feature_names, feature_types, enable_categorical ) - if _is_cupy_array(data): + if _is_cupy_alike(data): return _from_cupy_array(data, missing, threads, feature_names, feature_types) if _is_cupy_csr(data): raise TypeError("cupyx CSR is not supported yet.") @@ -1354,7 +1351,7 @@ def dispatch_meta_backend( data = _transform_dlpack(data) _meta_from_cupy_array(data, name, handle) return - if _is_cupy_array(data): + if _is_cupy_alike(data): _meta_from_cupy_array(data, name, handle) return if _is_cudf_ser(data): @@ -1419,7 +1416,7 @@ def _proxy_transform( return _transform_cudf_df( data, feature_names, feature_types, enable_categorical ) - if _is_cupy_array(data): + if _is_cupy_alike(data): data = _transform_cupy_array(data) return data, None, feature_names, feature_types if _is_dlpack(data): @@ -1470,7 +1467,7 @@ def dispatch_proxy_set_data( # pylint: disable=W0212 proxy._set_data_from_cuda_columnar(data, cast(List, cat_codes)) return - if _is_cupy_array(data): + if _is_cupy_alike(data): proxy._set_data_from_cuda_interface(data) # pylint: disable=W0212 return if _is_dlpack(data): diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index 3383ae0b7..8c3a96784 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -39,7 +39,7 @@ from .core import ( _deprecate_positional_args, _parse_eval_str, ) -from .data import _is_cudf_df, _is_cudf_ser, _is_cupy_array, _is_pandas_df +from .data import _is_cudf_df, _is_cudf_ser, _is_cupy_alike, _is_pandas_df from .training import train @@ -1177,7 +1177,7 @@ class XGBModel(XGBModelBase): base_margin=base_margin, validate_features=validate_features, ) - if _is_cupy_array(predts): + if _is_cupy_alike(predts): import cupy # pylint: disable=import-error predts = cupy.asnumpy(predts) # ensure numpy array is used. @@ -1458,7 +1458,7 @@ class XGBClassifier(XGBModel, XGBClassifierBase): classes = cp.unique(y.values) self.n_classes_ = len(classes) expected_classes = cp.array(self.classes_) - elif _is_cupy_array(y): + elif _is_cupy_alike(y): import cupy as cp # pylint: disable=E0401 classes = cp.unique(y) From d3a8d284abd0ac060493809e76d4896160147f3f Mon Sep 17 00:00:00 2001 From: david-cortes Date: Wed, 10 Jan 2024 22:08:42 +0100 Subject: [PATCH 090/109] [R] On-demand serialization + standardization of attributes (#9924) --------- Co-authored-by: Jiaming Yuan --- R-package/NAMESPACE | 13 +- R-package/R/callbacks.R | 81 ++- R-package/R/utils.R | 75 +- R-package/R/xgb.Booster.R | 683 +++++++++++------- R-package/R/xgb.DMatrix.R | 30 +- R-package/R/xgb.cv.R | 14 +- R-package/R/xgb.dump.R | 10 +- R-package/R/xgb.importance.R | 20 +- R-package/R/xgb.load.R | 15 +- R-package/R/xgb.load.raw.R | 19 +- R-package/R/xgb.model.dt.tree.R | 55 +- R-package/R/xgb.plot.shap.R | 8 +- R-package/R/xgb.save.R | 27 +- R-package/R/xgb.save.raw.R | 4 +- R-package/R/xgb.serialize.R | 21 - R-package/R/xgb.train.R | 114 +-- R-package/R/xgb.unserialize.R | 41 -- R-package/R/xgboost.R | 2 + R-package/demo/00Index | 1 - R-package/demo/README.md | 1 - R-package/demo/caret_wrapper.R | 44 -- R-package/demo/predict_leaf_indices.R | 2 +- R-package/demo/runall.R | 1 - .../a-compatibility-note-for-saveRDS-save.Rd | 53 +- R-package/man/cb.save.model.Rd | 17 +- R-package/man/coef.xgb.Booster.Rd | 50 ++ R-package/man/getinfo.Rd | 57 +- R-package/man/predict.xgb.Booster.Rd | 5 +- R-package/man/print.xgb.Booster.Rd | 10 +- R-package/man/setinfo.Rd | 42 -- R-package/man/variable.names.xgb.Booster.Rd | 22 + R-package/man/xgb.Booster.complete.Rd | 61 -- R-package/man/xgb.attr.Rd | 15 +- R-package/man/xgb.config.Rd | 14 +- R-package/man/xgb.copy.Booster.Rd | 53 ++ R-package/man/xgb.gblinear.history.Rd | 11 +- R-package/man/xgb.get.num.boosted.rounds.Rd | 22 + R-package/man/xgb.is.same.Booster.Rd | 59 ++ R-package/man/xgb.load.Rd | 2 +- R-package/man/xgb.load.raw.Rd | 4 +- R-package/man/xgb.model.dt.tree.Rd | 9 +- R-package/man/xgb.parameters.Rd | 16 +- R-package/man/xgb.save.Rd | 24 +- R-package/man/xgb.save.raw.Rd | 6 +- R-package/man/xgb.serialize.Rd | 29 - R-package/man/xgb.train.Rd | 44 +- R-package/man/xgb.unserialize.Rd | 21 - R-package/src/init.c | 18 +- R-package/src/xgboost_R.cc | 255 ++++++- R-package/src/xgboost_R.h | 53 +- R-package/tests/helper_scripts/install_deps.R | 1 - R-package/tests/testthat/test_basic.R | 216 ++++-- R-package/tests/testthat/test_callbacks.R | 64 +- .../tests/testthat/test_custom_objective.R | 8 +- R-package/tests/testthat/test_glm.R | 16 +- R-package/tests/testthat/test_helpers.R | 44 +- R-package/tests/testthat/test_io.R | 47 +- .../tests/testthat/test_model_compatibility.R | 10 +- .../tests/testthat/test_parameter_exposure.R | 6 +- R-package/tests/testthat/test_ranking.R | 8 +- R-package/tests/testthat/test_update.R | 36 +- R-package/vignettes/xgboost.Rnw | 223 ------ R-package/vignettes/xgboostPresentation.Rmd | 119 +-- R-package/vignettes/xgboostfromJSON.Rmd | 3 +- 64 files changed, 1773 insertions(+), 1281 deletions(-) delete mode 100644 R-package/R/xgb.serialize.R delete mode 100644 R-package/R/xgb.unserialize.R delete mode 100644 R-package/demo/caret_wrapper.R create mode 100644 R-package/man/coef.xgb.Booster.Rd delete mode 100644 R-package/man/setinfo.Rd create mode 100644 R-package/man/variable.names.xgb.Booster.Rd delete mode 100644 R-package/man/xgb.Booster.complete.Rd create mode 100644 R-package/man/xgb.copy.Booster.Rd create mode 100644 R-package/man/xgb.get.num.boosted.rounds.Rd create mode 100644 R-package/man/xgb.is.same.Booster.Rd delete mode 100644 R-package/man/xgb.serialize.Rd delete mode 100644 R-package/man/xgb.unserialize.Rd delete mode 100644 R-package/vignettes/xgboost.Rnw diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index e6f7a82b8..a29c9b1e0 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -2,16 +2,19 @@ S3method("[",xgb.DMatrix) S3method("dimnames<-",xgb.DMatrix) +S3method(coef,xgb.Booster) S3method(dim,xgb.DMatrix) S3method(dimnames,xgb.DMatrix) +S3method(getinfo,xgb.Booster) S3method(getinfo,xgb.DMatrix) S3method(predict,xgb.Booster) -S3method(predict,xgb.Booster.handle) S3method(print,xgb.Booster) S3method(print,xgb.DMatrix) S3method(print,xgb.cv.synchronous) +S3method(setinfo,xgb.Booster) S3method(setinfo,xgb.DMatrix) S3method(slice,xgb.DMatrix) +S3method(variable.names,xgb.Booster) export("xgb.attr<-") export("xgb.attributes<-") export("xgb.config<-") @@ -26,13 +29,13 @@ export(cb.save.model) export(getinfo) export(setinfo) export(slice) -export(xgb.Booster.complete) export(xgb.DMatrix) export(xgb.DMatrix.hasinfo) export(xgb.DMatrix.save) export(xgb.attr) export(xgb.attributes) export(xgb.config) +export(xgb.copy.Booster) export(xgb.create.features) export(xgb.cv) export(xgb.dump) @@ -41,10 +44,12 @@ export(xgb.get.DMatrix.data) export(xgb.get.DMatrix.num.non.missing) export(xgb.get.DMatrix.qcut) export(xgb.get.config) +export(xgb.get.num.boosted.rounds) export(xgb.ggplot.deepness) export(xgb.ggplot.importance) export(xgb.ggplot.shap.summary) export(xgb.importance) +export(xgb.is.same.Booster) export(xgb.load) export(xgb.load.raw) export(xgb.model.dt.tree) @@ -56,10 +61,8 @@ export(xgb.plot.shap.summary) export(xgb.plot.tree) export(xgb.save) export(xgb.save.raw) -export(xgb.serialize) export(xgb.set.config) export(xgb.train) -export(xgb.unserialize) export(xgboost) import(methods) importClassesFrom(Matrix,dgCMatrix) @@ -88,8 +91,10 @@ importFrom(graphics,title) importFrom(jsonlite,fromJSON) importFrom(jsonlite,toJSON) importFrom(methods,new) +importFrom(stats,coef) importFrom(stats,median) importFrom(stats,predict) +importFrom(stats,variable.names) importFrom(utils,head) importFrom(utils,object.size) importFrom(utils,str) diff --git a/R-package/R/callbacks.R b/R-package/R/callbacks.R index f8f3b5a30..b3d6bdb1a 100644 --- a/R-package/R/callbacks.R +++ b/R-package/R/callbacks.R @@ -228,7 +228,7 @@ cb.reset.parameters <- function(new_params) { }) if (!is.null(env$bst)) { - xgb.parameters(env$bst$handle) <- pars + xgb.parameters(env$bst) <- pars } else { for (fd in env$bst_folds) xgb.parameters(fd$bst) <- pars @@ -333,13 +333,13 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, if (!is.null(env$bst)) { if (!inherits(env$bst, 'xgb.Booster')) stop("'bst' in the parent frame must be an 'xgb.Booster'") - if (!is.null(best_score <- xgb.attr(env$bst$handle, 'best_score'))) { + if (!is.null(best_score <- xgb.attr(env$bst, 'best_score'))) { best_score <<- as.numeric(best_score) - best_iteration <<- as.numeric(xgb.attr(env$bst$handle, 'best_iteration')) + 1 - best_msg <<- as.numeric(xgb.attr(env$bst$handle, 'best_msg')) + best_iteration <<- as.numeric(xgb.attr(env$bst, 'best_iteration')) + 1 + best_msg <<- as.numeric(xgb.attr(env$bst, 'best_msg')) } else { - xgb.attributes(env$bst$handle) <- list(best_iteration = best_iteration - 1, - best_score = best_score) + xgb.attributes(env$bst) <- list(best_iteration = best_iteration - 1, + best_score = best_score) } } else if (is.null(env$bst_folds) || is.null(env$basket)) { stop("Parent frame has neither 'bst' nor ('bst_folds' and 'basket')") @@ -348,7 +348,7 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, finalizer <- function(env) { if (!is.null(env$bst)) { - attr_best_score <- as.numeric(xgb.attr(env$bst$handle, 'best_score')) + attr_best_score <- as.numeric(xgb.attr(env$bst, 'best_score')) if (best_score != attr_best_score) { # If the difference is too big, throw an error if (abs(best_score - attr_best_score) >= 1e-14) { @@ -358,9 +358,9 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, # If the difference is due to floating-point truncation, update best_score best_score <- attr_best_score } - env$bst$best_iteration <- best_iteration - env$bst$best_ntreelimit <- best_ntreelimit - env$bst$best_score <- best_score + xgb.attr(env$bst, "best_iteration") <- best_iteration + xgb.attr(env$bst, "best_ntreelimit") <- best_ntreelimit + xgb.attr(env$bst, "best_score") <- best_score } else { env$basket$best_iteration <- best_iteration env$basket$best_ntreelimit <- best_ntreelimit @@ -412,11 +412,15 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, #' @param save_period save the model to disk after every #' \code{save_period} iterations; 0 means save the model at the end. #' @param save_name the name or path for the saved model file. +#' +#' Note that the format of the model being saved is determined by the file +#' extension specified here (see \link{xgb.save} for details about how it works). +#' #' It can contain a \code{\link[base]{sprintf}} formatting specifier #' to include the integer iteration number in the file name. -#' E.g., with \code{save_name} = 'xgboost_%04d.model', -#' the file saved at iteration 50 would be named "xgboost_0050.model". -#' +#' E.g., with \code{save_name} = 'xgboost_%04d.ubj', +#' the file saved at iteration 50 would be named "xgboost_0050.ubj". +#' @seealso \link{xgb.save} #' @details #' This callback function allows to save an xgb-model file, either periodically after each \code{save_period}'s or at the end. #' @@ -430,7 +434,7 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, #' \code{\link{callbacks}} #' #' @export -cb.save.model <- function(save_period = 0, save_name = "xgboost.model") { +cb.save.model <- function(save_period = 0, save_name = "xgboost.ubj") { if (save_period < 0) stop("'save_period' cannot be negative") @@ -440,8 +444,13 @@ cb.save.model <- function(save_period = 0, save_name = "xgboost.model") { stop("'save_model' callback requires the 'bst' booster object in its calling frame") if ((save_period > 0 && (env$iteration - env$begin_iteration) %% save_period == 0) || - (save_period == 0 && env$iteration == env$end_iteration)) - xgb.save(env$bst, sprintf(save_name, env$iteration)) + (save_period == 0 && env$iteration == env$end_iteration)) { + # Note: this throws a warning if the name doesn't have anything to format through 'sprintf' + suppressWarnings({ + save_name <- sprintf(save_name, env$iteration) + }) + xgb.save(env$bst, save_name) + } } attr(callback, 'call') <- match.call() attr(callback, 'name') <- 'cb.save.model' @@ -512,8 +521,7 @@ cb.cv.predict <- function(save_models = FALSE) { env$basket$pred <- pred if (save_models) { env$basket$models <- lapply(env$bst_folds, function(fd) { - xgb.attr(fd$bst, 'niter') <- env$end_iteration - 1 - xgb.Booster.complete(xgb.handleToBooster(handle = fd$bst, raw = NULL), saveraw = TRUE) + return(fd$bst) }) } } @@ -665,7 +673,7 @@ cb.gblinear.history <- function(sparse = FALSE) { } else { # xgb.cv: cf <- vector("list", length(env$bst_folds)) for (i in seq_along(env$bst_folds)) { - dmp <- xgb.dump(xgb.handleToBooster(handle = env$bst_folds[[i]]$bst, raw = NULL)) + dmp <- xgb.dump(env$bst_folds[[i]]$bst) cf[[i]] <- as.numeric(grep('(booster|bias|weigh)', dmp, invert = TRUE, value = TRUE)) if (sparse) cf[[i]] <- as(cf[[i]], "sparseVector") } @@ -685,14 +693,19 @@ cb.gblinear.history <- function(sparse = FALSE) { callback } -#' Extract gblinear coefficients history. -#' -#' A helper function to extract the matrix of linear coefficients' history +#' @title Extract gblinear coefficients history. +#' @description A helper function to extract the matrix of linear coefficients' history #' from a gblinear model created while using the \code{cb.gblinear.history()} #' callback. +#' @details Note that this is an R-specific function that relies on R attributes that +#' are not saved when using xgboost's own serialization functions like \link{xgb.load} +#' or \link{xgb.load.raw}. #' +#' In order for a serialized model to be accepted by tgis function, one must use R +#' serializers such as \link{saveRDS}. #' @param model either an \code{xgb.Booster} or a result of \code{xgb.cv()}, trained -#' using the \code{cb.gblinear.history()} callback. +#' using the \code{cb.gblinear.history()} callback, but \bold{not} a booster +#' loaded from \link{xgb.load} or \link{xgb.load.raw}. #' @param class_index zero-based class index to extract the coefficients for only that #' specific class in a multinomial multiclass model. When it is NULL, all the #' coefficients are returned. Has no effect in non-multiclass models. @@ -713,20 +726,18 @@ xgb.gblinear.history <- function(model, class_index = NULL) { stop("model must be an object of either xgb.Booster or xgb.cv.synchronous class") is_cv <- inherits(model, "xgb.cv.synchronous") - if (is.null(model[["callbacks"]]) || is.null(model$callbacks[["cb.gblinear.history"]])) + if (is_cv) { + callbacks <- model$callbacks + } else { + callbacks <- attributes(model)$callbacks + } + + if (is.null(callbacks) || is.null(callbacks$cb.gblinear.history)) stop("model must be trained while using the cb.gblinear.history() callback") if (!is_cv) { - # extract num_class & num_feat from the internal model - dmp <- xgb.dump(model) - if (length(dmp) < 2 || dmp[2] != "bias:") - stop("It does not appear to be a gblinear model") - dmp <- dmp[-c(1, 2)] - n <- which(dmp == 'weight:') - if (length(n) != 1) - stop("It does not appear to be a gblinear model") - num_class <- n - 1 - num_feat <- (length(dmp) - 4) / num_class + num_class <- xgb.num_class(model) + num_feat <- xgb.num_feature(model) } else { # in case of CV, the object is expected to have this info if (model$params$booster != "gblinear") @@ -742,7 +753,7 @@ xgb.gblinear.history <- function(model, class_index = NULL) { (class_index[1] < 0 || class_index[1] >= num_class)) stop("class_index has to be within [0,", num_class - 1, "]") - coef_path <- environment(model$callbacks$cb.gblinear.history)[["coefs"]] + coef_path <- environment(callbacks$cb.gblinear.history)[["coefs"]] if (!is.null(class_index) && num_class > 1) { coef_path <- if (is.list(coef_path)) { lapply(coef_path, diff --git a/R-package/R/utils.R b/R-package/R/utils.R index c011ab8ed..945d86132 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -148,19 +148,17 @@ check.custom.eval <- function(env = parent.frame()) { # Update a booster handle for an iteration with dtrain data -xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { - if (!identical(class(booster_handle), "xgb.Booster.handle")) { - stop("booster_handle must be of xgb.Booster.handle class") - } +xgb.iter.update <- function(bst, dtrain, iter, obj) { if (!inherits(dtrain, "xgb.DMatrix")) { stop("dtrain must be of xgb.DMatrix class") } + handle <- xgb.get.handle(bst) if (is.null(obj)) { - .Call(XGBoosterUpdateOneIter_R, booster_handle, as.integer(iter), dtrain) + .Call(XGBoosterUpdateOneIter_R, handle, as.integer(iter), dtrain) } else { pred <- predict( - booster_handle, + bst, dtrain, outputmargin = TRUE, training = TRUE, @@ -185,7 +183,7 @@ xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { } .Call( - XGBoosterTrainOneIter_R, booster_handle, dtrain, iter, grad, hess + XGBoosterTrainOneIter_R, handle, dtrain, iter, grad, hess ) } return(TRUE) @@ -195,23 +193,22 @@ xgb.iter.update <- function(booster_handle, dtrain, iter, obj) { # Evaluate one iteration. # Returns a named vector of evaluation metrics # with the names in a 'datasetname-metricname' format. -xgb.iter.eval <- function(booster_handle, watchlist, iter, feval) { - if (!identical(class(booster_handle), "xgb.Booster.handle")) - stop("class of booster_handle must be xgb.Booster.handle") +xgb.iter.eval <- function(bst, watchlist, iter, feval) { + handle <- xgb.get.handle(bst) if (length(watchlist) == 0) return(NULL) evnames <- names(watchlist) if (is.null(feval)) { - msg <- .Call(XGBoosterEvalOneIter_R, booster_handle, as.integer(iter), watchlist, as.list(evnames)) + msg <- .Call(XGBoosterEvalOneIter_R, handle, as.integer(iter), watchlist, as.list(evnames)) mat <- matrix(strsplit(msg, '\\s+|:')[[1]][-1], nrow = 2) res <- structure(as.numeric(mat[2, ]), names = mat[1, ]) } else { res <- sapply(seq_along(watchlist), function(j) { w <- watchlist[[j]] ## predict using all trees - preds <- predict(booster_handle, w, outputmargin = TRUE, iterationrange = c(1, 1)) + preds <- predict(bst, w, outputmargin = TRUE, iterationrange = c(1, 1)) eval_res <- feval(preds, w) out <- eval_res$value names(out) <- paste0(evnames[j], "-", eval_res$metric) @@ -352,16 +349,45 @@ xgb.createFolds <- function(y, k) { #' @name xgboost-deprecated NULL -#' Do not use \code{\link[base]{saveRDS}} or \code{\link[base]{save}} for long-term archival of -#' models. Instead, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}}. +#' @title Model Serialization and Compatibility +#' @description #' -#' It is a common practice to use the built-in \code{\link[base]{saveRDS}} function (or -#' \code{\link[base]{save}}) to persist R objects to the disk. While it is possible to persist -#' \code{xgb.Booster} objects using \code{\link[base]{saveRDS}}, it is not advisable to do so if -#' the model is to be accessed in the future. If you train a model with the current version of -#' XGBoost and persist it with \code{\link[base]{saveRDS}}, the model is not guaranteed to be -#' accessible in later releases of XGBoost. To ensure that your model can be accessed in future -#' releases of XGBoost, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}} instead. +#' When it comes to serializing XGBoost models, it's possible to use R serializers such as +#' \link{save} or \link{saveRDS} to serialize an XGBoost R model, but XGBoost also provides +#' its own serializers with better compatibility guarantees, which allow loading +#' said models in other language bindings of XGBoost. +#' +#' Note that an `xgb.Booster` object, outside of its core components, might also keep:\itemize{ +#' \item Additional model configuration (accessible through \link{xgb.config}), +#' which includes model fitting parameters like `max_depth` and runtime parameters like `nthread`. +#' These are not necessarily useful for prediction/importance/plotting. +#' \item Additional R-specific attributes - e.g. results of callbacks, such as evaluation logs, +#' which are kept as a `data.table` object, accessible through `attributes(model)$evaluation_log` +#' if present. +#' } +#' +#' The first one (configurations) does not have the same compatibility guarantees as +#' the model itself, including attributes that are set and accessed through \link{xgb.attributes} - that is, such configuration +#' might be lost after loading the booster in a different XGBoost version, regardless of the +#' serializer that was used. These are saved when using \link{saveRDS}, but will be discarded +#' if loaded into an incompatible XGBoost version. They are not saved when using XGBoost's +#' serializers from its public interface including \link{xgb.save} and \link{xgb.save.raw}. +#' +#' The second ones (R attributes) are not part of the standard XGBoost model structure, and thus are +#' not saved when using XGBoost's own serializers. These attributes are only used for informational +#' purposes, such as keeping track of evaluation metrics as the model was fit, or saving the R +#' call that produced the model, but are otherwise not used for prediction / importance / plotting / etc. +#' These R attributes are only preserved when using R's serializers. +#' +#' Note that XGBoost models in R starting from version `2.1.0` and onwards, and XGBoost models +#' before version `2.1.0`; have a very different R object structure and are incompatible with +#' each other. Hence, models that were saved with R serializers live `saveRDS` or `save` before +#' version `2.1.0` will not work with latter `xgboost` versions and vice versa. Be aware that +#' the structure of R model objects could in theory change again in the future, so XGBoost's serializers +#' should be preferred for long-term storage. +#' +#' Furthermore, note that using the package `qs` for serialization will require version 0.26 or +#' higher of said package, and will have the same compatibility restrictions as R serializers. #' #' @details #' Use \code{\link{xgb.save}} to save the XGBoost model as a stand-alone file. You may opt into @@ -374,9 +400,10 @@ NULL #' The \code{\link{xgb.save.raw}} function is useful if you'd like to persist the XGBoost model #' as part of another R object. #' -#' Note: Do not use \code{\link{xgb.serialize}} to store models long-term. It persists not only the -#' model but also internal configurations and parameters, and its format is not stable across -#' multiple XGBoost versions. Use \code{\link{xgb.serialize}} only for checkpointing. +#' Use \link{saveRDS} if you require the R-specific attributes that a booster might have, such +#' as evaluation logs, but note that future compatibility of such objects is outside XGBoost's +#' control as it relies on R's serialization format (see e.g. the details section in +#' \link{serialize} and \link{save} from base R). #' #' For more details and explanation about model persistence and archival, consult the page #' \url{https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html}. diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 9fdfc9dd6..cee7e9fc5 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -1,180 +1,85 @@ -# Construct an internal xgboost Booster and return a handle to it. +# Construct an internal xgboost Booster and get its current number of rounds. # internal utility function -xgb.Booster.handle <- function(params, cachelist, modelfile, handle) { +# Note: the number of rounds in the C booster gets reset to zero when changing +# key booster parameters like 'process_type=update', but in some cases, when +# replacing previous iterations, it needs to make a check that the new number +# of iterations doesn't exceed the previous ones, hence it keeps track of the +# current number of iterations before resetting the parameters in order to +# perform the check later on. +xgb.Booster <- function(params, cachelist, modelfile) { if (typeof(cachelist) != "list" || !all(vapply(cachelist, inherits, logical(1), what = 'xgb.DMatrix'))) { stop("cachelist must be a list of xgb.DMatrix objects") } ## Load existing model, dispatch for on disk model file and in memory buffer if (!is.null(modelfile)) { - if (typeof(modelfile) == "character") { + if (is.character(modelfile)) { ## A filename - handle <- .Call(XGBoosterCreate_R, cachelist) + bst <- .Call(XGBoosterCreate_R, cachelist) modelfile <- path.expand(modelfile) - .Call(XGBoosterLoadModel_R, handle, enc2utf8(modelfile[1])) - class(handle) <- "xgb.Booster.handle" + .Call(XGBoosterLoadModel_R, xgb.get.handle(bst), enc2utf8(modelfile[1])) + niter <- xgb.get.num.boosted.rounds(bst) if (length(params) > 0) { - xgb.parameters(handle) <- params + xgb.parameters(bst) <- params } - return(handle) - } else if (typeof(modelfile) == "raw") { + return(list(bst = bst, niter = niter)) + } else if (is.raw(modelfile)) { ## A memory buffer - bst <- xgb.unserialize(modelfile, handle) + bst <- xgb.load.raw(modelfile) + niter <- xgb.get.num.boosted.rounds(bst) xgb.parameters(bst) <- params - return(bst) + return(list(bst = bst, niter = niter)) } else if (inherits(modelfile, "xgb.Booster")) { ## A booster object - bst <- xgb.Booster.complete(modelfile, saveraw = TRUE) - bst <- xgb.unserialize(bst$raw) + bst <- .Call(XGDuplicate_R, modelfile) + niter <- xgb.get.num.boosted.rounds(bst) xgb.parameters(bst) <- params - return(bst) + return(list(bst = bst, niter = niter)) } else { stop("modelfile must be either character filename, or raw booster dump, or xgb.Booster object") } } ## Create new model - handle <- .Call(XGBoosterCreate_R, cachelist) - class(handle) <- "xgb.Booster.handle" + bst <- .Call(XGBoosterCreate_R, cachelist) if (length(params) > 0) { - xgb.parameters(handle) <- params + xgb.parameters(bst) <- params } - return(handle) + return(list(bst = bst, niter = 0L)) } -# Convert xgb.Booster.handle to xgb.Booster -# internal utility function -xgb.handleToBooster <- function(handle, raw) { - bst <- list(handle = handle, raw = raw) - class(bst) <- "xgb.Booster" - return(bst) -} - -# Check whether xgb.Booster.handle is null +# Check whether xgb.Booster handle is null # internal utility function is.null.handle <- function(handle) { if (is.null(handle)) return(TRUE) - if (!identical(class(handle), "xgb.Booster.handle")) - stop("argument type must be xgb.Booster.handle") + if (!inherits(handle, "externalptr")) + stop("argument type must be 'externalptr'") - if (.Call(XGCheckNullPtr_R, handle)) - return(TRUE) - - return(FALSE) + return(.Call(XGCheckNullPtr_R, handle)) } -# Return a verified to be valid handle out of either xgb.Booster.handle or -# xgb.Booster internal utility function +# Return a verified to be valid handle out of xgb.Booster +# internal utility function xgb.get.handle <- function(object) { if (inherits(object, "xgb.Booster")) { - handle <- object$handle - } else if (inherits(object, "xgb.Booster.handle")) { - handle <- object + handle <- object$ptr + if (is.null(handle) || !inherits(handle, "externalptr")) { + stop("'xgb.Booster' object is corrupted or is from an incompatible xgboost version.") + } } else { - stop("argument must be of either xgb.Booster or xgb.Booster.handle class") + stop("argument must be an 'xgb.Booster' object.") } if (is.null.handle(handle)) { - stop("invalid xgb.Booster.handle") + stop("invalid 'xgb.Booster' (blank 'externalptr').") } - handle -} - -#' Restore missing parts of an incomplete xgb.Booster object -#' -#' It attempts to complete an `xgb.Booster` object by restoring either its missing -#' raw model memory dump (when it has no `raw` data but its `xgb.Booster.handle` is valid) -#' or its missing internal handle (when its `xgb.Booster.handle` is not valid -#' but it has a raw Booster memory dump). -#' -#' @param object Object of class `xgb.Booster`. -#' @param saveraw A flag indicating whether to append `raw` Booster memory dump data -#' when it doesn't already exist. -#' -#' @details -#' -#' While this method is primarily for internal use, it might be useful in some practical situations. -#' -#' E.g., when an `xgb.Booster` model is saved as an R object and then is loaded as an R object, -#' its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods -#' should still work for such a model object since those methods would be using -#' `xgb.Booster.complete()` internally. However, one might find it to be more efficient to call the -#' `xgb.Booster.complete()` function explicitly once after loading a model as an R-object. -#' That would prevent further repeated implicit reconstruction of an internal booster model. -#' -#' @return -#' An object of `xgb.Booster` class. -#' -#' @examples -#' -#' data(agaricus.train, package = "xgboost") -#' -#' bst <- xgboost( -#' data = agaricus.train$data, -#' label = agaricus.train$label, -#' max_depth = 2, -#' eta = 1, -#' nthread = 2, -#' nrounds = 2, -#' objective = "binary:logistic" -#' ) -#' -#' fname <- file.path(tempdir(), "xgb_model.Rds") -#' saveRDS(bst, fname) -#' -#' # Warning: The resulting RDS file is only compatible with the current XGBoost version. -#' # Refer to the section titled "a-compatibility-note-for-saveRDS-save". -#' bst1 <- readRDS(fname) -#' # the handle is invalid: -#' print(bst1$handle) -#' -#' bst1 <- xgb.Booster.complete(bst1) -#' # now the handle points to a valid internal booster model: -#' print(bst1$handle) -#' -#' @export -xgb.Booster.complete <- function(object, saveraw = TRUE) { - if (!inherits(object, "xgb.Booster")) - stop("argument type must be xgb.Booster") - - if (is.null.handle(object$handle)) { - object$handle <- xgb.Booster.handle( - params = list(), - cachelist = list(), - modelfile = object$raw, - handle = object$handle - ) - } else { - if (is.null(object$raw) && saveraw) { - object$raw <- xgb.serialize(object$handle) - } - } - - attrs <- xgb.attributes(object) - if (!is.null(attrs$best_ntreelimit)) { - object$best_ntreelimit <- as.integer(attrs$best_ntreelimit) - } - if (!is.null(attrs$best_iteration)) { - ## Convert from 0 based back to 1 based. - object$best_iteration <- as.integer(attrs$best_iteration) + 1 - } - if (!is.null(attrs$best_score)) { - object$best_score <- as.numeric(attrs$best_score) - } - if (!is.null(attrs$best_msg)) { - object$best_msg <- attrs$best_msg - } - if (!is.null(attrs$niter)) { - object$niter <- as.integer(attrs$niter) - } - - return(object) + return(handle) } #' Predict method for XGBoost model #' #' Predicted values based on either xgboost model or model handle object. #' -#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param object Object of class `xgb.Booster`. #' @param newdata Takes `matrix`, `dgCMatrix`, `dgRMatrix`, `dsparseVector`, #' local data file, or `xgb.DMatrix`. #' For single-row predictions on sparse data, it is recommended to use the CSR format. @@ -358,27 +263,19 @@ xgb.Booster.complete <- function(object, saveraw = TRUE) { #' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) #' sum(pred5 != lb) / length(lb) #' -#' @rdname predict.xgb.Booster #' @export predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FALSE, ntreelimit = NULL, predleaf = FALSE, predcontrib = FALSE, approxcontrib = FALSE, predinteraction = FALSE, reshape = FALSE, training = FALSE, iterationrange = NULL, strict_shape = FALSE, ...) { - object <- xgb.Booster.complete(object, saveraw = FALSE) - if (!inherits(newdata, "xgb.DMatrix")) { - config <- jsonlite::fromJSON(xgb.config(object)) - nthread <- strtoi(config$learner$generic_param$nthread) + nthread <- xgb.nthread(object) newdata <- xgb.DMatrix( newdata, missing = missing, nthread = NVL(nthread, -1) ) } - if (!is.null(object[["feature_names"]]) && - !is.null(colnames(newdata)) && - !identical(object[["feature_names"]], colnames(newdata))) - stop("Feature names stored in `object` and `newdata` are different!") - if (NVL(object$params[['booster']], '') == 'gblinear' || is.null(ntreelimit)) + if (NVL(xgb.booster_type(object), '') == 'gblinear' || is.null(ntreelimit)) ntreelimit <- 0 if (ntreelimit != 0 && is.null(iterationrange)) { @@ -391,11 +288,12 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA ## both are specified, let libgxgboost throw an error } else { ## no limit is supplied, use best - if (is.null(object$best_iteration)) { + best_iteration <- xgb.best_iteration(object) + if (is.null(best_iteration)) { iterationrange <- c(0, 0) } else { ## We don't need to + 1 as R is 1-based index. - iterationrange <- c(0, as.integer(object$best_iteration)) + iterationrange <- c(0, as.integer(best_iteration)) } } ## Handle the 0 length values. @@ -438,7 +336,10 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA } predts <- .Call( - XGBoosterPredictFromDMatrix_R, object$handle, newdata, jsonlite::toJSON(args, auto_unbox = TRUE) + XGBoosterPredictFromDMatrix_R, + xgb.get.handle(object), + newdata, + jsonlite::toJSON(args, auto_unbox = TRUE) ) names(predts) <- c("shape", "results") shape <- predts$shape @@ -509,22 +410,12 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA return(arr) } -#' @rdname predict.xgb.Booster -#' @export -predict.xgb.Booster.handle <- function(object, ...) { - bst <- xgb.handleToBooster(handle = object, raw = NULL) - - ret <- predict(bst, ...) - return(ret) -} - - -#' Accessors for serializable attributes of a model +#' @title Accessors for serializable attributes of a model #' -#' These methods allow to manipulate the key-value attribute strings of an xgboost model. +#' @description These methods allow to manipulate the key-value attribute strings of an xgboost model. #' -#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' @param object Object of class `xgb.Booster`. \bold{Will be modified in-place} when assigning to it. #' @param name A non-empty character string specifying which attribute is to be accessed. #' @param value For `xgb.attr<-`, a value of an attribute; for `xgb.attributes<-`, #' it is a list (or an object coercible to a list) with the names of attributes to set @@ -546,16 +437,15 @@ predict.xgb.Booster.handle <- function(object, ...) { #' change the value of that parameter for a model. #' Use [xgb.parameters<-()] to set or change model parameters. #' -#' The attribute setters would usually work more efficiently for `xgb.Booster.handle` -#' than for `xgb.Booster`, since only just a handle (pointer) would need to be copied. -#' That would only matter if attributes need to be set many times. -#' Note, however, that when feeding a handle of an `xgb.Booster` object to the attribute setters, -#' the raw model cache of an `xgb.Booster` object would not be automatically updated, -#' and it would be the user's responsibility to call [xgb.serialize()] to update it. -#' #' The `xgb.attributes<-` setter either updates the existing or adds one or several attributes, #' but it doesn't delete the other existing attributes. #' +#' Important: since this modifies the booster's C object, semantics for assignment here +#' will differ from R's, as any object reference to the same booster will be modified +#' too, while assignment of R attributes through `attributes(model)$ <- ` +#' will follow the usual copy-on-write R semantics (see \link{xgb.copy.Booster} for an +#' example of these behaviors). +#' #' @return #' - `xgb.attr()` returns either a string value of an attribute #' or `NULL` if an attribute wasn't stored in a model. @@ -597,14 +487,25 @@ predict.xgb.Booster.handle <- function(object, ...) { xgb.attr <- function(object, name) { if (is.null(name) || nchar(as.character(name[1])) == 0) stop("invalid attribute name") handle <- xgb.get.handle(object) - .Call(XGBoosterGetAttr_R, handle, as.character(name[1])) + out <- .Call(XGBoosterGetAttr_R, handle, as.character(name[1])) + if (!NROW(out) || !nchar(out)) { + return(NULL) + } + if (!is.null(out)) { + if (name %in% c("best_iteration", "best_ntreelimit", "best_score")) { + out <- as.numeric(out) + } + } + return(out) } #' @rdname xgb.attr #' @export `xgb.attr<-` <- function(object, name, value) { - if (is.null(name) || nchar(as.character(name[1])) == 0) stop("invalid attribute name") + name <- as.character(name[1]) + if (!NROW(name) || !nchar(name)) stop("invalid attribute name") handle <- xgb.get.handle(object) + if (!is.null(value)) { # Coerce the elements to be scalar strings. # Q: should we warn user about non-scalar elements? @@ -614,11 +515,8 @@ xgb.attr <- function(object, name) { value <- as.character(value[1]) } } - .Call(XGBoosterSetAttr_R, handle, as.character(name[1]), value) - if (is(object, 'xgb.Booster') && !is.null(object$raw)) { - object$raw <- xgb.serialize(object$handle) - } - object + .Call(XGBoosterSetAttr_R, handle, name, value) + return(object) } #' @rdname xgb.attr @@ -626,12 +524,10 @@ xgb.attr <- function(object, name) { xgb.attributes <- function(object) { handle <- xgb.get.handle(object) attr_names <- .Call(XGBoosterGetAttrNames_R, handle) - if (is.null(attr_names)) return(NULL) - res <- lapply(attr_names, function(x) { - .Call(XGBoosterGetAttr_R, handle, x) - }) - names(res) <- attr_names - res + if (!NROW(attr_names)) return(list()) + out <- lapply(attr_names, function(name) xgb.attr(object, name)) + names(out) <- attr_names + return(out) } #' @rdname xgb.attr @@ -641,31 +537,21 @@ xgb.attributes <- function(object) { if (is.null(names(a)) || any(nchar(names(a)) == 0)) { stop("attribute names cannot be empty strings") } - # Coerce the elements to be scalar strings. - # Q: should we warn a user about non-scalar elements? - a <- lapply(a, function(x) { - if (is.null(x)) return(NULL) - if (is.numeric(x[1])) { - format(x[1], digits = 17) - } else { - as.character(x[1]) - } - }) - handle <- xgb.get.handle(object) for (i in seq_along(a)) { - .Call(XGBoosterSetAttr_R, handle, names(a[i]), a[[i]]) + xgb.attr(object, names(a[i])) <- a[[i]] } - if (is(object, 'xgb.Booster') && !is.null(object$raw)) { - object$raw <- xgb.serialize(object$handle) - } - object + return(object) } -#' Accessors for model parameters as JSON string -#' -#' @param object Object of class `xgb.Booster`. -#' @param value A JSON string. +#' @title Accessors for model parameters as JSON string +#' @details Note that assignment is performed in-place on the booster C object, which unlike assignment +#' of R attributes, doesn't follow typical copy-on-write semantics for assignment - i.e. all references +#' to the same booster will also get updated. #' +#' See \link{xgb.copy.Booster} for an example of this behavior. +#' @param object Object of class `xgb.Booster`. \bold{Will be modified in-place} when assigning to it. +#' @param value An R list. +#' @return `xgb.config` will return the parameters as an R list. #' @examples #' data(agaricus.train, package = "xgboost") #' @@ -690,31 +576,36 @@ xgb.attributes <- function(object) { #' @export xgb.config <- function(object) { handle <- xgb.get.handle(object) - .Call(XGBoosterSaveJsonConfig_R, handle) + return(jsonlite::fromJSON(.Call(XGBoosterSaveJsonConfig_R, handle))) } #' @rdname xgb.config #' @export `xgb.config<-` <- function(object, value) { handle <- xgb.get.handle(object) - .Call(XGBoosterLoadJsonConfig_R, handle, value) - object$raw <- NULL # force renew the raw buffer - object <- xgb.Booster.complete(object) - object + .Call( + XGBoosterLoadJsonConfig_R, + handle, + jsonlite::toJSON(value, auto_unbox = TRUE, null = "null") + ) + return(object) } -#' Accessors for model parameters +#' @title Accessors for model parameters +#' @description Only the setter for xgboost parameters is currently implemented. +#' @details Just like \link{xgb.attr}, this function will make in-place modifications +#' on the booster object which do not follow typical R assignment semantics - that is, +#' all references to the same booster will also be updated, unlike assingment of R +#' attributes which follow copy-on-write semantics. #' -#' Only the setter for xgboost parameters is currently implemented. +#' See \link{xgb.copy.Booster} for an example of this behavior. #' -#' @param object Object of class `xgb.Booster` or `xgb.Booster.handle`. +#' Be aware that setting parameters of a fitted booster related to training continuation / updates +#' will reset its number of rounds indicator to zero. +#' @param object Object of class `xgb.Booster`. \bold{Will be modified in-place}. #' @param value A list (or an object coercible to a list) with the names of parameters to set #' and the elements corresponding to parameter values. -#' -#' @details -#' Note that the setter would usually work more efficiently for `xgb.Booster.handle` -#' than for `xgb.Booster`, since only just a handle would need to be copied. -#' +#' @return The same booster `object`, which gets modified in-place. #' @examples #' data(agaricus.train, package = "xgboost") #' train <- agaricus.train @@ -751,28 +642,301 @@ xgb.config <- function(object) { for (i in seq_along(p)) { .Call(XGBoosterSetParam_R, handle, names(p[i]), p[[i]]) } - if (is(object, 'xgb.Booster') && !is.null(object$raw)) { - object$raw <- xgb.serialize(object$handle) + return(object) +} + +#' @rdname getinfo +#' @export +getinfo.xgb.Booster <- function(object, name) { + name <- as.character(head(name, 1L)) + allowed_fields <- c("feature_name", "feature_type") + if (!(name %in% allowed_fields)) { + stop("getinfo: name must be one of the following: ", paste(allowed_fields, collapse = ", ")) } - object + handle <- xgb.get.handle(object) + out <- .Call( + XGBoosterGetStrFeatureInfo_R, + handle, + name + ) + if (!NROW(out)) { + return(NULL) + } + return(out) +} + +#' @rdname getinfo +#' @export +setinfo.xgb.Booster <- function(object, name, info) { + name <- as.character(head(name, 1L)) + allowed_fields <- c("feature_name", "feature_type") + if (!(name %in% allowed_fields)) { + stop("setinfo: unknown info name ", name) + } + info <- as.character(info) + handle <- xgb.get.handle(object) + .Call( + XGBoosterSetStrFeatureInfo_R, + handle, + name, + info + ) + return(TRUE) +} + +#' @title Get number of boosting in a fitted booster +#' @param model A fitted `xgb.Booster` model. +#' @return The number of rounds saved in the model, as an integer. +#' @details Note that setting booster parameters related to training +#' continuation / updates through \link{xgb.parameters<-} will reset the +#' number of rounds to zero. +#' @export +xgb.get.num.boosted.rounds <- function(model) { + return(.Call(XGBoosterBoostedRounds_R, xgb.get.handle(model))) +} + +#' @title Get Features Names from Booster +#' @description Returns the feature / variable / column names from a fitted +#' booster object, which are set automatically during the call to \link{xgb.train} +#' from the DMatrix names, or which can be set manually through \link{setinfo}. +#' +#' If the object doesn't have feature names, will return `NULL`. +#' +#' It is equivalent to calling `getinfo(object, "feature_name")`. +#' @param object An `xgb.Booster` object. +#' @param ... Not used. +#' @export +variable.names.xgb.Booster <- function(object, ...) { + return(getinfo(object, "feature_name")) } -# Extract the number of trees in a model. -# TODO: either add a getter to C-interface, or simply set an 'ntree' attribute after each iteration. -# internal utility function xgb.ntree <- function(bst) { - length(grep('^booster', xgb.dump(bst))) + config <- xgb.config(bst) + out <- strtoi(config$learner$gradient_booster$gbtree_model_param$num_trees) + return(out) } +xgb.nthread <- function(bst) { + config <- xgb.config(bst) + out <- strtoi(config$learner$generic_param$nthread) + return(out) +} -#' Print xgb.Booster +xgb.booster_type <- function(bst) { + config <- xgb.config(bst) + out <- config$learner$learner_train_param$booster + return(out) +} + +xgb.num_class <- function(bst) { + config <- xgb.config(bst) + out <- strtoi(config$learner$learner_model_param$num_class) + return(out) +} + +xgb.feature_names <- function(bst) { + return(getinfo(bst, "feature_name")) +} + +xgb.feature_types <- function(bst) { + return(getinfo(bst, "feature_type")) +} + +xgb.num_feature <- function(bst) { + handle <- xgb.get.handle(bst) + return(.Call(XGBoosterGetNumFeature_R, handle)) +} + +xgb.best_iteration <- function(bst) { + out <- xgb.attr(bst, "best_iteration") + if (!NROW(out) || !nchar(out)) { + out <- NULL + } + return(out) +} + +#' @title Extract coefficients from linear booster +#' @description Extracts the coefficients from a 'gblinear' booster object, +#' as produced by \code{xgb.train} when using parameter `booster="gblinear"`. #' -#' Print information about `xgb.Booster`. +#' Note: this function will error out if passing a booster model +#' which is not of "gblinear" type. +#' @param object A fitted booster of 'gblinear' type. +#' @param ... Not used. +#' @return The extracted coefficients:\itemize{ +#' \item If there's only one coefficient per column in the data, will be returned as a +#' vector, potentially containing the feature names if available, with the intercept +#' as first column. +#' \item If there's more than one coefficient per column in the data (e.g. when using +#' `objective="multi:softmax"`), will be returned as a matrix with dimensions equal +#' to `[num_features, num_cols]`, with the intercepts as first row. Note that the column +#' (classes in multi-class classification) dimension will not be named. +#' } #' +#' The intercept returned here will include the 'base_score' parameter (unlike the 'bias' +#' or the last coefficient in the model dump, which doesn't have 'base_score' added to it), +#' hence one should get the same values from calling `predict(..., outputmargin = TRUE)` and +#' from performing a matrix multiplication with `model.matrix(~., ...)`. +#' +#' Be aware that the coefficients are obtained by first converting them to strings and +#' back, so there will always be some very small lose of precision compared to the actual +#' coefficients as used by \link{predict.xgb.Booster}. +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars[, 1] +#' x <- as.matrix(mtcars[, -1]) +#' dm <- xgb.DMatrix(data = x, label = y, nthread = 1) +#' params <- list(booster = "gblinear", nthread = 1) +#' model <- xgb.train(data = dm, params = params, nrounds = 2) +#' coef(model) +#' @export +coef.xgb.Booster <- function(object, ...) { + booster_type <- xgb.booster_type(object) + if (booster_type != "gblinear") { + stop("Coefficients are not defined for Booster type ", booster_type) + } + model_json <- jsonlite::fromJSON(rawToChar(xgb.save.raw(object, raw_format = "json"))) + base_score <- model_json$learner$learner_model_param$base_score + num_feature <- as.numeric(model_json$learner$learner_model_param$num_feature) + + weights <- model_json$learner$gradient_booster$model$weights + n_cols <- length(weights) / (num_feature + 1) + if (n_cols != floor(n_cols) || n_cols < 1) { + stop("Internal error: could not determine shape of coefficients.") + } + sep <- num_feature * n_cols + coefs <- weights[seq(1, sep)] + intercepts <- weights[seq(sep + 1, length(weights))] + intercepts <- intercepts + as.numeric(base_score) + + feature_names <- xgb.feature_names(object) + if (!NROW(feature_names)) { + # This mimics the default naming in R which names columns as "V1..N" + # when names are needed but not available + feature_names <- paste0("V", seq(1L, num_feature)) + } + feature_names <- c("(Intercept)", feature_names) + if (n_cols == 1L) { + out <- c(intercepts, coefs) + names(out) <- feature_names + } else { + coefs <- matrix(coefs, nrow = num_feature, byrow = TRUE) + dim(intercepts) <- c(1L, n_cols) + out <- rbind(intercepts, coefs) + row.names(out) <- feature_names + # TODO: if a class names attributes is added, + # should use those names here. + } + return(out) +} + +#' @title Deep-copies a Booster Object +#' @description Creates a deep copy of an 'xgb.Booster' object, such that the +#' C object pointer contained will be a different object, and hence functions +#' like \link{xgb.attr} will not affect the object from which it was copied. +#' @param model An 'xgb.Booster' object. +#' @return A deep copy of `model` - it will be identical in every way, but C-level +#' functions called on that copy will not affect the `model` variable. +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars$mpg +#' x <- mtcars[, -1] +#' dm <- xgb.DMatrix(x, label = y, nthread = 1) +#' model <- xgb.train( +#' data = dm, +#' params = list(nthread = 1), +#' nround = 3 +#' ) +#' +#' # Set an arbitrary attribute kept at the C level +#' xgb.attr(model, "my_attr") <- 100 +#' print(xgb.attr(model, "my_attr")) +#' +#' # Just assigning to a new variable will not create +#' # a deep copy - C object pointer is shared, and in-place +#' # modifications will affect both objects +#' model_shallow_copy <- model +#' xgb.attr(model_shallow_copy, "my_attr") <- 333 +#' # 'model' was also affected by this change: +#' print(xgb.attr(model, "my_attr")) +#' +#' model_deep_copy <- xgb.copy.Booster(model) +#' xgb.attr(model_deep_copy, "my_attr") <- 444 +#' # 'model' was NOT affected by this change +#' # (keeps previous value that was assigned before) +#' print(xgb.attr(model, "my_attr")) +#' +#' # Verify that the new object was actually modified +#' print(xgb.attr(model_deep_copy, "my_attr")) +#' @export +xgb.copy.Booster <- function(model) { + if (!inherits(model, "xgb.Booster")) { + stop("'model' must be an 'xgb.Booster' object.") + } + return(.Call(XGDuplicate_R, model)) +} + +#' @title Check if two boosters share the same C object +#' @description Checks whether two booster objects refer to the same underlying C object. +#' @details As booster objects (as returned by e.g. \link{xgb.train}) contain an R 'externalptr' +#' object, they don't follow typical copy-on-write semantics of other R objects - that is, if +#' one assigns a booster to a different variable and modifies that new variable through in-place +#' methods like \link{xgb.attr<-}, the modification will be applied to both the old and the new +#' variable, unlike typical R assignments which would only modify the latter. +#' +#' This function allows checking whether two booster objects share the same 'externalptr', +#' regardless of the R attributes that they might have. +#' +#' In order to duplicate a booster in such a way that the copy wouldn't share the same +#' 'externalptr', one can use function \link{xgb.copy.Booster}. +#' @param obj1 Booster model to compare with `obj2`. +#' @param obj2 Booster model to compare with `obj1`. +#' @return Either `TRUE` or `FALSE` according to whether the two boosters share +#' the underlying C object. +#' @seealso \link{xgb.copy.Booster} +#' @examples +#' library(xgboost) +#' data(mtcars) +#' y <- mtcars$mpg +#' x <- as.matrix(mtcars[, -1]) +#' model <- xgb.train( +#' params = list(nthread = 1), +#' data = xgb.DMatrix(x, label = y, nthread = 1), +#' nround = 3 +#' ) +#' +#' model_shallow_copy <- model +#' xgb.is.same.Booster(model, model_shallow_copy) # same C object +#' +#' model_deep_copy <- xgb.copy.Booster(model) +#' xgb.is.same.Booster(model, model_deep_copy) # different C objects +#' +#' # In-place assignments modify all references, +#' # but not full/deep copies of the booster +#' xgb.attr(model_shallow_copy, "my_attr") <- 111 +#' xgb.attr(model, "my_attr") # gets modified +#' xgb.attr(model_deep_copy, "my_attr") # doesn't get modified +#' @export +xgb.is.same.Booster <- function(obj1, obj2) { + if (!inherits(obj1, "xgb.Booster") || !inherits(obj2, "xgb.Booster")) { + stop("'xgb.is.same.Booster' is only applicable to 'xgb.Booster' objects.") + } + return( + .Call( + XGPointerEqComparison_R, + xgb.get.handle(obj1), + xgb.get.handle(obj2) + ) + ) +} + +#' @title Print xgb.Booster +#' @description Print information about `xgb.Booster`. #' @param x An `xgb.Booster` object. -#' @param verbose Whether to print detailed data (e.g., attribute values). -#' @param ... Not currently used. -#' +#' @param ... Not used. +#' @return The same `x` object, returned invisibly #' @examples #' data(agaricus.train, package = "xgboost") #' train <- agaricus.train @@ -790,79 +954,40 @@ xgb.ntree <- function(bst) { #' attr(bst, "myattr") <- "memo" #' #' print(bst) -#' print(bst, verbose = TRUE) #' #' @export -print.xgb.Booster <- function(x, verbose = FALSE, ...) { +print.xgb.Booster <- function(x, ...) { + # this lets it error out when the object comes from an earlier R xgboost version + handle <- xgb.get.handle(x) cat('##### xgb.Booster\n') - valid_handle <- !is.null.handle(x$handle) - if (!valid_handle) - cat("Handle is invalid! Suggest using xgb.Booster.complete\n") - - cat('raw: ') - if (!is.null(x$raw)) { - cat(format(object.size(x$raw), units = "auto"), '\n') - } else { - cat('NULL\n') - } - if (!is.null(x$call)) { + R_attrs <- attributes(x) + if (!is.null(R_attrs$call)) { cat('call:\n ') - print(x$call) + print(R_attrs$call) } - if (!is.null(x$params)) { - cat('params (as set within xgb.train):\n') - cat(' ', - paste(names(x$params), - paste0('"', unlist(x$params), '"'), - sep = ' = ', collapse = ', '), '\n', sep = '') - } - # TODO: need an interface to access all the xgboosts parameters + cat('# of features:', xgb.num_feature(x), '\n') + cat('# of rounds: ', xgb.get.num.boosted.rounds(x), '\n') - attrs <- character(0) - if (valid_handle) - attrs <- xgb.attributes(x) - if (length(attrs) > 0) { + attr_names <- .Call(XGBoosterGetAttrNames_R, handle) + if (NROW(attr_names)) { cat('xgb.attributes:\n') - if (verbose) { - cat(paste(paste0(' ', names(attrs)), - paste0('"', unlist(attrs), '"'), - sep = ' = ', collapse = '\n'), '\n', sep = '') - } else { - cat(' ', paste(names(attrs), collapse = ', '), '\n', sep = '') - } + cat(" ", paste(attr_names, collapse = ", "), "\n") } - if (!is.null(x$callbacks) && length(x$callbacks) > 0) { + if (!is.null(R_attrs$callbacks) && length(R_attrs$callbacks) > 0) { cat('callbacks:\n') - lapply(callback.calls(x$callbacks), function(x) { + lapply(callback.calls(R_attrs$callbacks), function(x) { cat(' ') print(x) }) } - if (!is.null(x$feature_names)) - cat('# of features:', length(x$feature_names), '\n') - - cat('niter: ', x$niter, '\n', sep = '') - # TODO: uncomment when faster xgb.ntree is implemented - #cat('ntree: ', xgb.ntree(x), '\n', sep='') - - for (n in setdiff(names(x), c('handle', 'raw', 'call', 'params', 'callbacks', - 'evaluation_log', 'niter', 'feature_names'))) { - if (is.atomic(x[[n]])) { - cat(n, ':', x[[n]], '\n', sep = ' ') - } else { - cat(n, ':\n\t', sep = ' ') - print(x[[n]]) - } - } - - if (!is.null(x$evaluation_log)) { + if (!is.null(R_attrs$evaluation_log)) { cat('evaluation_log:\n') - print(x$evaluation_log, row.names = FALSE, topn = 2) + print(R_attrs$evaluation_log, row.names = FALSE, topn = 2) } - invisible(x) + return(invisible(x)) } diff --git a/R-package/R/xgb.DMatrix.R b/R-package/R/xgb.DMatrix.R index f9a810c85..7c4c30bd3 100644 --- a/R-package/R/xgb.DMatrix.R +++ b/R-package/R/xgb.DMatrix.R @@ -335,14 +335,13 @@ dimnames.xgb.DMatrix <- function(x) { } -#' Get information of an xgb.DMatrix object -#' -#' Get information of an xgb.DMatrix object -#' @param object Object of class \code{xgb.DMatrix} +#' @title Get or set information of xgb.DMatrix and xgb.Booster objects +#' @param object Object of class \code{xgb.DMatrix} of `xgb.Booster`. #' @param name the name of the information field to get (see details) -#' +#' @return For `getinfo`, will return the requested field. For `setinfo`, will always return value `TRUE` +#' if it succeeds. #' @details -#' The \code{name} field can be one of the following: +#' The \code{name} field can be one of the following for `xgb.DMatrix`: #' #' \itemize{ #' \item \code{label} @@ -357,9 +356,17 @@ dimnames.xgb.DMatrix <- function(x) { #' } #' See the documentation for \link{xgb.DMatrix} for more information about these fields. #' +#' For `xgb.Booster`, can be one of the following: +#' \itemize{ +#' \item \code{feature_type} +#' \item \code{feature_name} +#' } +#' #' Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' #' for a DMatrix that had 'qid' assigned. #' +#' \bold{Important}: when calling `setinfo`, the objects are modified in-place. See +#' \link{xgb.copy.Booster} for an idea of this in-place assignment works. #' @examples #' data(agaricus.train, package='xgboost') #' dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) @@ -412,13 +419,7 @@ getinfo.xgb.DMatrix <- function(object, name) { return(ret) } - -#' Set information of an xgb.DMatrix object -#' -#' Set information of an xgb.DMatrix object -#' -#' @param object Object of class "xgb.DMatrix" -#' @param name the name of the field to get +#' @rdname getinfo #' @param info the specific field of information to set #' #' @details @@ -441,11 +442,10 @@ getinfo.xgb.DMatrix <- function(object, name) { #' setinfo(dtrain, 'label', 1-labels) #' labels2 <- getinfo(dtrain, 'label') #' stopifnot(all.equal(labels2, 1-labels)) -#' @rdname setinfo #' @export setinfo <- function(object, name, info) UseMethod("setinfo") -#' @rdname setinfo +#' @rdname getinfo #' @export setinfo.xgb.DMatrix <- function(object, name, info) { .internal.setinfo.xgb.DMatrix(object, name, info) diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index b0d8c4ebe..a960957ca 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -204,13 +204,13 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing dtrain <- slice(dall, unlist(folds[-k])) else dtrain <- slice(dall, train_folds[[k]]) - handle <- xgb.Booster.handle( + bst <- xgb.Booster( params = params, cachelist = list(dtrain, dtest), - modelfile = NULL, - handle = NULL + modelfile = NULL ) - list(dtrain = dtrain, bst = handle, watchlist = list(train = dtrain, test = dtest), index = folds[[k]]) + bst <- bst$bst + list(dtrain = dtrain, bst = bst, watchlist = list(train = dtrain, test = dtest), index = folds[[k]]) }) rm(dall) # a "basket" to collect some results from callbacks @@ -231,13 +231,13 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing msg <- lapply(bst_folds, function(fd) { xgb.iter.update( - booster_handle = fd$bst, + bst = fd$bst, dtrain = fd$dtrain, iter = iteration - 1, obj = obj ) xgb.iter.eval( - booster_handle = fd$bst, + bst = fd$bst, watchlist = fd$watchlist, iter = iteration - 1, feval = feval @@ -267,7 +267,7 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing ret <- c(ret, basket) class(ret) <- 'xgb.cv.synchronous' - invisible(ret) + return(invisible(ret)) } diff --git a/R-package/R/xgb.dump.R b/R-package/R/xgb.dump.R index 4421836d1..3a3d2c7dc 100644 --- a/R-package/R/xgb.dump.R +++ b/R-package/R/xgb.dump.R @@ -56,9 +56,13 @@ xgb.dump <- function(model, fname = NULL, fmap = "", with_stats = FALSE, if (!(is.null(fmap) || is.character(fmap))) stop("fmap: argument must be a character string (when provided)") - model <- xgb.Booster.complete(model) - model_dump <- .Call(XGBoosterDumpModel_R, model$handle, NVL(fmap, "")[1], as.integer(with_stats), - as.character(dump_format)) + model_dump <- .Call( + XGBoosterDumpModel_R, + xgb.get.handle(model), + NVL(fmap, "")[1], + as.integer(with_stats), + as.character(dump_format) + ) if (dump_format == "dot") { return(sapply(model_dump, function(x) gsub("^booster\\[\\d+\\]\\n", "\\1", x))) } diff --git a/R-package/R/xgb.importance.R b/R-package/R/xgb.importance.R index c94e1babb..44f2eb9b3 100644 --- a/R-package/R/xgb.importance.R +++ b/R-package/R/xgb.importance.R @@ -119,21 +119,21 @@ xgb.importance <- function(feature_names = NULL, model = NULL, trees = NULL, if (!(is.null(data) && is.null(label) && is.null(target))) warning("xgb.importance: parameters 'data', 'label' and 'target' are deprecated") - if (!inherits(model, "xgb.Booster")) - stop("model: must be an object of class xgb.Booster") - - if (is.null(feature_names) && !is.null(model$feature_names)) - feature_names <- model$feature_names + if (is.null(feature_names)) { + model_feature_names <- xgb.feature_names(model) + if (NROW(model_feature_names)) { + feature_names <- model_feature_names + } + } if (!(is.null(feature_names) || is.character(feature_names))) stop("feature_names: Has to be a character vector") - model <- xgb.Booster.complete(model) - config <- jsonlite::fromJSON(xgb.config(model)) - if (config$learner$gradient_booster$name == "gblinear") { + handle <- xgb.get.handle(model) + if (xgb.booster_type(model) == "gblinear") { args <- list(importance_type = "weight", feature_names = feature_names) results <- .Call( - XGBoosterFeatureScore_R, model$handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") + XGBoosterFeatureScore_R, handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") ) names(results) <- c("features", "shape", "weight") if (length(results$shape) == 2) { @@ -154,7 +154,7 @@ xgb.importance <- function(feature_names = NULL, model = NULL, trees = NULL, for (importance_type in c("weight", "total_gain", "total_cover")) { args <- list(importance_type = importance_type, feature_names = feature_names, tree_idx = trees) results <- .Call( - XGBoosterFeatureScore_R, model$handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") + XGBoosterFeatureScore_R, handle, jsonlite::toJSON(args, auto_unbox = TRUE, null = "null") ) names(results) <- c("features", "shape", importance_type) concatenated[ diff --git a/R-package/R/xgb.load.R b/R-package/R/xgb.load.R index 27844ed8c..7d1eab7e9 100644 --- a/R-package/R/xgb.load.R +++ b/R-package/R/xgb.load.R @@ -17,7 +17,7 @@ #' An object of \code{xgb.Booster} class. #' #' @seealso -#' \code{\link{xgb.save}}, \code{\link{xgb.Booster.complete}}. +#' \code{\link{xgb.save}} #' #' @examples #' data(agaricus.train, package='xgboost') @@ -46,25 +46,20 @@ xgb.load <- function(modelfile) { if (is.null(modelfile)) stop("xgb.load: modelfile cannot be NULL") - handle <- xgb.Booster.handle( + bst <- xgb.Booster( params = list(), cachelist = list(), - modelfile = modelfile, - handle = NULL + modelfile = modelfile ) + bst <- bst$bst # re-use modelfile if it is raw so we do not need to serialize if (typeof(modelfile) == "raw") { warning( paste( "The support for loading raw booster with `xgb.load` will be ", - "discontinued in upcoming release. Use `xgb.load.raw` or", - " `xgb.unserialize` instead. " + "discontinued in upcoming release. Use `xgb.load.raw` instead. " ) ) - bst <- xgb.handleToBooster(handle = handle, raw = modelfile) - } else { - bst <- xgb.handleToBooster(handle = handle, raw = NULL) } - bst <- xgb.Booster.complete(bst, saveraw = TRUE) return(bst) } diff --git a/R-package/R/xgb.load.raw.R b/R-package/R/xgb.load.raw.R index b159e9de1..73ac50dc6 100644 --- a/R-package/R/xgb.load.raw.R +++ b/R-package/R/xgb.load.raw.R @@ -3,21 +3,10 @@ #' User can generate raw memory buffer by calling xgb.save.raw #' #' @param buffer the buffer returned by xgb.save.raw -#' @param as_booster Return the loaded model as xgb.Booster instead of xgb.Booster.handle. -#' #' @export -xgb.load.raw <- function(buffer, as_booster = FALSE) { +xgb.load.raw <- function(buffer) { cachelist <- list() - handle <- .Call(XGBoosterCreate_R, cachelist) - .Call(XGBoosterLoadModelFromRaw_R, handle, buffer) - class(handle) <- "xgb.Booster.handle" - - if (as_booster) { - booster <- list(handle = handle, raw = NULL) - class(booster) <- "xgb.Booster" - booster <- xgb.Booster.complete(booster, saveraw = TRUE) - return(booster) - } else { - return(handle) - } + bst <- .Call(XGBoosterCreate_R, cachelist) + .Call(XGBoosterLoadModelFromRaw_R, xgb.get.handle(bst), buffer) + return(bst) } diff --git a/R-package/R/xgb.model.dt.tree.R b/R-package/R/xgb.model.dt.tree.R index 9a32d82a0..df0e672a9 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -2,8 +2,10 @@ #' #' Parse a boosted tree model text dump into a `data.table` structure. #' -#' @param feature_names Character vector used to overwrite the feature names -#' of the model. The default (`NULL`) uses the original feature names. +#' @param feature_names Character vector of feature names. If the model already +#' contains feature names, those will be used when \code{feature_names=NULL} (default value). +#' +#' Note that, if the model already contains feature names, it's \bold{not} possible to override them here. #' @param model Object of class `xgb.Booster`. #' @param text Character vector previously generated by the function [xgb.dump()] #' (called with parameter `with_stats = TRUE`). `text` takes precedence over `model`. @@ -54,8 +56,6 @@ #' objective = "binary:logistic" #' ) #' -#' (dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) -#' #' # This bst model already has feature_names stored with it, so those would be used when #' # feature_names is not set: #' (dt <- xgb.model.dt.tree(model = bst)) @@ -79,8 +79,15 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, " (or NULL if 'model' was provided).") } - if (is.null(feature_names) && !is.null(model) && !is.null(model$feature_names)) - feature_names <- model$feature_names + model_feature_names <- NULL + if (inherits(model, "xgb.Booster")) { + model_feature_names <- xgb.feature_names(model) + if (NROW(model_feature_names) && !is.null(feature_names)) { + stop("'model' contains feature names. Cannot override them.") + } + } + if (is.null(feature_names) && !is.null(model) && !is.null(model_feature_names)) + feature_names <- model_feature_names if (!(is.null(feature_names) || is.character(feature_names))) { stop("feature_names: must be a character vector") @@ -90,8 +97,10 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, stop("trees: must be a vector of integers.") } + from_text <- TRUE if (is.null(text)) { text <- xgb.dump(model = model, with_stats = TRUE) + from_text <- FALSE } if (length(text) < 2 || !any(grepl('leaf=(\\d+)', text))) { @@ -120,8 +129,28 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, td[, isLeaf := grepl("leaf", t, fixed = TRUE)] # parse branch lines - branch_rx <- paste0("f(\\d+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", - "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") + branch_rx_nonames <- paste0("f(\\d+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", + "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") + branch_rx_w_names <- paste0("\\d+:\\[(.+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", + "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") + text_has_feature_names <- FALSE + if (NROW(model_feature_names)) { + branch_rx <- branch_rx_w_names + text_has_feature_names <- TRUE + } else { + # Note: when passing a text dump, it might or might not have feature names, + # but that aspect is unknown from just the text attributes + branch_rx <- branch_rx_nonames + if (from_text) { + if (sum(grepl(branch_rx_w_names, text)) > sum(grepl(branch_rx_nonames, text))) { + branch_rx <- branch_rx_w_names + text_has_feature_names <- TRUE + } + } + } + if (text_has_feature_names && is.null(model) && !is.null(feature_names)) { + stop("'text' contains feature names. Cannot override them.") + } branch_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") td[ isLeaf == FALSE, @@ -144,10 +173,12 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, is_stump <- function() { return(length(td$Feature) == 1 && is.na(td$Feature)) } - if (!is.null(feature_names) && !is_stump()) { - if (length(feature_names) <= max(as.numeric(td$Feature), na.rm = TRUE)) - stop("feature_names has less elements than there are features used in the model") - td[isLeaf == FALSE, Feature := feature_names[as.numeric(Feature) + 1]] + if (!text_has_feature_names) { + if (!is.null(feature_names) && !is_stump()) { + if (length(feature_names) <= max(as.numeric(td$Feature), na.rm = TRUE)) + stop("feature_names has less elements than there are features used in the model") + td[isLeaf == FALSE, Feature := feature_names[as.numeric(Feature) + 1]] + } } # parse leaf lines diff --git a/R-package/R/xgb.plot.shap.R b/R-package/R/xgb.plot.shap.R index 35cf664ec..788a09539 100644 --- a/R-package/R/xgb.plot.shap.R +++ b/R-package/R/xgb.plot.shap.R @@ -303,7 +303,11 @@ xgb.shap.data <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, if (is.character(features) && is.null(colnames(data))) stop("either provide `data` with column names or provide `features` as column indices") - if (is.null(model$feature_names) && model$nfeatures != ncol(data)) + model_feature_names <- NULL + if (is.null(features) && !is.null(model)) { + model_feature_names <- xgb.feature_names(model) + } + if (is.null(model_feature_names) && xgb.num_feature(model) != ncol(data)) stop("if model has no feature_names, columns in `data` must match features in model") if (!is.null(subsample)) { @@ -332,7 +336,7 @@ xgb.shap.data <- function(data, shap_contrib = NULL, features = NULL, top_n = 1, } if (is.null(features)) { - if (!is.null(model$feature_names)) { + if (!is.null(model_feature_names)) { imp <- xgb.importance(model = model, trees = trees) } else { imp <- xgb.importance(model = model, trees = trees, feature_names = colnames(data)) diff --git a/R-package/R/xgb.save.R b/R-package/R/xgb.save.R index 474153bda..e1a61d196 100644 --- a/R-package/R/xgb.save.R +++ b/R-package/R/xgb.save.R @@ -1,12 +1,24 @@ #' Save xgboost model to binary file #' -#' Save xgboost model to a file in binary format. +#' Save xgboost model to a file in binary or JSON format. #' -#' @param model model object of \code{xgb.Booster} class. -#' @param fname name of the file to write. +#' @param model Model object of \code{xgb.Booster} class. +#' @param fname Name of the file to write. +#' +#' Note that the extension of this file name determined the serialization format to use:\itemize{ +#' \item Extension ".ubj" will use the universal binary JSON format (recommended). +#' This format uses binary types for e.g. floating point numbers, thereby preventing any loss +#' of precision when converting to a human-readable JSON text or similar. +#' \item Extension ".json" will use plain JSON, which is a human-readable format. +#' \item Extension ".deprecated" will use a \bold{deprecated} binary format. This format will +#' not be able to save attributes introduced after v1 of XGBoost, such as the "best_iteration" +#' attribute that boosters might keep, nor feature names or user-specifiec attributes. +#' \item If the format is not specified by passing one of the file extensions above, will +#' default to UBJ. +#' } #' #' @details -#' This methods allows to save a model in an xgboost-internal binary format which is universal +#' This methods allows to save a model in an xgboost-internal binary or text format which is universal #' among the various xgboost interfaces. In R, the saved model file could be read-in later #' using either the \code{\link{xgb.load}} function or the \code{xgb_model} parameter #' of \code{\link{xgb.train}}. @@ -14,13 +26,13 @@ #' Note: a model can also be saved as an R-object (e.g., by using \code{\link[base]{readRDS}} #' or \code{\link[base]{save}}). However, it would then only be compatible with R, and #' corresponding R-methods would need to be used to load it. Moreover, persisting the model with -#' \code{\link[base]{readRDS}} or \code{\link[base]{save}}) will cause compatibility problems in +#' \code{\link[base]{readRDS}} or \code{\link[base]{save}}) might cause compatibility problems in #' future versions of XGBoost. Consult \code{\link{a-compatibility-note-for-saveRDS-save}} to learn #' how to persist models in a future-proof way, i.e. to make the model accessible in future #' releases of XGBoost. #' #' @seealso -#' \code{\link{xgb.load}}, \code{\link{xgb.Booster.complete}}. +#' \code{\link{xgb.load}} #' #' @examples #' data(agaricus.train, package='xgboost') @@ -51,8 +63,7 @@ xgb.save <- function(model, fname) { stop("model must be xgb.Booster.", if (inherits(model, "xgb.DMatrix")) " Use xgb.DMatrix.save to save an xgb.DMatrix object." else "") } - model <- xgb.Booster.complete(model, saveraw = FALSE) fname <- path.expand(fname) - .Call(XGBoosterSaveModel_R, model$handle, enc2utf8(fname[1])) + .Call(XGBoosterSaveModel_R, xgb.get.handle(model), enc2utf8(fname[1])) return(TRUE) } diff --git a/R-package/R/xgb.save.raw.R b/R-package/R/xgb.save.raw.R index 63c06e071..c124a752b 100644 --- a/R-package/R/xgb.save.raw.R +++ b/R-package/R/xgb.save.raw.R @@ -11,8 +11,6 @@ #' \item \code{deprecated}: Encode the booster into old customized binary format. #' } #' -#' Right now the default is \code{deprecated} but will be changed to \code{ubj} in upcoming release. -#' #' @examples #' data(agaricus.train, package='xgboost') #' data(agaricus.test, package='xgboost') @@ -30,7 +28,7 @@ #' bst <- xgb.load.raw(raw) #' #' @export -xgb.save.raw <- function(model, raw_format = "deprecated") { +xgb.save.raw <- function(model, raw_format = "ubj") { handle <- xgb.get.handle(model) args <- list(format = raw_format) .Call(XGBoosterSaveModelToRaw_R, handle, jsonlite::toJSON(args, auto_unbox = TRUE)) diff --git a/R-package/R/xgb.serialize.R b/R-package/R/xgb.serialize.R deleted file mode 100644 index c20d2b51c..000000000 --- a/R-package/R/xgb.serialize.R +++ /dev/null @@ -1,21 +0,0 @@ -#' Serialize the booster instance into R's raw vector. The serialization method differs -#' from \code{\link{xgb.save.raw}} as the latter one saves only the model but not -#' parameters. This serialization format is not stable across different xgboost versions. -#' -#' @param booster the booster instance -#' -#' @examples -#' data(agaricus.train, package='xgboost') -#' data(agaricus.test, package='xgboost') -#' train <- agaricus.train -#' test <- agaricus.test -#' bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, -#' eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") -#' raw <- xgb.serialize(bst) -#' bst <- xgb.unserialize(raw) -#' -#' @export -xgb.serialize <- function(booster) { - handle <- xgb.get.handle(booster) - .Call(XGBoosterSerializeToBuffer_R, handle) -} diff --git a/R-package/R/xgb.train.R b/R-package/R/xgb.train.R index e20c1af3e..a313ed32f 100644 --- a/R-package/R/xgb.train.R +++ b/R-package/R/xgb.train.R @@ -152,6 +152,10 @@ #' See \code{\link{callbacks}}. Some of the callbacks are automatically created depending on the #' parameters' values. User can provide either existing or their own callback methods in order #' to customize the training process. +#' +#' Note that some callbacks might try to set an evaluation log - be aware that these evaluation logs +#' are kept as R attributes, and thus do not get saved when using non-R serializaters like +#' \link{xgb.save} (but are kept when using R serializers like \link{saveRDS}). #' @param ... other parameters to pass to \code{params}. #' @param label vector of response values. Should not be provided when data is #' a local data file name or an \code{xgb.DMatrix}. @@ -160,6 +164,9 @@ #' This parameter is only used when input is a dense matrix. #' @param weight a vector indicating the weight for each row of the input. #' +#' @return +#' An object of class \code{xgb.Booster}. +#' #' @details #' These are the training functions for \code{xgboost}. #' @@ -201,28 +208,20 @@ #' \item \code{cb.save.model}: when \code{save_period > 0} is set. #' } #' -#' @return -#' An object of class \code{xgb.Booster} with the following elements: -#' \itemize{ -#' \item \code{handle} a handle (pointer) to the xgboost model in memory. -#' \item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. -#' \item \code{niter} number of boosting iterations. -#' \item \code{evaluation_log} evaluation history stored as a \code{data.table} with the -#' first column corresponding to iteration number and the rest corresponding to evaluation -#' metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. -#' \item \code{call} a function call. -#' \item \code{params} parameters that were passed to the xgboost library. Note that it does not -#' capture parameters changed by the \code{\link{cb.reset.parameters}} callback. -#' \item \code{callbacks} callback functions that were either automatically assigned or -#' explicitly passed. -#' \item \code{best_iteration} iteration number with the best evaluation metric value -#' (only available with early stopping). -#' \item \code{best_score} the best evaluation metric value during early stopping. -#' (only available with early stopping). -#' \item \code{feature_names} names of the training dataset features -#' (only when column names were defined in training data). -#' \item \code{nfeatures} number of features in training data. -#' } +#' Note that objects of type `xgb.Booster` as returned by this function behave a bit differently +#' from typical R objects (it's an 'altrep' list class), and it makes a separation between +#' internal booster attributes (restricted to jsonifyable data), accessed through \link{xgb.attr} +#' and shared between interfaces through serialization functions like \link{xgb.save}; and +#' R-specific attributes, accessed through \link{attributes} and \link{attr}, which are otherwise +#' only used in the R interface, only kept when using R's serializers like \link{saveRDS}, and +#' not anyhow used by functions like \link{predict.xgb.Booster}. +#' +#' Be aware that one such R attribute that is automatically added is `params` - this attribute +#' is assigned from the `params` argument to this function, and is only meant to serve as a +#' reference for what went into the booster, but is not used in other methods that take a booster +#' object - so for example, changing the booster's configuration requires calling `xgb.config<-` +#' or 'xgb.parameters<-', while simply modifying `attributes(model)$params$<...>` will have no +#' effect elsewhere. #' #' @seealso #' \code{\link{callbacks}}, @@ -371,27 +370,31 @@ xgb.train <- function(params = list(), data, nrounds, watchlist = list(), # The tree updating process would need slightly different handling is_update <- NVL(params[['process_type']], '.') == 'update' + past_evaluation_log <- NULL + if (inherits(xgb_model, "xgb.Booster")) { + past_evaluation_log <- attributes(xgb_model)$evaluation_log + } + # Construct a booster (either a new one or load from xgb_model) - handle <- xgb.Booster.handle( + bst <- xgb.Booster( params = params, cachelist = append(watchlist, dtrain), - modelfile = xgb_model, - handle = NULL + modelfile = xgb_model + ) + niter_init <- bst$niter + bst <- bst$bst + .Call( + XGBoosterCopyInfoFromDMatrix_R, + xgb.get.handle(bst), + dtrain ) - bst <- xgb.handleToBooster(handle = handle, raw = NULL) # extract parameters that can affect the relationship b/w #trees and #iterations - num_class <- max(as.numeric(NVL(params[['num_class']], 1)), 1) - num_parallel_tree <- max(as.numeric(NVL(params[['num_parallel_tree']], 1)), 1) + # Note: it might look like these aren't used, but they need to be defined in this + # environment for the callbacks for work correctly. + num_class <- max(as.numeric(NVL(params[['num_class']], 1)), 1) # nolint + num_parallel_tree <- max(as.numeric(NVL(params[['num_parallel_tree']], 1)), 1) # nolint - # When the 'xgb_model' was set, find out how many boosting iterations it has - niter_init <- 0 - if (!is.null(xgb_model)) { - niter_init <- as.numeric(xgb.attr(bst, 'niter')) + 1 - if (length(niter_init) == 0) { - niter_init <- xgb.ntree(bst) %/% (num_parallel_tree * num_class) - } - } if (is_update && nrounds > niter_init) stop("nrounds cannot be larger than ", niter_init, " (nrounds of xgb_model)") @@ -405,7 +408,7 @@ xgb.train <- function(params = list(), data, nrounds, watchlist = list(), for (f in cb$pre_iter) f() xgb.iter.update( - booster_handle = bst$handle, + bst = bst, dtrain = dtrain, iter = iteration - 1, obj = obj @@ -413,46 +416,43 @@ xgb.train <- function(params = list(), data, nrounds, watchlist = list(), if (length(watchlist) > 0) { bst_evaluation <- xgb.iter.eval( # nolint: object_usage_linter - booster_handle = bst$handle, + bst = bst, watchlist = watchlist, iter = iteration - 1, feval = feval ) } - xgb.attr(bst$handle, 'niter') <- iteration - 1 - for (f in cb$post_iter) f() if (stop_condition) break } for (f in cb$finalize) f(finalize = TRUE) - bst <- xgb.Booster.complete(bst, saveraw = TRUE) - - # store the total number of boosting iterations - bst$niter <- end_iteration - # store the evaluation results - if (length(evaluation_log) > 0 && - nrow(evaluation_log) > 0) { + keep_evaluation_log <- FALSE + if (length(evaluation_log) > 0 && nrow(evaluation_log) > 0) { + keep_evaluation_log <- TRUE # include the previous compatible history when available if (inherits(xgb_model, 'xgb.Booster') && !is_update && - !is.null(xgb_model$evaluation_log) && + !is.null(past_evaluation_log) && isTRUE(all.equal(colnames(evaluation_log), - colnames(xgb_model$evaluation_log)))) { - evaluation_log <- rbindlist(list(xgb_model$evaluation_log, evaluation_log)) + colnames(past_evaluation_log)))) { + evaluation_log <- rbindlist(list(past_evaluation_log, evaluation_log)) } - bst$evaluation_log <- evaluation_log } - bst$call <- match.call() - bst$params <- params - bst$callbacks <- callbacks - if (!is.null(colnames(dtrain))) - bst$feature_names <- colnames(dtrain) - bst$nfeatures <- ncol(dtrain) + extra_attrs <- list( + call = match.call(), + params = params, + callbacks = callbacks + ) + if (keep_evaluation_log) { + extra_attrs$evaluation_log <- evaluation_log + } + curr_attrs <- attributes(bst) + attributes(bst) <- c(curr_attrs, extra_attrs) return(bst) } diff --git a/R-package/R/xgb.unserialize.R b/R-package/R/xgb.unserialize.R deleted file mode 100644 index 291d3e7da..000000000 --- a/R-package/R/xgb.unserialize.R +++ /dev/null @@ -1,41 +0,0 @@ -#' Load the instance back from \code{\link{xgb.serialize}} -#' -#' @param buffer the buffer containing booster instance saved by \code{\link{xgb.serialize}} -#' @param handle An \code{xgb.Booster.handle} object which will be overwritten with -#' the new deserialized object. Must be a null handle (e.g. when loading the model through -#' `readRDS`). If not provided, a new handle will be created. -#' @return An \code{xgb.Booster.handle} object. -#' -#' @export -xgb.unserialize <- function(buffer, handle = NULL) { - cachelist <- list() - if (is.null(handle)) { - handle <- .Call(XGBoosterCreate_R, cachelist) - } else { - if (!is.null.handle(handle)) - stop("'handle' is not null/empty. Cannot overwrite existing handle.") - .Call(XGBoosterCreateInEmptyObj_R, cachelist, handle) - } - tryCatch( - .Call(XGBoosterUnserializeFromBuffer_R, handle, buffer), - error = function(e) { - error_msg <- conditionMessage(e) - m <- regexec("(src[\\\\/]learner.cc:[0-9]+): Check failed: (header == serialisation_header_)", - error_msg, perl = TRUE) - groups <- regmatches(error_msg, m)[[1]] - if (length(groups) == 3) { - warning(paste("The model had been generated by XGBoost version 1.0.0 or earlier and was ", - "loaded from a RDS file. We strongly ADVISE AGAINST using saveRDS() ", - "function, to ensure that your model can be read in current and upcoming ", - "XGBoost releases. Please use xgb.save() instead to preserve models for the ", - "long term. For more details and explanation, see ", - "https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html", - sep = "")) - .Call(XGBoosterLoadModelFromRaw_R, handle, buffer) - } else { - stop(e) - } - }) - class(handle) <- "xgb.Booster.handle" - return(handle) -} diff --git a/R-package/R/xgboost.R b/R-package/R/xgboost.R index af6253a72..170aa5ffd 100644 --- a/R-package/R/xgboost.R +++ b/R-package/R/xgboost.R @@ -100,8 +100,10 @@ NULL #' @importFrom jsonlite toJSON #' @importFrom methods new #' @importFrom utils object.size str tail +#' @importFrom stats coef #' @importFrom stats predict #' @importFrom stats median +#' @importFrom stats variable.names #' @importFrom utils head #' @importFrom graphics barplot #' @importFrom graphics lines diff --git a/R-package/demo/00Index b/R-package/demo/00Index index 13ffdc6b6..fa09fa900 100644 --- a/R-package/demo/00Index +++ b/R-package/demo/00Index @@ -1,5 +1,4 @@ basic_walkthrough Basic feature walkthrough -caret_wrapper Use xgboost to train in caret library custom_objective Customize loss function, and evaluation metric boost_from_prediction Boosting from existing prediction predict_first_ntree Predicting using first n trees diff --git a/R-package/demo/README.md b/R-package/demo/README.md index 0a07a7426..99a492230 100644 --- a/R-package/demo/README.md +++ b/R-package/demo/README.md @@ -1,7 +1,6 @@ XGBoost R Feature Walkthrough ==== * [Basic walkthrough of wrappers](basic_walkthrough.R) -* [Train a xgboost model from caret library](caret_wrapper.R) * [Customize loss function, and evaluation metric](custom_objective.R) * [Boosting from existing prediction](boost_from_prediction.R) * [Predicting using first n trees](predict_first_ntree.R) diff --git a/R-package/demo/caret_wrapper.R b/R-package/demo/caret_wrapper.R deleted file mode 100644 index 0e63f27ce..000000000 --- a/R-package/demo/caret_wrapper.R +++ /dev/null @@ -1,44 +0,0 @@ -# install development version of caret library that contains xgboost models -require(caret) -require(xgboost) -require(data.table) -require(vcd) -require(e1071) - -# Load Arthritis dataset in memory. -data(Arthritis) -# Create a copy of the dataset with data.table package -# (data.table is 100% compliant with R dataframe but its syntax is a lot more consistent -# and its performance are really good). -df <- data.table(Arthritis, keep.rownames = FALSE) - -# Let's add some new categorical features to see if it helps. -# Of course these feature are highly correlated to the Age feature. -# Usually it's not a good thing in ML, but Tree algorithms (including boosted trees) are able to select the best features, -# even in case of highly correlated features. -# For the first feature we create groups of age by rounding the real age. -# Note that we transform it to factor (categorical data) so the algorithm treat them as independant values. -df[, AgeDiscret := as.factor(round(Age / 10, 0))] - -# Here is an even stronger simplification of the real age with an arbitrary split at 30 years old. -# I choose this value based on nothing. -# We will see later if simplifying the information based on arbitrary values is a good strategy -# (I am sure you already have an idea of how well it will work!). -df[, AgeCat := as.factor(ifelse(Age > 30, "Old", "Young"))] - -# We remove ID as there is nothing to learn from this feature (it will just add some noise as the dataset is small). -df[, ID := NULL] - -#-------------Basic Training using XGBoost in caret Library----------------- -# Set up control parameters for caret::train -# Here we use 10-fold cross-validation, repeating twice, and using random search for tuning hyper-parameters. -fitControl <- trainControl(method = "repeatedcv", number = 10, repeats = 2, search = "random") -# train a xgbTree model using caret::train -model <- train(factor(Improved) ~ ., data = df, method = "xgbTree", trControl = fitControl) - -# Instead of tree for our boosters, you can also fit a linear regression or logistic regression model -# using xgbLinear -# model <- train(factor(Improved)~., data = df, method = "xgbLinear", trControl = fitControl) - -# See model results -print(model) diff --git a/R-package/demo/predict_leaf_indices.R b/R-package/demo/predict_leaf_indices.R index 5ef9372ac..21b6fa71d 100644 --- a/R-package/demo/predict_leaf_indices.R +++ b/R-package/demo/predict_leaf_indices.R @@ -27,7 +27,7 @@ head(pred_with_leaf) create.new.tree.features <- function(model, original.features) { pred_with_leaf <- predict(model, original.features, predleaf = TRUE) cols <- list() - for (i in 1:model$niter) { + for (i in 1:xgb.get.num.boosted.rounds(model)) { # max is not the real max but it s not important for the purpose of adding features leaf.id <- sort(unique(pred_with_leaf[, i])) cols[[i]] <- factor(x = pred_with_leaf[, i], level = leaf.id) diff --git a/R-package/demo/runall.R b/R-package/demo/runall.R index 7a35e247b..ab1822a5b 100644 --- a/R-package/demo/runall.R +++ b/R-package/demo/runall.R @@ -9,6 +9,5 @@ demo(create_sparse_matrix, package = 'xgboost') demo(predict_leaf_indices, package = 'xgboost') demo(early_stopping, package = 'xgboost') demo(poisson_regression, package = 'xgboost') -demo(caret_wrapper, package = 'xgboost') demo(tweedie_regression, package = 'xgboost') #demo(gpu_accelerated, package = 'xgboost') # can only run when built with GPU support diff --git a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd index a8f46547e..860f4f0c1 100644 --- a/R-package/man/a-compatibility-note-for-saveRDS-save.Rd +++ b/R-package/man/a-compatibility-note-for-saveRDS-save.Rd @@ -2,16 +2,44 @@ % Please edit documentation in R/utils.R \name{a-compatibility-note-for-saveRDS-save} \alias{a-compatibility-note-for-saveRDS-save} -\title{Do not use \code{\link[base]{saveRDS}} or \code{\link[base]{save}} for long-term archival of -models. Instead, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}}.} +\title{Model Serialization and Compatibility} \description{ -It is a common practice to use the built-in \code{\link[base]{saveRDS}} function (or -\code{\link[base]{save}}) to persist R objects to the disk. While it is possible to persist -\code{xgb.Booster} objects using \code{\link[base]{saveRDS}}, it is not advisable to do so if -the model is to be accessed in the future. If you train a model with the current version of -XGBoost and persist it with \code{\link[base]{saveRDS}}, the model is not guaranteed to be -accessible in later releases of XGBoost. To ensure that your model can be accessed in future -releases of XGBoost, use \code{\link{xgb.save}} or \code{\link{xgb.save.raw}} instead. +When it comes to serializing XGBoost models, it's possible to use R serializers such as +\link{save} or \link{saveRDS} to serialize an XGBoost R model, but XGBoost also provides +its own serializers with better compatibility guarantees, which allow loading +said models in other language bindings of XGBoost. + +Note that an \code{xgb.Booster} object, outside of its core components, might also keep:\itemize{ +\item Additional model configuration (accessible through \link{xgb.config}), +which includes model fitting parameters like \code{max_depth} and runtime parameters like \code{nthread}. +These are not necessarily useful for prediction/importance/plotting. +\item Additional R-specific attributes - e.g. results of callbacks, such as evaluation logs, +which are kept as a \code{data.table} object, accessible through \code{attributes(model)$evaluation_log} +if present. +} + +The first one (configurations) does not have the same compatibility guarantees as +the model itself, including attributes that are set and accessed through \link{xgb.attributes} - that is, such configuration +might be lost after loading the booster in a different XGBoost version, regardless of the +serializer that was used. These are saved when using \link{saveRDS}, but will be discarded +if loaded into an incompatible XGBoost version. They are not saved when using XGBoost's +serializers from its public interface including \link{xgb.save} and \link{xgb.save.raw}. + +The second ones (R attributes) are not part of the standard XGBoost model structure, and thus are +not saved when using XGBoost's own serializers. These attributes are only used for informational +purposes, such as keeping track of evaluation metrics as the model was fit, or saving the R +call that produced the model, but are otherwise not used for prediction / importance / plotting / etc. +These R attributes are only preserved when using R's serializers. + +Note that XGBoost models in R starting from version \verb{2.1.0} and onwards, and XGBoost models +before version \verb{2.1.0}; have a very different R object structure and are incompatible with +each other. Hence, models that were saved with R serializers live \code{saveRDS} or \code{save} before +version \verb{2.1.0} will not work with latter \code{xgboost} versions and vice versa. Be aware that +the structure of R model objects could in theory change again in the future, so XGBoost's serializers +should be preferred for long-term storage. + +Furthermore, note that using the package \code{qs} for serialization will require version 0.26 or +higher of said package, and will have the same compatibility restrictions as R serializers. } \details{ Use \code{\link{xgb.save}} to save the XGBoost model as a stand-alone file. You may opt into @@ -24,9 +52,10 @@ re-construct the corresponding model. To read the model back, use \code{\link{xg The \code{\link{xgb.save.raw}} function is useful if you'd like to persist the XGBoost model as part of another R object. -Note: Do not use \code{\link{xgb.serialize}} to store models long-term. It persists not only the -model but also internal configurations and parameters, and its format is not stable across -multiple XGBoost versions. Use \code{\link{xgb.serialize}} only for checkpointing. +Use \link{saveRDS} if you require the R-specific attributes that a booster might have, such +as evaluation logs, but note that future compatibility of such objects is outside XGBoost's +control as it relies on R's serialization format (see e.g. the details section in +\link{serialize} and \link{save} from base R). For more details and explanation about model persistence and archival, consult the page \url{https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html}. diff --git a/R-package/man/cb.save.model.Rd b/R-package/man/cb.save.model.Rd index 584fd69b7..7701ad990 100644 --- a/R-package/man/cb.save.model.Rd +++ b/R-package/man/cb.save.model.Rd @@ -4,17 +4,22 @@ \alias{cb.save.model} \title{Callback closure for saving a model file.} \usage{ -cb.save.model(save_period = 0, save_name = "xgboost.model") +cb.save.model(save_period = 0, save_name = "xgboost.ubj") } \arguments{ \item{save_period}{save the model to disk after every \code{save_period} iterations; 0 means save the model at the end.} \item{save_name}{the name or path for the saved model file. -It can contain a \code{\link[base]{sprintf}} formatting specifier -to include the integer iteration number in the file name. -E.g., with \code{save_name} = 'xgboost_\%04d.model', -the file saved at iteration 50 would be named "xgboost_0050.model".} + +\if{html}{\out{
}}\preformatted{ Note that the format of the model being saved is determined by the file + extension specified here (see \link{xgb.save} for details about how it works). + + It can contain a \code{\link[base]{sprintf}} formatting specifier + to include the integer iteration number in the file name. + E.g., with \code{save_name} = 'xgboost_\%04d.ubj', + the file saved at iteration 50 would be named "xgboost_0050.ubj". +}\if{html}{\out{
}}} } \description{ Callback closure for saving a model file. @@ -29,5 +34,7 @@ Callback function expects the following values to be set in its calling frame: \code{end_iteration}. } \seealso{ +\link{xgb.save} + \code{\link{callbacks}} } diff --git a/R-package/man/coef.xgb.Booster.Rd b/R-package/man/coef.xgb.Booster.Rd new file mode 100644 index 000000000..7318077bb --- /dev/null +++ b/R-package/man/coef.xgb.Booster.Rd @@ -0,0 +1,50 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{coef.xgb.Booster} +\alias{coef.xgb.Booster} +\title{Extract coefficients from linear booster} +\usage{ +\method{coef}{xgb.Booster}(object, ...) +} +\arguments{ +\item{object}{A fitted booster of 'gblinear' type.} + +\item{...}{Not used.} +} +\value{ +The extracted coefficients:\itemize{ +\item If there's only one coefficient per column in the data, will be returned as a +vector, potentially containing the feature names if available, with the intercept +as first column. +\item If there's more than one coefficient per column in the data (e.g. when using +\code{objective="multi:softmax"}), will be returned as a matrix with dimensions equal +to \verb{[num_features, num_cols]}, with the intercepts as first row. Note that the column +(classes in multi-class classification) dimension will not be named. +} + +The intercept returned here will include the 'base_score' parameter (unlike the 'bias' +or the last coefficient in the model dump, which doesn't have 'base_score' added to it), +hence one should get the same values from calling \code{predict(..., outputmargin = TRUE)} and +from performing a matrix multiplication with \code{model.matrix(~., ...)}. + +Be aware that the coefficients are obtained by first converting them to strings and +back, so there will always be some very small lose of precision compared to the actual +coefficients as used by \link{predict.xgb.Booster}. +} +\description{ +Extracts the coefficients from a 'gblinear' booster object, +as produced by \code{xgb.train} when using parameter \code{booster="gblinear"}. + +Note: this function will error out if passing a booster model +which is not of "gblinear" type. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars[, 1] +x <- as.matrix(mtcars[, -1]) +dm <- xgb.DMatrix(data = x, label = y, nthread = 1) +params <- list(booster = "gblinear", nthread = 1) +model <- xgb.train(data = dm, params = params, nrounds = 2) +coef(model) +} diff --git a/R-package/man/getinfo.Rd b/R-package/man/getinfo.Rd index cb552886b..7cc0d6ecb 100644 --- a/R-package/man/getinfo.Rd +++ b/R-package/man/getinfo.Rd @@ -1,24 +1,42 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.DMatrix.R -\name{getinfo} +% Please edit documentation in R/xgb.Booster.R, R/xgb.DMatrix.R +\name{getinfo.xgb.Booster} +\alias{getinfo.xgb.Booster} +\alias{setinfo.xgb.Booster} \alias{getinfo} \alias{getinfo.xgb.DMatrix} -\title{Get information of an xgb.DMatrix object} +\alias{setinfo} +\alias{setinfo.xgb.DMatrix} +\title{Get or set information of xgb.DMatrix and xgb.Booster objects} \usage{ +\method{getinfo}{xgb.Booster}(object, name) + +\method{setinfo}{xgb.Booster}(object, name, info) + getinfo(object, name) \method{getinfo}{xgb.DMatrix}(object, name) + +setinfo(object, name, info) + +\method{setinfo}{xgb.DMatrix}(object, name, info) } \arguments{ -\item{object}{Object of class \code{xgb.DMatrix}} +\item{object}{Object of class \code{xgb.DMatrix} of \code{xgb.Booster}.} \item{name}{the name of the information field to get (see details)} + +\item{info}{the specific field of information to set} +} +\value{ +For \code{getinfo}, will return the requested field. For \code{setinfo}, will always return value \code{TRUE} +if it succeeds. } \description{ -Get information of an xgb.DMatrix object +Get or set information of xgb.DMatrix and xgb.Booster objects } \details{ -The \code{name} field can be one of the following: +The \code{name} field can be one of the following for \code{xgb.DMatrix}: \itemize{ \item \code{label} @@ -33,8 +51,28 @@ The \code{name} field can be one of the following: } See the documentation for \link{xgb.DMatrix} for more information about these fields. +For \code{xgb.Booster}, can be one of the following: +\itemize{ +\item \code{feature_type} +\item \code{feature_name} +} + Note that, while 'qid' cannot be retrieved, it's possible to get the equivalent 'group' for a DMatrix that had 'qid' assigned. + +\bold{Important}: when calling \code{setinfo}, the objects are modified in-place. See +\link{xgb.copy.Booster} for an idea of this in-place assignment works. + +See the documentation for \link{xgb.DMatrix} for possible fields that can be set +(which correspond to arguments in that function). + +Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} +but \bold{aren't} allowed here:\itemize{ +\item data +\item missing +\item silent +\item nthread +} } \examples{ data(agaricus.train, package='xgboost') @@ -45,4 +83,11 @@ setinfo(dtrain, 'label', 1-labels) labels2 <- getinfo(dtrain, 'label') stopifnot(all(labels2 == 1-labels)) +data(agaricus.train, package='xgboost') +dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) + +labels <- getinfo(dtrain, 'label') +setinfo(dtrain, 'label', 1-labels) +labels2 <- getinfo(dtrain, 'label') +stopifnot(all.equal(labels2, 1-labels)) } diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index f47cab321..66194c64f 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -2,7 +2,6 @@ % Please edit documentation in R/xgb.Booster.R \name{predict.xgb.Booster} \alias{predict.xgb.Booster} -\alias{predict.xgb.Booster.handle} \title{Predict method for XGBoost model} \usage{ \method{predict}{xgb.Booster}( @@ -21,11 +20,9 @@ strict_shape = FALSE, ... ) - -\method{predict}{xgb.Booster.handle}(object, ...) } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} +\item{object}{Object of class \code{xgb.Booster}.} \item{newdata}{Takes \code{matrix}, \code{dgCMatrix}, \code{dgRMatrix}, \code{dsparseVector}, local data file, or \code{xgb.DMatrix}. diff --git a/R-package/man/print.xgb.Booster.Rd b/R-package/man/print.xgb.Booster.Rd index 4d09bb5ec..9a783efaf 100644 --- a/R-package/man/print.xgb.Booster.Rd +++ b/R-package/man/print.xgb.Booster.Rd @@ -4,14 +4,15 @@ \alias{print.xgb.Booster} \title{Print xgb.Booster} \usage{ -\method{print}{xgb.Booster}(x, verbose = FALSE, ...) +\method{print}{xgb.Booster}(x, ...) } \arguments{ \item{x}{An \code{xgb.Booster} object.} -\item{verbose}{Whether to print detailed data (e.g., attribute values).} - -\item{...}{Not currently used.} +\item{...}{Not used.} +} +\value{ +The same \code{x} object, returned invisibly } \description{ Print information about \code{xgb.Booster}. @@ -33,6 +34,5 @@ bst <- xgboost( attr(bst, "myattr") <- "memo" print(bst) -print(bst, verbose = TRUE) } diff --git a/R-package/man/setinfo.Rd b/R-package/man/setinfo.Rd deleted file mode 100644 index 549fc9b20..000000000 --- a/R-package/man/setinfo.Rd +++ /dev/null @@ -1,42 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.DMatrix.R -\name{setinfo} -\alias{setinfo} -\alias{setinfo.xgb.DMatrix} -\title{Set information of an xgb.DMatrix object} -\usage{ -setinfo(object, name, info) - -\method{setinfo}{xgb.DMatrix}(object, name, info) -} -\arguments{ -\item{object}{Object of class "xgb.DMatrix"} - -\item{name}{the name of the field to get} - -\item{info}{the specific field of information to set} -} -\description{ -Set information of an xgb.DMatrix object -} -\details{ -See the documentation for \link{xgb.DMatrix} for possible fields that can be set -(which correspond to arguments in that function). - -Note that the following fields are allowed in the construction of an \code{xgb.DMatrix} -but \bold{aren't} allowed here:\itemize{ -\item data -\item missing -\item silent -\item nthread -} -} -\examples{ -data(agaricus.train, package='xgboost') -dtrain <- with(agaricus.train, xgb.DMatrix(data, label = label, nthread = 2)) - -labels <- getinfo(dtrain, 'label') -setinfo(dtrain, 'label', 1-labels) -labels2 <- getinfo(dtrain, 'label') -stopifnot(all.equal(labels2, 1-labels)) -} diff --git a/R-package/man/variable.names.xgb.Booster.Rd b/R-package/man/variable.names.xgb.Booster.Rd new file mode 100644 index 000000000..aec09751d --- /dev/null +++ b/R-package/man/variable.names.xgb.Booster.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{variable.names.xgb.Booster} +\alias{variable.names.xgb.Booster} +\title{Get Features Names from Booster} +\usage{ +\method{variable.names}{xgb.Booster}(object, ...) +} +\arguments{ +\item{object}{An \code{xgb.Booster} object.} + +\item{...}{Not used.} +} +\description{ +Returns the feature / variable / column names from a fitted +booster object, which are set automatically during the call to \link{xgb.train} +from the DMatrix names, or which can be set manually through \link{setinfo}. + +If the object doesn't have feature names, will return \code{NULL}. + +It is equivalent to calling \code{getinfo(object, "feature_name")}. +} diff --git a/R-package/man/xgb.Booster.complete.Rd b/R-package/man/xgb.Booster.complete.Rd deleted file mode 100644 index 102224a8f..000000000 --- a/R-package/man/xgb.Booster.complete.Rd +++ /dev/null @@ -1,61 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.Booster.R -\name{xgb.Booster.complete} -\alias{xgb.Booster.complete} -\title{Restore missing parts of an incomplete xgb.Booster object} -\usage{ -xgb.Booster.complete(object, saveraw = TRUE) -} -\arguments{ -\item{object}{Object of class \code{xgb.Booster}.} - -\item{saveraw}{A flag indicating whether to append \code{raw} Booster memory dump data -when it doesn't already exist.} -} -\value{ -An object of \code{xgb.Booster} class. -} -\description{ -It attempts to complete an \code{xgb.Booster} object by restoring either its missing -raw model memory dump (when it has no \code{raw} data but its \code{xgb.Booster.handle} is valid) -or its missing internal handle (when its \code{xgb.Booster.handle} is not valid -but it has a raw Booster memory dump). -} -\details{ -While this method is primarily for internal use, it might be useful in some practical situations. - -E.g., when an \code{xgb.Booster} model is saved as an R object and then is loaded as an R object, -its handle (pointer) to an internal xgboost model would be invalid. The majority of xgboost methods -should still work for such a model object since those methods would be using -\code{xgb.Booster.complete()} internally. However, one might find it to be more efficient to call the -\code{xgb.Booster.complete()} function explicitly once after loading a model as an R-object. -That would prevent further repeated implicit reconstruction of an internal booster model. -} -\examples{ - -data(agaricus.train, package = "xgboost") - -bst <- xgboost( - data = agaricus.train$data, - label = agaricus.train$label, - max_depth = 2, - eta = 1, - nthread = 2, - nrounds = 2, - objective = "binary:logistic" -) - -fname <- file.path(tempdir(), "xgb_model.Rds") -saveRDS(bst, fname) - -# Warning: The resulting RDS file is only compatible with the current XGBoost version. -# Refer to the section titled "a-compatibility-note-for-saveRDS-save". -bst1 <- readRDS(fname) -# the handle is invalid: -print(bst1$handle) - -bst1 <- xgb.Booster.complete(bst1) -# now the handle points to a valid internal booster model: -print(bst1$handle) - -} diff --git a/R-package/man/xgb.attr.Rd b/R-package/man/xgb.attr.Rd index 2aab62812..8038a2048 100644 --- a/R-package/man/xgb.attr.Rd +++ b/R-package/man/xgb.attr.Rd @@ -16,7 +16,7 @@ xgb.attributes(object) xgb.attributes(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} +\item{object}{Object of class \code{xgb.Booster}. \bold{Will be modified in-place} when assigning to it.} \item{name}{A non-empty character string specifying which attribute is to be accessed.} @@ -51,15 +51,14 @@ Also, setting an attribute that has the same name as one of xgboost's parameters change the value of that parameter for a model. Use \code{\link[=xgb.parameters<-]{xgb.parameters<-()}} to set or change model parameters. -The attribute setters would usually work more efficiently for \code{xgb.Booster.handle} -than for \code{xgb.Booster}, since only just a handle (pointer) would need to be copied. -That would only matter if attributes need to be set many times. -Note, however, that when feeding a handle of an \code{xgb.Booster} object to the attribute setters, -the raw model cache of an \code{xgb.Booster} object would not be automatically updated, -and it would be the user's responsibility to call \code{\link[=xgb.serialize]{xgb.serialize()}} to update it. - The \verb{xgb.attributes<-} setter either updates the existing or adds one or several attributes, but it doesn't delete the other existing attributes. + +Important: since this modifies the booster's C object, semantics for assignment here +will differ from R's, as any object reference to the same booster will be modified +too, while assignment of R attributes through \verb{attributes(model)$ <- } +will follow the usual copy-on-write R semantics (see \link{xgb.copy.Booster} for an +example of these behaviors). } \examples{ data(agaricus.train, package = "xgboost") diff --git a/R-package/man/xgb.config.Rd b/R-package/man/xgb.config.Rd index 83040b877..1ab810644 100644 --- a/R-package/man/xgb.config.Rd +++ b/R-package/man/xgb.config.Rd @@ -10,13 +10,23 @@ xgb.config(object) xgb.config(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster}.} +\item{object}{Object of class \code{xgb.Booster}. \bold{Will be modified in-place} when assigning to it.} -\item{value}{A JSON string.} +\item{value}{An R list.} +} +\value{ +\code{xgb.config} will return the parameters as an R list. } \description{ Accessors for model parameters as JSON string } +\details{ +Note that assignment is performed in-place on the booster C object, which unlike assignment +of R attributes, doesn't follow typical copy-on-write semantics for assignment - i.e. all references +to the same booster will also get updated. + +See \link{xgb.copy.Booster} for an example of this behavior. +} \examples{ data(agaricus.train, package = "xgboost") diff --git a/R-package/man/xgb.copy.Booster.Rd b/R-package/man/xgb.copy.Booster.Rd new file mode 100644 index 000000000..8426d039e --- /dev/null +++ b/R-package/man/xgb.copy.Booster.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{xgb.copy.Booster} +\alias{xgb.copy.Booster} +\title{Deep-copies a Booster Object} +\usage{ +xgb.copy.Booster(model) +} +\arguments{ +\item{model}{An 'xgb.Booster' object.} +} +\value{ +A deep copy of \code{model} - it will be identical in every way, but C-level +functions called on that copy will not affect the \code{model} variable. +} +\description{ +Creates a deep copy of an 'xgb.Booster' object, such that the +C object pointer contained will be a different object, and hence functions +like \link{xgb.attr} will not affect the object from which it was copied. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars$mpg +x <- mtcars[, -1] +dm <- xgb.DMatrix(x, label = y, nthread = 1) +model <- xgb.train( + data = dm, + params = list(nthread = 1), + nround = 3 +) + +# Set an arbitrary attribute kept at the C level +xgb.attr(model, "my_attr") <- 100 +print(xgb.attr(model, "my_attr")) + +# Just assigning to a new variable will not create +# a deep copy - C object pointer is shared, and in-place +# modifications will affect both objects +model_shallow_copy <- model +xgb.attr(model_shallow_copy, "my_attr") <- 333 +# 'model' was also affected by this change: +print(xgb.attr(model, "my_attr")) + +model_deep_copy <- xgb.copy.Booster(model) +xgb.attr(model_deep_copy, "my_attr") <- 444 +# 'model' was NOT affected by this change +# (keeps previous value that was assigned before) +print(xgb.attr(model, "my_attr")) + +# Verify that the new object was actually modified +print(xgb.attr(model_deep_copy, "my_attr")) +} diff --git a/R-package/man/xgb.gblinear.history.Rd b/R-package/man/xgb.gblinear.history.Rd index bc8d46747..103be16f1 100644 --- a/R-package/man/xgb.gblinear.history.Rd +++ b/R-package/man/xgb.gblinear.history.Rd @@ -8,7 +8,8 @@ xgb.gblinear.history(model, class_index = NULL) } \arguments{ \item{model}{either an \code{xgb.Booster} or a result of \code{xgb.cv()}, trained -using the \code{cb.gblinear.history()} callback.} +using the \code{cb.gblinear.history()} callback, but \bold{not} a booster +loaded from \link{xgb.load} or \link{xgb.load.raw}.} \item{class_index}{zero-based class index to extract the coefficients for only that specific class in a multinomial multiclass model. When it is NULL, all the @@ -27,3 +28,11 @@ A helper function to extract the matrix of linear coefficients' history from a gblinear model created while using the \code{cb.gblinear.history()} callback. } +\details{ +Note that this is an R-specific function that relies on R attributes that +are not saved when using xgboost's own serialization functions like \link{xgb.load} +or \link{xgb.load.raw}. + +In order for a serialized model to be accepted by tgis function, one must use R +serializers such as \link{saveRDS}. +} diff --git a/R-package/man/xgb.get.num.boosted.rounds.Rd b/R-package/man/xgb.get.num.boosted.rounds.Rd new file mode 100644 index 000000000..74c94d95b --- /dev/null +++ b/R-package/man/xgb.get.num.boosted.rounds.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{xgb.get.num.boosted.rounds} +\alias{xgb.get.num.boosted.rounds} +\title{Get number of boosting in a fitted booster} +\usage{ +xgb.get.num.boosted.rounds(model) +} +\arguments{ +\item{model}{A fitted \code{xgb.Booster} model.} +} +\value{ +The number of rounds saved in the model, as an integer. +} +\description{ +Get number of boosting in a fitted booster +} +\details{ +Note that setting booster parameters related to training +continuation / updates through \link{xgb.parameters<-} will reset the +number of rounds to zero. +} diff --git a/R-package/man/xgb.is.same.Booster.Rd b/R-package/man/xgb.is.same.Booster.Rd new file mode 100644 index 000000000..d2a2f4d17 --- /dev/null +++ b/R-package/man/xgb.is.same.Booster.Rd @@ -0,0 +1,59 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{xgb.is.same.Booster} +\alias{xgb.is.same.Booster} +\title{Check if two boosters share the same C object} +\usage{ +xgb.is.same.Booster(obj1, obj2) +} +\arguments{ +\item{obj1}{Booster model to compare with \code{obj2}.} + +\item{obj2}{Booster model to compare with \code{obj1}.} +} +\value{ +Either \code{TRUE} or \code{FALSE} according to whether the two boosters share +the underlying C object. +} +\description{ +Checks whether two booster objects refer to the same underlying C object. +} +\details{ +As booster objects (as returned by e.g. \link{xgb.train}) contain an R 'externalptr' +object, they don't follow typical copy-on-write semantics of other R objects - that is, if +one assigns a booster to a different variable and modifies that new variable through in-place +methods like \link{xgb.attr<-}, the modification will be applied to both the old and the new +variable, unlike typical R assignments which would only modify the latter. + +This function allows checking whether two booster objects share the same 'externalptr', +regardless of the R attributes that they might have. + +In order to duplicate a booster in such a way that the copy wouldn't share the same +'externalptr', one can use function \link{xgb.copy.Booster}. +} +\examples{ +library(xgboost) +data(mtcars) +y <- mtcars$mpg +x <- as.matrix(mtcars[, -1]) +model <- xgb.train( + params = list(nthread = 1), + data = xgb.DMatrix(x, label = y, nthread = 1), + nround = 3 +) + +model_shallow_copy <- model +xgb.is.same.Booster(model, model_shallow_copy) # same C object + +model_deep_copy <- xgb.copy.Booster(model) +xgb.is.same.Booster(model, model_deep_copy) # different C objects + +# In-place assignments modify all references, +# but not full/deep copies of the booster +xgb.attr(model_shallow_copy, "my_attr") <- 111 +xgb.attr(model, "my_attr") # gets modified +xgb.attr(model_deep_copy, "my_attr") # doesn't get modified +} +\seealso{ +\link{xgb.copy.Booster} +} diff --git a/R-package/man/xgb.load.Rd b/R-package/man/xgb.load.Rd index e6a2d6cdd..1a6873171 100644 --- a/R-package/man/xgb.load.Rd +++ b/R-package/man/xgb.load.Rd @@ -48,5 +48,5 @@ xgb.save(bst, fname) bst <- xgb.load(fname) } \seealso{ -\code{\link{xgb.save}}, \code{\link{xgb.Booster.complete}}. +\code{\link{xgb.save}} } diff --git a/R-package/man/xgb.load.raw.Rd b/R-package/man/xgb.load.raw.Rd index 0af890e69..f0248cd9e 100644 --- a/R-package/man/xgb.load.raw.Rd +++ b/R-package/man/xgb.load.raw.Rd @@ -4,12 +4,10 @@ \alias{xgb.load.raw} \title{Load serialised xgboost model from R's raw vector} \usage{ -xgb.load.raw(buffer, as_booster = FALSE) +xgb.load.raw(buffer) } \arguments{ \item{buffer}{the buffer returned by xgb.save.raw} - -\item{as_booster}{Return the loaded model as xgb.Booster instead of xgb.Booster.handle.} } \description{ User can generate raw memory buffer by calling xgb.save.raw diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index 330998ab8..e63bd4b10 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -14,8 +14,11 @@ xgb.model.dt.tree( ) } \arguments{ -\item{feature_names}{Character vector used to overwrite the feature names -of the model. The default (\code{NULL}) uses the original feature names.} +\item{feature_names}{Character vector of feature names. If the model already +contains feature names, those will be used when \code{feature_names=NULL} (default value). + +\if{html}{\out{
}}\preformatted{ Note that, if the model already contains feature names, it's \\bold\{not\} possible to override them here. +}\if{html}{\out{
}}} \item{model}{Object of class \code{xgb.Booster}.} @@ -76,8 +79,6 @@ bst <- xgboost( objective = "binary:logistic" ) -(dt <- xgb.model.dt.tree(colnames(agaricus.train$data), bst)) - # This bst model already has feature_names stored with it, so those would be used when # feature_names is not set: (dt <- xgb.model.dt.tree(model = bst)) diff --git a/R-package/man/xgb.parameters.Rd b/R-package/man/xgb.parameters.Rd index 5305afa51..8d5044cab 100644 --- a/R-package/man/xgb.parameters.Rd +++ b/R-package/man/xgb.parameters.Rd @@ -7,17 +7,27 @@ xgb.parameters(object) <- value } \arguments{ -\item{object}{Object of class \code{xgb.Booster} or \code{xgb.Booster.handle}.} +\item{object}{Object of class \code{xgb.Booster}. \bold{Will be modified in-place}.} \item{value}{A list (or an object coercible to a list) with the names of parameters to set and the elements corresponding to parameter values.} } +\value{ +The same booster \code{object}, which gets modified in-place. +} \description{ Only the setter for xgboost parameters is currently implemented. } \details{ -Note that the setter would usually work more efficiently for \code{xgb.Booster.handle} -than for \code{xgb.Booster}, since only just a handle would need to be copied. +Just like \link{xgb.attr}, this function will make in-place modifications +on the booster object which do not follow typical R assignment semantics - that is, +all references to the same booster will also be updated, unlike assingment of R +attributes which follow copy-on-write semantics. + +See \link{xgb.copy.Booster} for an example of this behavior. + +Be aware that setting parameters of a fitted booster related to training continuation / updates +will reset its number of rounds indicator to zero. } \examples{ data(agaricus.train, package = "xgboost") diff --git a/R-package/man/xgb.save.Rd b/R-package/man/xgb.save.Rd index ee4b799c5..0db80a120 100644 --- a/R-package/man/xgb.save.Rd +++ b/R-package/man/xgb.save.Rd @@ -7,15 +7,27 @@ xgb.save(model, fname) } \arguments{ -\item{model}{model object of \code{xgb.Booster} class.} +\item{model}{Model object of \code{xgb.Booster} class.} -\item{fname}{name of the file to write.} +\item{fname}{Name of the file to write. + +Note that the extension of this file name determined the serialization format to use:\itemize{ +\item Extension ".ubj" will use the universal binary JSON format (recommended). +This format uses binary types for e.g. floating point numbers, thereby preventing any loss +of precision when converting to a human-readable JSON text or similar. +\item Extension ".json" will use plain JSON, which is a human-readable format. +\item Extension ".deprecated" will use a \bold{deprecated} binary format. This format will +not be able to save attributes introduced after v1 of XGBoost, such as the "best_iteration" +attribute that boosters might keep, nor feature names or user-specifiec attributes. +\item If the format is not specified by passing one of the file extensions above, will +default to UBJ. +}} } \description{ -Save xgboost model to a file in binary format. +Save xgboost model to a file in binary or JSON format. } \details{ -This methods allows to save a model in an xgboost-internal binary format which is universal +This methods allows to save a model in an xgboost-internal binary or text format which is universal among the various xgboost interfaces. In R, the saved model file could be read-in later using either the \code{\link{xgb.load}} function or the \code{xgb_model} parameter of \code{\link{xgb.train}}. @@ -23,7 +35,7 @@ of \code{\link{xgb.train}}. Note: a model can also be saved as an R-object (e.g., by using \code{\link[base]{readRDS}} or \code{\link[base]{save}}). However, it would then only be compatible with R, and corresponding R-methods would need to be used to load it. Moreover, persisting the model with -\code{\link[base]{readRDS}} or \code{\link[base]{save}}) will cause compatibility problems in +\code{\link[base]{readRDS}} or \code{\link[base]{save}}) might cause compatibility problems in future versions of XGBoost. Consult \code{\link{a-compatibility-note-for-saveRDS-save}} to learn how to persist models in a future-proof way, i.e. to make the model accessible in future releases of XGBoost. @@ -51,5 +63,5 @@ xgb.save(bst, fname) bst <- xgb.load(fname) } \seealso{ -\code{\link{xgb.load}}, \code{\link{xgb.Booster.complete}}. +\code{\link{xgb.load}} } diff --git a/R-package/man/xgb.save.raw.Rd b/R-package/man/xgb.save.raw.Rd index 498272148..15400bb14 100644 --- a/R-package/man/xgb.save.raw.Rd +++ b/R-package/man/xgb.save.raw.Rd @@ -5,7 +5,7 @@ \title{Save xgboost model to R's raw vector, user can call xgb.load.raw to load the model back from raw vector} \usage{ -xgb.save.raw(model, raw_format = "deprecated") +xgb.save.raw(model, raw_format = "ubj") } \arguments{ \item{model}{the model object.} @@ -15,9 +15,7 @@ xgb.save.raw(model, raw_format = "deprecated") \item \code{json}: Encode the booster into JSON text document. \item \code{ubj}: Encode the booster into Universal Binary JSON. \item \code{deprecated}: Encode the booster into old customized binary format. -} - -Right now the default is \code{deprecated} but will be changed to \code{ubj} in upcoming release.} +}} } \description{ Save xgboost model from xgboost or xgb.train diff --git a/R-package/man/xgb.serialize.Rd b/R-package/man/xgb.serialize.Rd deleted file mode 100644 index 5bf4205f8..000000000 --- a/R-package/man/xgb.serialize.Rd +++ /dev/null @@ -1,29 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.serialize.R -\name{xgb.serialize} -\alias{xgb.serialize} -\title{Serialize the booster instance into R's raw vector. The serialization method differs -from \code{\link{xgb.save.raw}} as the latter one saves only the model but not -parameters. This serialization format is not stable across different xgboost versions.} -\usage{ -xgb.serialize(booster) -} -\arguments{ -\item{booster}{the booster instance} -} -\description{ -Serialize the booster instance into R's raw vector. The serialization method differs -from \code{\link{xgb.save.raw}} as the latter one saves only the model but not -parameters. This serialization format is not stable across different xgboost versions. -} -\examples{ -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') -train <- agaricus.train -test <- agaricus.test -bst <- xgb.train(data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, - eta = 1, nthread = 2, nrounds = 2,objective = "binary:logistic") -raw <- xgb.serialize(bst) -bst <- xgb.unserialize(raw) - -} diff --git a/R-package/man/xgb.train.Rd b/R-package/man/xgb.train.Rd index b2eaff27c..0421b9c4a 100644 --- a/R-package/man/xgb.train.Rd +++ b/R-package/man/xgb.train.Rd @@ -205,7 +205,12 @@ file with a previously saved model.} \item{callbacks}{a list of callback functions to perform various task during boosting. See \code{\link{callbacks}}. Some of the callbacks are automatically created depending on the parameters' values. User can provide either existing or their own callback methods in order -to customize the training process.} +to customize the training process. + +\if{html}{\out{
}}\preformatted{ Note that some callbacks might try to set an evaluation log - be aware that these evaluation logs + are kept as R attributes, and thus do not get saved when using non-R serializaters like + \link{xgb.save} (but are kept when using R serializers like \link{saveRDS}). +}\if{html}{\out{
}}} \item{...}{other parameters to pass to \code{params}.} @@ -219,27 +224,7 @@ This parameter is only used when input is a dense matrix.} \item{weight}{a vector indicating the weight for each row of the input.} } \value{ -An object of class \code{xgb.Booster} with the following elements: -\itemize{ -\item \code{handle} a handle (pointer) to the xgboost model in memory. -\item \code{raw} a cached memory dump of the xgboost model saved as R's \code{raw} type. -\item \code{niter} number of boosting iterations. -\item \code{evaluation_log} evaluation history stored as a \code{data.table} with the -first column corresponding to iteration number and the rest corresponding to evaluation -metrics' values. It is created by the \code{\link{cb.evaluation.log}} callback. -\item \code{call} a function call. -\item \code{params} parameters that were passed to the xgboost library. Note that it does not -capture parameters changed by the \code{\link{cb.reset.parameters}} callback. -\item \code{callbacks} callback functions that were either automatically assigned or -explicitly passed. -\item \code{best_iteration} iteration number with the best evaluation metric value -(only available with early stopping). -\item \code{best_score} the best evaluation metric value during early stopping. -(only available with early stopping). -\item \code{feature_names} names of the training dataset features -(only when column names were defined in training data). -\item \code{nfeatures} number of features in training data. -} +An object of class \code{xgb.Booster}. } \description{ \code{xgb.train} is an advanced interface for training an xgboost model. @@ -285,6 +270,21 @@ and the \code{print_every_n} parameter is passed to it. \item \code{cb.early.stop}: when \code{early_stopping_rounds} is set. \item \code{cb.save.model}: when \code{save_period > 0} is set. } + +Note that objects of type \code{xgb.Booster} as returned by this function behave a bit differently +from typical R objects (it's an 'altrep' list class), and it makes a separation between +internal booster attributes (restricted to jsonifyable data), accessed through \link{xgb.attr} +and shared between interfaces through serialization functions like \link{xgb.save}; and +R-specific attributes, accessed through \link{attributes} and \link{attr}, which are otherwise +only used in the R interface, only kept when using R's serializers like \link{saveRDS}, and +not anyhow used by functions like \link{predict.xgb.Booster}. + +Be aware that one such R attribute that is automatically added is \code{params} - this attribute +is assigned from the \code{params} argument to this function, and is only meant to serve as a +reference for what went into the booster, but is not used in other methods that take a booster +object - so for example, changing the booster's configuration requires calling \verb{xgb.config<-} +or 'xgb.parameters<-', while simply modifying \verb{attributes(model)$params$<...>} will have no +effect elsewhere. } \examples{ data(agaricus.train, package='xgboost') diff --git a/R-package/man/xgb.unserialize.Rd b/R-package/man/xgb.unserialize.Rd deleted file mode 100644 index f83ee635d..000000000 --- a/R-package/man/xgb.unserialize.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xgb.unserialize.R -\name{xgb.unserialize} -\alias{xgb.unserialize} -\title{Load the instance back from \code{\link{xgb.serialize}}} -\usage{ -xgb.unserialize(buffer, handle = NULL) -} -\arguments{ -\item{buffer}{the buffer containing booster instance saved by \code{\link{xgb.serialize}}} - -\item{handle}{An \code{xgb.Booster.handle} object which will be overwritten with -the new deserialized object. Must be a null handle (e.g. when loading the model through -\code{readRDS}). If not provided, a new handle will be created.} -} -\value{ -An \code{xgb.Booster.handle} object. -} -\description{ -Load the instance back from \code{\link{xgb.serialize}} -} diff --git a/R-package/src/init.c b/R-package/src/init.c index 5eee8ebe6..81c28c401 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -15,9 +15,16 @@ Check these declarations against the C/Fortran source code. */ /* .Call calls */ +extern void XGBInitializeAltrepClass_R(DllInfo *info); +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 XGBoosterCreateInEmptyObj_R(SEXP, SEXP); +extern SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP, SEXP); +extern SEXP XGBoosterSetStrFeatureInfo_R(SEXP, SEXP, SEXP); +extern SEXP XGBoosterGetStrFeatureInfo_R(SEXP, SEXP); +extern SEXP XGBoosterBoostedRounds_R(SEXP); +extern SEXP XGBoosterGetNumFeature_R(SEXP); extern SEXP XGBoosterDumpModel_R(SEXP, SEXP, SEXP, SEXP); extern SEXP XGBoosterEvalOneIter_R(SEXP, SEXP, SEXP, SEXP); extern SEXP XGBoosterGetAttrNames_R(SEXP); @@ -57,9 +64,15 @@ extern SEXP XGBGetGlobalConfig_R(void); extern SEXP XGBoosterFeatureScore_R(SEXP, SEXP); static const R_CallMethodDef CallEntries[] = { + {"XGDuplicate_R", (DL_FUNC) &XGDuplicate_R, 1}, + {"XGPointerEqComparison_R", (DL_FUNC) &XGPointerEqComparison_R, 2}, {"XGBoosterTrainOneIter_R", (DL_FUNC) &XGBoosterTrainOneIter_R, 5}, {"XGBoosterCreate_R", (DL_FUNC) &XGBoosterCreate_R, 1}, - {"XGBoosterCreateInEmptyObj_R", (DL_FUNC) &XGBoosterCreateInEmptyObj_R, 2}, + {"XGBoosterCopyInfoFromDMatrix_R", (DL_FUNC) &XGBoosterCopyInfoFromDMatrix_R, 2}, + {"XGBoosterSetStrFeatureInfo_R",(DL_FUNC) &XGBoosterSetStrFeatureInfo_R,3}, // NOLINT + {"XGBoosterGetStrFeatureInfo_R",(DL_FUNC) &XGBoosterGetStrFeatureInfo_R,2}, // NOLINT + {"XGBoosterBoostedRounds_R", (DL_FUNC) &XGBoosterBoostedRounds_R, 1}, + {"XGBoosterGetNumFeature_R", (DL_FUNC) &XGBoosterGetNumFeature_R, 1}, {"XGBoosterDumpModel_R", (DL_FUNC) &XGBoosterDumpModel_R, 4}, {"XGBoosterEvalOneIter_R", (DL_FUNC) &XGBoosterEvalOneIter_R, 4}, {"XGBoosterGetAttrNames_R", (DL_FUNC) &XGBoosterGetAttrNames_R, 1}, @@ -106,4 +119,5 @@ __declspec(dllexport) void attribute_visible R_init_xgboost(DllInfo *dll) { R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); R_useDynamicSymbols(dll, FALSE); + XGBInitializeAltrepClass_R(dll); } diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index d7d4c49e1..63f36ad6a 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -260,16 +260,18 @@ char cpp_ex_msg[512]; using dmlc::BeginPtr; XGB_DLL SEXP XGCheckNullPtr_R(SEXP handle) { - return ScalarLogical(R_ExternalPtrAddr(handle) == NULL); + return Rf_ScalarLogical(R_ExternalPtrAddr(handle) == nullptr); } -XGB_DLL void _DMatrixFinalizer(SEXP ext) { +namespace { +void _DMatrixFinalizer(SEXP ext) { R_API_BEGIN(); if (R_ExternalPtrAddr(ext) == NULL) return; CHECK_CALL(XGDMatrixFree(R_ExternalPtrAddr(ext))); R_ClearExternalPtr(ext); R_API_END(); } +} /* namespace */ XGB_DLL SEXP XGBSetGlobalConfig_R(SEXP json_str) { R_API_BEGIN(); @@ -527,8 +529,14 @@ XGB_DLL SEXP XGDMatrixSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP array) { } SEXP str_info_holder = PROTECT(Rf_allocVector(VECSXP, len)); - for (size_t i = 0; i < len; ++i) { - SET_VECTOR_ELT(str_info_holder, i, Rf_asChar(VECTOR_ELT(array, i))); + if (TYPEOF(array) == STRSXP) { + for (size_t i = 0; i < len; ++i) { + SET_VECTOR_ELT(str_info_holder, i, STRING_ELT(array, i)); + } + } else { + for (size_t i = 0; i < len; ++i) { + SET_VECTOR_ELT(str_info_holder, i, Rf_asChar(VECTOR_ELT(array, i))); + } } SEXP field_ = PROTECT(Rf_asChar(field)); @@ -614,6 +622,14 @@ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle) { return ScalarInteger(static_cast(ncol)); } +XGB_DLL SEXP XGDuplicate_R(SEXP obj) { + return Rf_duplicate(obj); +} + +XGB_DLL SEXP XGPointerEqComparison_R(SEXP obj1, SEXP obj2) { + return Rf_ScalarLogical(R_ExternalPtrAddr(obj1) == R_ExternalPtrAddr(obj2)); +} + XGB_DLL SEXP XGDMatrixGetQuantileCut_R(SEXP handle) { const char *out_names[] = {"indptr", "data", ""}; SEXP continuation_token = Rf_protect(R_MakeUnwindCont()); @@ -682,14 +698,134 @@ XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle) { } // functions related to booster -void _BoosterFinalizer(SEXP ext) { - if (R_ExternalPtrAddr(ext) == NULL) return; - CHECK_CALL(XGBoosterFree(R_ExternalPtrAddr(ext))); - R_ClearExternalPtr(ext); +namespace { +void _BoosterFinalizer(SEXP R_ptr) { + if (R_ExternalPtrAddr(R_ptr) == NULL) return; + CHECK_CALL(XGBoosterFree(R_ExternalPtrAddr(R_ptr))); + R_ClearExternalPtr(R_ptr); +} + +/* Booster is represented as an altrep list with one element which +corresponds to an 'externalptr' holding the C object, forbidding +modification by not implementing setters, and adding custom serialization. */ +R_altrep_class_t XGBAltrepPointerClass; + +R_xlen_t XGBAltrepPointerLength_R(SEXP R_altrepped_obj) { + return 1; +} + +SEXP XGBAltrepPointerGetElt_R(SEXP R_altrepped_obj, R_xlen_t idx) { + return R_altrep_data1(R_altrepped_obj); +} + +SEXP XGBMakeEmptyAltrep() { + SEXP class_name = Rf_protect(Rf_mkString("xgb.Booster")); + SEXP elt_names = Rf_protect(Rf_mkString("ptr")); + SEXP R_ptr = Rf_protect(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + SEXP R_altrepped_obj = Rf_protect(R_new_altrep(XGBAltrepPointerClass, R_ptr, R_NilValue)); + Rf_setAttrib(R_altrepped_obj, R_NamesSymbol, elt_names); + Rf_setAttrib(R_altrepped_obj, R_ClassSymbol, class_name); + Rf_unprotect(4); + return R_altrepped_obj; +} + +/* Note: the idea for separating this function from the one above is to be +able to trigger all R allocations first before doing non-R allocations. */ +void XGBAltrepSetPointer(SEXP R_altrepped_obj, BoosterHandle handle) { + SEXP R_ptr = R_altrep_data1(R_altrepped_obj); + R_SetExternalPtrAddr(R_ptr, handle); + R_RegisterCFinalizerEx(R_ptr, _BoosterFinalizer, TRUE); +} + +SEXP XGBAltrepSerializer_R(SEXP R_altrepped_obj) { + R_API_BEGIN(); + BoosterHandle handle = R_ExternalPtrAddr(R_altrep_data1(R_altrepped_obj)); + char const *serialized_bytes; + bst_ulong serialized_length; + CHECK_CALL(XGBoosterSerializeToBuffer( + handle, &serialized_length, &serialized_bytes)); + SEXP R_state = Rf_protect(Rf_allocVector(RAWSXP, serialized_length)); + if (serialized_length != 0) { + std::memcpy(RAW(R_state), serialized_bytes, serialized_length); + } + Rf_unprotect(1); + return R_state; + R_API_END(); + return R_NilValue; /* <- should not be reached */ +} + +SEXP XGBAltrepDeserializer_R(SEXP unused, SEXP R_state) { + SEXP R_altrepped_obj = Rf_protect(XGBMakeEmptyAltrep()); + R_API_BEGIN(); + BoosterHandle handle = nullptr; + CHECK_CALL(XGBoosterCreate(nullptr, 0, &handle)); + int res_code = XGBoosterUnserializeFromBuffer(handle, + RAW(R_state), + Rf_xlength(R_state)); + if (res_code != 0) { + XGBoosterFree(handle); + } + CHECK_CALL(res_code); + XGBAltrepSetPointer(R_altrepped_obj, handle); + R_API_END(); + Rf_unprotect(1); + return R_altrepped_obj; +} + +// https://purrple.cat/blog/2018/10/14/altrep-and-cpp/ +Rboolean XGBAltrepInspector_R( + SEXP x, int pre, int deep, int pvec, + void (*inspect_subtree)(SEXP, int, int, int)) { + Rprintf("Altrepped external pointer [address:%p]\n", + R_ExternalPtrAddr(R_altrep_data1(x))); + return TRUE; +} + +SEXP XGBAltrepDuplicate_R(SEXP R_altrepped_obj, Rboolean deep) { + R_API_BEGIN(); + if (!deep) { + SEXP out = Rf_protect(XGBMakeEmptyAltrep()); + R_set_altrep_data1(out, R_altrep_data1(R_altrepped_obj)); + Rf_unprotect(1); + return out; + } else { + SEXP out = Rf_protect(XGBMakeEmptyAltrep()); + char const *serialized_bytes; + bst_ulong serialized_length; + CHECK_CALL(XGBoosterSerializeToBuffer( + R_ExternalPtrAddr(R_altrep_data1(R_altrepped_obj)), + &serialized_length, &serialized_bytes)); + BoosterHandle new_handle = nullptr; + CHECK_CALL(XGBoosterCreate(nullptr, 0, &new_handle)); + int res_code = XGBoosterUnserializeFromBuffer(new_handle, + serialized_bytes, + serialized_length); + if (res_code != 0) { + XGBoosterFree(new_handle); + } + CHECK_CALL(res_code); + XGBAltrepSetPointer(out, new_handle); + Rf_unprotect(1); + return out; + } + R_API_END(); + return R_NilValue; /* <- should not be reached */ +} + +} /* namespace */ + +XGB_DLL void XGBInitializeAltrepClass_R(DllInfo *dll) { + XGBAltrepPointerClass = R_make_altlist_class("XGBAltrepPointerClass", "xgboost", dll); + R_set_altrep_Length_method(XGBAltrepPointerClass, XGBAltrepPointerLength_R); + R_set_altlist_Elt_method(XGBAltrepPointerClass, XGBAltrepPointerGetElt_R); + R_set_altrep_Inspect_method(XGBAltrepPointerClass, XGBAltrepInspector_R); + R_set_altrep_Serialized_state_method(XGBAltrepPointerClass, XGBAltrepSerializer_R); + R_set_altrep_Unserialize_method(XGBAltrepPointerClass, XGBAltrepDeserializer_R); + R_set_altrep_Duplicate_method(XGBAltrepPointerClass, XGBAltrepDuplicate_R); } XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { - SEXP ret = PROTECT(R_MakeExternalPtr(nullptr, R_NilValue, R_NilValue)); + SEXP out = Rf_protect(XGBMakeEmptyAltrep()); R_API_BEGIN(); R_xlen_t len = Rf_xlength(dmats); BoosterHandle handle; @@ -703,33 +839,104 @@ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats) { res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); } CHECK_CALL(res_code); - R_SetExternalPtrAddr(ret, handle); - R_RegisterCFinalizerEx(ret, _BoosterFinalizer, TRUE); + XGBAltrepSetPointer(out, handle); R_API_END(); - UNPROTECT(1); - return ret; + Rf_unprotect(1); + return out; } -XGB_DLL SEXP XGBoosterCreateInEmptyObj_R(SEXP dmats, SEXP R_handle) { +XGB_DLL SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP booster, SEXP dmat) { R_API_BEGIN(); - R_xlen_t len = Rf_xlength(dmats); - BoosterHandle handle; + char const **feature_names; + bst_ulong len_feature_names = 0; + CHECK_CALL(XGDMatrixGetStrFeatureInfo(R_ExternalPtrAddr(dmat), + "feature_name", + &len_feature_names, + &feature_names)); + if (len_feature_names) { + CHECK_CALL(XGBoosterSetStrFeatureInfo(R_ExternalPtrAddr(booster), + "feature_name", + feature_names, + len_feature_names)); + } + + char const **feature_types; + bst_ulong len_feature_types = 0; + CHECK_CALL(XGDMatrixGetStrFeatureInfo(R_ExternalPtrAddr(dmat), + "feature_type", + &len_feature_types, + &feature_types)); + if (len_feature_types) { + CHECK_CALL(XGBoosterSetStrFeatureInfo(R_ExternalPtrAddr(booster), + "feature_type", + feature_types, + len_feature_types)); + } + R_API_END(); + return R_NilValue; +} + +XGB_DLL SEXP XGBoosterSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP features) { + R_API_BEGIN(); + SEXP field_char = Rf_protect(Rf_asChar(field)); + bst_ulong len_features = Rf_xlength(features); int res_code; { - std::vector dvec(len); - for (R_xlen_t i = 0; i < len; ++i) { - dvec[i] = R_ExternalPtrAddr(VECTOR_ELT(dmats, i)); + std::vector str_arr(len_features); + for (bst_ulong idx = 0; idx < len_features; idx++) { + str_arr[idx] = CHAR(STRING_ELT(features, idx)); } - res_code = XGBoosterCreate(BeginPtr(dvec), dvec.size(), &handle); + res_code = XGBoosterSetStrFeatureInfo(R_ExternalPtrAddr(handle), + CHAR(field_char), + str_arr.data(), + len_features); } CHECK_CALL(res_code); - R_SetExternalPtrAddr(R_handle, handle); - R_RegisterCFinalizerEx(R_handle, _BoosterFinalizer, TRUE); + Rf_unprotect(1); R_API_END(); return R_NilValue; } +XGB_DLL SEXP XGBoosterGetStrFeatureInfo_R(SEXP handle, SEXP field) { + R_API_BEGIN(); + bst_ulong len; + const char **out_features; + SEXP field_char = Rf_protect(Rf_asChar(field)); + CHECK_CALL(XGBoosterGetStrFeatureInfo(R_ExternalPtrAddr(handle), + CHAR(field_char), &len, &out_features)); + SEXP out = Rf_protect(Rf_allocVector(STRSXP, len)); + for (bst_ulong idx = 0; idx < len; idx++) { + SET_STRING_ELT(out, idx, Rf_mkChar(out_features[idx])); + } + Rf_unprotect(2); + return out; + R_API_END(); + return R_NilValue; /* <- should not be reached */ +} + +XGB_DLL SEXP XGBoosterBoostedRounds_R(SEXP handle) { + SEXP out = Rf_protect(Rf_allocVector(INTSXP, 1)); + R_API_BEGIN(); + CHECK_CALL(XGBoosterBoostedRounds(R_ExternalPtrAddr(handle), INTEGER(out))); + R_API_END(); + Rf_unprotect(1); + return out; +} + +/* Note: R's integer class is 32-bit-and-signed only, while xgboost +supports more, so it returns it as a floating point instead */ +XGB_DLL SEXP XGBoosterGetNumFeature_R(SEXP handle) { + SEXP out = Rf_protect(Rf_allocVector(REALSXP, 1)); + R_API_BEGIN(); + bst_ulong res; + CHECK_CALL(XGBoosterGetNumFeature(R_ExternalPtrAddr(handle), &res)); + REAL(out)[0] = static_cast(res); + R_API_END(); + Rf_unprotect(1); + return out; +} + XGB_DLL SEXP XGBoosterSetParam_R(SEXP handle, SEXP name, SEXP val) { R_API_BEGIN(); SEXP name_ = PROTECT(Rf_asChar(name)); @@ -745,8 +952,8 @@ XGB_DLL SEXP XGBoosterSetParam_R(SEXP handle, SEXP name, SEXP val) { XGB_DLL SEXP XGBoosterUpdateOneIter_R(SEXP handle, SEXP iter, SEXP dtrain) { R_API_BEGIN(); CHECK_CALL(XGBoosterUpdateOneIter(R_ExternalPtrAddr(handle), - asInteger(iter), - R_ExternalPtrAddr(dtrain))); + Rf_asInteger(iter), + R_ExternalPtrAddr(dtrain))); R_API_END(); return R_NilValue; } diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 4e3458957..79d441792 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -8,7 +8,9 @@ #define XGBOOST_R_H_ // NOLINT(*) +#include #include +#include #include #include @@ -143,6 +145,25 @@ XGB_DLL SEXP XGDMatrixNumRow_R(SEXP handle); */ XGB_DLL SEXP XGDMatrixNumCol_R(SEXP handle); +/*! + * \brief Call R C-level function 'duplicate' + * \param obj Object to duplicate + */ +XGB_DLL SEXP XGDuplicate_R(SEXP obj); + +/*! + * \brief Equality comparison for two pointers + * \param obj1 R 'externalptr' + * \param obj2 R 'externalptr' + */ +XGB_DLL SEXP XGPointerEqComparison_R(SEXP obj1, SEXP obj2); + +/*! + * \brief Register the Altrep class used for the booster + * \param dll DLL info as provided by R_init + */ +XGB_DLL void XGBInitializeAltrepClass_R(DllInfo *dll); + /*! * \brief return the quantile cuts used for the histogram method * \param handle an instance of data matrix @@ -174,13 +195,37 @@ XGB_DLL SEXP XGDMatrixGetDataAsCSR_R(SEXP handle); */ XGB_DLL SEXP XGBoosterCreate_R(SEXP dmats); +/*! + * \brief copy information about features from a DMatrix into a Booster + * \param booster R 'externalptr' pointing to a booster object + * \param dmat R 'externalptr' pointing to a DMatrix object + */ +XGB_DLL SEXP XGBoosterCopyInfoFromDMatrix_R(SEXP booster, SEXP dmat); /*! - * \brief create xgboost learner, saving the pointer into an existing R object - * \param dmats a list of dmatrix handles that will be cached - * \param R_handle a clean R external pointer (not holding any object) + * \brief handle R 'externalptr' holding the booster object + * \param field field name + * \param features features to set for the field */ -XGB_DLL SEXP XGBoosterCreateInEmptyObj_R(SEXP dmats, SEXP R_handle); +XGB_DLL SEXP XGBoosterSetStrFeatureInfo_R(SEXP handle, SEXP field, SEXP features); + +/*! + * \brief handle R 'externalptr' holding the booster object + * \param field field name + */ +XGB_DLL SEXP XGBoosterGetStrFeatureInfo_R(SEXP handle, SEXP field); + +/*! + * \brief Get the number of boosted rounds from a model + * \param handle R 'externalptr' holding the booster object + */ +XGB_DLL SEXP XGBoosterBoostedRounds_R(SEXP handle); + +/*! + * \brief Get the number of features to which the model was fitted + * \param handle R 'externalptr' holding the booster object + */ +XGB_DLL SEXP XGBoosterGetNumFeature_R(SEXP handle); /*! * \brief set parameters diff --git a/R-package/tests/helper_scripts/install_deps.R b/R-package/tests/helper_scripts/install_deps.R index cf9ab0034..3ae44f6b1 100644 --- a/R-package/tests/helper_scripts/install_deps.R +++ b/R-package/tests/helper_scripts/install_deps.R @@ -3,7 +3,6 @@ ## inconsistent is found. pkgs <- c( ## CI - "caret", "pkgbuild", "roxygen2", "XML", diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 44c530c90..8dd934765 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -25,10 +25,10 @@ test_that("train and predict binary classification", { "train-error" ) expect_equal(class(bst), "xgb.Booster") - expect_equal(bst$niter, nrounds) - expect_false(is.null(bst$evaluation_log)) - expect_equal(nrow(bst$evaluation_log), nrounds) - expect_lt(bst$evaluation_log[, min(train_error)], 0.03) + expect_equal(xgb.get.num.boosted.rounds(bst), nrounds) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_equal(nrow(attributes(bst)$evaluation_log), nrounds) + expect_lt(attributes(bst)$evaluation_log[, min(train_error)], 0.03) pred <- predict(bst, test$data) expect_length(pred, 1611) @@ -36,7 +36,7 @@ test_that("train and predict binary classification", { pred1 <- predict(bst, train$data, ntreelimit = 1) expect_length(pred1, 6513) err_pred1 <- sum((pred1 > 0.5) != train$label) / length(train$label) - err_log <- bst$evaluation_log[1, train_error] + err_log <- attributes(bst)$evaluation_log[1, train_error] expect_lt(abs(err_pred1 - err_log), 10e-6) pred2 <- predict(bst, train$data, iterationrange = c(1, 2)) @@ -160,9 +160,9 @@ test_that("train and predict softprob", { ), "train-merror" ) - expect_false(is.null(bst$evaluation_log)) - expect_lt(bst$evaluation_log[, min(train_merror)], 0.025) - expect_equal(bst$niter * 3, xgb.ntree(bst)) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_lt(attributes(bst)$evaluation_log[, min(train_merror)], 0.025) + expect_equal(xgb.get.num.boosted.rounds(bst) * 3, xgb.ntree(bst)) pred <- predict(bst, as.matrix(iris[, -5])) expect_length(pred, nrow(iris) * 3) # row sums add up to total probability of 1: @@ -172,12 +172,12 @@ test_that("train and predict softprob", { expect_equal(as.numeric(t(mpred)), pred) pred_labels <- max.col(mpred) - 1 err <- sum(pred_labels != lb) / length(lb) - expect_equal(bst$evaluation_log[5, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[5, train_merror], err, tolerance = 5e-6) # manually calculate error at the 1st iteration: mpred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, ntreelimit = 1) pred_labels <- max.col(mpred) - 1 err <- sum(pred_labels != lb) / length(lb) - expect_equal(bst$evaluation_log[1, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[1, train_merror], err, tolerance = 5e-6) mpred1 <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, iterationrange = c(1, 2)) expect_equal(mpred, mpred1) @@ -211,14 +211,14 @@ test_that("train and predict softmax", { ), "train-merror" ) - expect_false(is.null(bst$evaluation_log)) - expect_lt(bst$evaluation_log[, min(train_merror)], 0.025) - expect_equal(bst$niter * 3, xgb.ntree(bst)) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_lt(attributes(bst)$evaluation_log[, min(train_merror)], 0.025) + expect_equal(xgb.get.num.boosted.rounds(bst) * 3, xgb.ntree(bst)) pred <- predict(bst, as.matrix(iris[, -5])) expect_length(pred, nrow(iris)) err <- sum(pred != lb) / length(lb) - expect_equal(bst$evaluation_log[5, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[5, train_merror], err, tolerance = 5e-6) }) test_that("train and predict RF", { @@ -232,12 +232,12 @@ test_that("train and predict RF", { num_parallel_tree = 20, subsample = 0.6, colsample_bytree = 0.1, watchlist = list(train = xgb.DMatrix(train$data, label = lb)) ) - expect_equal(bst$niter, 1) + expect_equal(xgb.get.num.boosted.rounds(bst), 1) expect_equal(xgb.ntree(bst), 20) pred <- predict(bst, train$data) pred_err <- sum((pred > 0.5) != lb) / length(lb) - expect_lt(abs(bst$evaluation_log[1, train_error] - pred_err), 10e-6) + expect_lt(abs(attributes(bst)$evaluation_log[1, train_error] - pred_err), 10e-6) # expect_lt(pred_err, 0.03) pred <- predict(bst, train$data, ntreelimit = 20) @@ -260,18 +260,18 @@ test_that("train and predict RF with softprob", { num_parallel_tree = 4, subsample = 0.5, colsample_bytree = 0.5, watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ) - expect_equal(bst$niter, 15) + expect_equal(xgb.get.num.boosted.rounds(bst), 15) expect_equal(xgb.ntree(bst), 15 * 3 * 4) # predict for all iterations: pred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE) expect_equal(dim(pred), c(nrow(iris), 3)) pred_labels <- max.col(pred) - 1 err <- sum(pred_labels != lb) / length(lb) - expect_equal(bst$evaluation_log[nrounds, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[nrounds, train_merror], err, tolerance = 5e-6) # predict for 7 iterations and adjust for 4 parallel trees per iteration pred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, ntreelimit = 7 * 4) err <- sum((max.col(pred) - 1) != lb) / length(lb) - expect_equal(bst$evaluation_log[7, train_merror], err, tolerance = 5e-6) + expect_equal(attributes(bst)$evaluation_log[7, train_merror], err, tolerance = 5e-6) }) test_that("use of multiple eval metrics works", { @@ -284,9 +284,9 @@ test_that("use of multiple eval metrics works", { ), "train-error.*train-auc.*train-logloss" ) - expect_false(is.null(bst$evaluation_log)) - expect_equal(dim(bst$evaluation_log), c(2, 4)) - expect_equal(colnames(bst$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_equal(dim(attributes(bst)$evaluation_log), c(2, 4)) + expect_equal(colnames(attributes(bst)$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) expect_output( bst2 <- xgb.train( data = xgb.DMatrix(train$data, label = train$label), max_depth = 2, @@ -296,9 +296,9 @@ test_that("use of multiple eval metrics works", { ), "train-error.*train-auc.*train-logloss" ) - expect_false(is.null(bst2$evaluation_log)) - expect_equal(dim(bst2$evaluation_log), c(2, 4)) - expect_equal(colnames(bst2$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) + expect_false(is.null(attributes(bst2)$evaluation_log)) + expect_equal(dim(attributes(bst2)$evaluation_log), c(2, 4)) + expect_equal(colnames(attributes(bst2)$evaluation_log), c("iter", "train_error", "train_auc", "train_logloss")) }) @@ -318,41 +318,25 @@ test_that("training continuation works", { # continue for two more: bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = bst1) if (!windows_flag && !solaris_flag) { - expect_equal(bst$raw, bst2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(bst2)) } - expect_false(is.null(bst2$evaluation_log)) - expect_equal(dim(bst2$evaluation_log), c(4, 2)) - expect_equal(bst2$evaluation_log, bst$evaluation_log) + expect_false(is.null(attributes(bst2)$evaluation_log)) + expect_equal(dim(attributes(bst2)$evaluation_log), c(4, 2)) + expect_equal(attributes(bst2)$evaluation_log, attributes(bst)$evaluation_log) # test continuing from raw model data - bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = bst1$raw) + bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = xgb.save.raw(bst1)) if (!windows_flag && !solaris_flag) { - expect_equal(bst$raw, bst2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(bst2)) } - expect_equal(dim(bst2$evaluation_log), c(2, 2)) + expect_equal(dim(attributes(bst2)$evaluation_log), c(2, 2)) # test continuing from a model in file fname <- file.path(tempdir(), "xgboost.json") xgb.save(bst1, fname) bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, xgb_model = fname) if (!windows_flag && !solaris_flag) { - expect_equal(bst$raw, bst2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(bst2)) } - expect_equal(dim(bst2$evaluation_log), c(2, 2)) -}) - -test_that("model serialization works", { - out_path <- file.path(tempdir(), "model_serialization") - dtrain <- xgb.DMatrix(train$data, label = train$label, nthread = n_threads) - watchlist <- list(train = dtrain) - param <- list(objective = "binary:logistic", nthread = n_threads) - booster <- xgb.train(param, dtrain, nrounds = 4, watchlist) - raw <- xgb.serialize(booster) - saveRDS(raw, out_path) - raw <- readRDS(out_path) - - loaded <- xgb.unserialize(raw) - raw_from_loaded <- xgb.serialize(loaded) - expect_equal(raw, raw_from_loaded) - file.remove(out_path) + expect_equal(dim(attributes(bst2)$evaluation_log), c(2, 2)) }) test_that("xgb.cv works", { @@ -455,8 +439,8 @@ test_that("max_delta_step works", { # model with restricted max_delta_step bst2 <- xgb.train(param, dtrain, nrounds, watchlist, verbose = 1, max_delta_step = 1) # the no-restriction model is expected to have consistently lower loss during the initial iterations - expect_true(all(bst1$evaluation_log$train_logloss < bst2$evaluation_log$train_logloss)) - expect_lt(mean(bst1$evaluation_log$train_logloss) / mean(bst2$evaluation_log$train_logloss), 0.8) + expect_true(all(attributes(bst1)$evaluation_log$train_logloss < attributes(bst2)$evaluation_log$train_logloss)) + expect_lt(mean(attributes(bst1)$evaluation_log$train_logloss) / mean(attributes(bst2)$evaluation_log$train_logloss), 0.8) }) test_that("colsample_bytree works", { @@ -675,3 +659,131 @@ test_that("Can use ranking objectives with either 'qid' or 'group'", { pred_gr <- predict(model_gr, x) expect_equal(pred_qid, pred_gr) }) + +test_that("Coefficients from gblinear have the expected shape and names", { + # Single-column coefficients + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + mm <- model.matrix(~., data = mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list( + booster = "gblinear", + nthread = 1 + ), + nrounds = 3 + ) + coefs <- coef(model) + expect_equal(length(coefs), ncol(x) + 1) + expect_equal(names(coefs), c("(Intercept)", colnames(x))) + pred_auto <- predict(model, x) + pred_manual <- as.numeric(mm %*% coefs) + expect_equal(pred_manual, pred_auto, tolerance = 1e-5) + + # Multi-column coefficients + data(iris) + y <- as.numeric(iris$Species) - 1 + x <- as.matrix(iris[, -5]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + mm <- model.matrix(~., data = iris[, -5]) + model <- xgb.train( + data = dm, + params = list( + booster = "gblinear", + objective = "multi:softprob", + num_class = 3, + nthread = 1 + ), + nrounds = 3 + ) + coefs <- coef(model) + expect_equal(nrow(coefs), ncol(x) + 1) + expect_equal(ncol(coefs), 3) + expect_equal(row.names(coefs), c("(Intercept)", colnames(x))) + pred_auto <- predict(model, x, outputmargin = TRUE, reshape = TRUE) + pred_manual <- unname(mm %*% coefs) + expect_equal(pred_manual, pred_auto, tolerance = 1e-7) +}) + +test_that("Deep copies work as expected", { + data(mtcars) + y <- mtcars$mpg + x <- mtcars[, -1] + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list(nthread = 1), + nrounds = 3 + ) + + xgb.attr(model, "my_attr") <- 100 + model_shallow_copy <- model + xgb.attr(model_shallow_copy, "my_attr") <- 333 + attr_orig <- xgb.attr(model, "my_attr") + attr_shallow <- xgb.attr(model_shallow_copy, "my_attr") + expect_equal(attr_orig, attr_shallow) + + model_deep_copy <- xgb.copy.Booster(model) + xgb.attr(model_deep_copy, "my_attr") <- 444 + attr_orig <- xgb.attr(model, "my_attr") + attr_deep <- xgb.attr(model_deep_copy, "my_attr") + expect_false(attr_orig == attr_deep) +}) + +test_that("Pointer comparison works as expected", { + library(xgboost) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + model <- xgb.train( + params = list(nthread = 1), + data = xgb.DMatrix(x, label = y, nthread = 1), + nrounds = 3 + ) + + model_shallow_copy <- model + expect_true(xgb.is.same.Booster(model, model_shallow_copy)) + + model_deep_copy <- xgb.copy.Booster(model) + expect_false(xgb.is.same.Booster(model, model_deep_copy)) + + xgb.attr(model_shallow_copy, "my_attr") <- 111 + expect_equal(xgb.attr(model, "my_attr"), "111") + expect_null(xgb.attr(model_deep_copy, "my_attr")) +}) + +test_that("DMatrix field are set to booster when training", { + set.seed(123) + y <- rnorm(100) + x <- matrix(rnorm(100 * 3), nrow = 100) + x[, 2] <- abs(as.integer(x[, 2])) + + dm_unnamed <- xgb.DMatrix(x, label = y, nthread = 1) + dm_feature_names <- xgb.DMatrix(x, label = y, feature_names = c("a", "b", "c"), nthread = 1) + dm_feature_types <- xgb.DMatrix(x, label = y) + setinfo(dm_feature_types, "feature_type", c("q", "c", "q")) + dm_both <- xgb.DMatrix(x, label = y, feature_names = c("a", "b", "c"), nthread = 1) + setinfo(dm_both, "feature_type", c("q", "c", "q")) + + params <- list(nthread = 1) + model_unnamed <- xgb.train(data = dm_unnamed, params = params, nrounds = 3) + model_feature_names <- xgb.train(data = dm_feature_names, params = params, nrounds = 3) + model_feature_types <- xgb.train(data = dm_feature_types, params = params, nrounds = 3) + model_both <- xgb.train(data = dm_both, params = params, nrounds = 3) + + expect_null(getinfo(model_unnamed, "feature_name")) + expect_equal(getinfo(model_feature_names, "feature_name"), c("a", "b", "c")) + expect_null(getinfo(model_feature_types, "feature_name")) + expect_equal(getinfo(model_both, "feature_name"), c("a", "b", "c")) + + expect_null(variable.names(model_unnamed)) + expect_equal(variable.names(model_feature_names), c("a", "b", "c")) + expect_null(variable.names(model_feature_types)) + expect_equal(variable.names(model_both), c("a", "b", "c")) + + expect_null(getinfo(model_unnamed, "feature_type")) + expect_null(getinfo(model_feature_names, "feature_type")) + expect_equal(getinfo(model_feature_types, "feature_type"), c("q", "c", "q")) + expect_equal(getinfo(model_both, "feature_type"), c("q", "c", "q")) +}) diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index bee98f688..afa270c0b 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -111,9 +111,9 @@ test_that("can store evaluation_log without printing", { expect_silent( bst <- xgb.train(param, dtrain, nrounds = 10, watchlist, eta = 1, verbose = 0) ) - expect_false(is.null(bst$evaluation_log)) - expect_false(is.null(bst$evaluation_log$train_error)) - expect_lt(bst$evaluation_log[, min(train_error)], 0.2) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_false(is.null(attributes(bst)$evaluation_log$train_error)) + expect_lt(attributes(bst)$evaluation_log[, min(train_error)], 0.2) }) test_that("cb.reset.parameters works as expected", { @@ -121,34 +121,34 @@ test_that("cb.reset.parameters works as expected", { # fixed eta set.seed(111) bst0 <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 0.9, verbose = 0) - expect_false(is.null(bst0$evaluation_log)) - expect_false(is.null(bst0$evaluation_log$train_error)) + expect_false(is.null(attributes(bst0)$evaluation_log)) + expect_false(is.null(attributes(bst0)$evaluation_log$train_error)) # same eta but re-set as a vector parameter in the callback set.seed(111) my_par <- list(eta = c(0.9, 0.9)) bst1 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bst1$evaluation_log$train_error)) - expect_equal(bst0$evaluation_log$train_error, - bst1$evaluation_log$train_error) + expect_false(is.null(attributes(bst1)$evaluation_log$train_error)) + expect_equal(attributes(bst0)$evaluation_log$train_error, + attributes(bst1)$evaluation_log$train_error) # same eta but re-set via a function in the callback set.seed(111) my_par <- list(eta = function(itr, itr_end) 0.9) bst2 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bst2$evaluation_log$train_error)) - expect_equal(bst0$evaluation_log$train_error, - bst2$evaluation_log$train_error) + expect_false(is.null(attributes(bst2)$evaluation_log$train_error)) + expect_equal(attributes(bst0)$evaluation_log$train_error, + attributes(bst2)$evaluation_log$train_error) # different eta re-set as a vector parameter in the callback set.seed(111) my_par <- list(eta = c(0.6, 0.5)) bst3 <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bst3$evaluation_log$train_error)) - expect_false(all(bst0$evaluation_log$train_error == bst3$evaluation_log$train_error)) + expect_false(is.null(attributes(bst3)$evaluation_log$train_error)) + expect_false(all(attributes(bst0)$evaluation_log$train_error == attributes(bst3)$evaluation_log$train_error)) # resetting multiple parameters at the same time runs with no error my_par <- list(eta = c(1., 0.5), gamma = c(1, 2), max_depth = c(4, 8)) @@ -166,8 +166,8 @@ test_that("cb.reset.parameters works as expected", { my_par <- list(eta = c(0., 0.)) bstX <- xgb.train(param, dtrain, nrounds = 2, watchlist, verbose = 0, callbacks = list(cb.reset.parameters(my_par))) - expect_false(is.null(bstX$evaluation_log$train_error)) - er <- unique(bstX$evaluation_log$train_error) + expect_false(is.null(attributes(bstX)$evaluation_log$train_error)) + er <- unique(attributes(bstX)$evaluation_log$train_error) expect_length(er, 1) expect_gt(er, 0.4) }) @@ -183,14 +183,14 @@ test_that("cb.save.model works as expected", { expect_true(file.exists(files[2])) b1 <- xgb.load(files[1]) xgb.parameters(b1) <- list(nthread = 2) - expect_equal(xgb.ntree(b1), 1) + expect_equal(xgb.get.num.boosted.rounds(b1), 1) b2 <- xgb.load(files[2]) xgb.parameters(b2) <- list(nthread = 2) - expect_equal(xgb.ntree(b2), 2) + expect_equal(xgb.get.num.boosted.rounds(b2), 2) xgb.config(b2) <- xgb.config(bst) expect_equal(xgb.config(bst), xgb.config(b2)) - expect_equal(bst$raw, b2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(b2)) # save_period = 0 saves the last iteration's model bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, eta = 1, verbose = 0, @@ -198,7 +198,7 @@ test_that("cb.save.model works as expected", { expect_true(file.exists(files[3])) b2 <- xgb.load(files[3]) xgb.config(b2) <- xgb.config(bst) - expect_equal(bst$raw, b2$raw) + expect_equal(xgb.save.raw(bst), xgb.save.raw(b2)) for (f in files) if (file.exists(f)) file.remove(f) }) @@ -209,14 +209,14 @@ test_that("early stopping xgb.train works", { bst <- xgb.train(param, dtrain, nrounds = 20, watchlist, eta = 0.3, early_stopping_rounds = 3, maximize = FALSE) , "Stopping. Best iteration") - expect_false(is.null(bst$best_iteration)) - expect_lt(bst$best_iteration, 19) - expect_equal(bst$best_iteration, bst$best_ntreelimit) + expect_false(is.null(xgb.attr(bst, "best_iteration"))) + expect_lt(xgb.attr(bst, "best_iteration"), 19) + expect_equal(xgb.attr(bst, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) pred <- predict(bst, dtest) expect_equal(length(pred), 1611) err_pred <- err(ltest, pred) - err_log <- bst$evaluation_log[bst$best_iteration, test_error] + err_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration"), test_error] expect_equal(err_log, err_pred, tolerance = 5e-6) set.seed(11) @@ -224,15 +224,15 @@ test_that("early stopping xgb.train works", { bst0 <- xgb.train(param, dtrain, nrounds = 20, watchlist, eta = 0.3, early_stopping_rounds = 3, maximize = FALSE, verbose = 0) ) - expect_equal(bst$evaluation_log, bst0$evaluation_log) + expect_equal(attributes(bst)$evaluation_log, attributes(bst0)$evaluation_log) fname <- file.path(tempdir(), "model.bin") xgb.save(bst, fname) loaded <- xgb.load(fname) - expect_false(is.null(loaded$best_iteration)) - expect_equal(loaded$best_iteration, bst$best_ntreelimit) - expect_equal(loaded$best_ntreelimit, bst$best_ntreelimit) + expect_false(is.null(xgb.attr(loaded, "best_iteration"))) + expect_equal(xgb.attr(loaded, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) + expect_equal(xgb.attr(loaded, "best_ntreelimit"), xgb.attr(bst, "best_ntreelimit")) }) test_that("early stopping using a specific metric works", { @@ -243,14 +243,14 @@ test_that("early stopping using a specific metric works", { callbacks = list(cb.early.stop(stopping_rounds = 3, maximize = FALSE, metric_name = 'test_logloss'))) , "Stopping. Best iteration") - expect_false(is.null(bst$best_iteration)) - expect_lt(bst$best_iteration, 19) - expect_equal(bst$best_iteration, bst$best_ntreelimit) + expect_false(is.null(xgb.attr(bst, "best_iteration"))) + expect_lt(xgb.attr(bst, "best_iteration"), 19) + expect_equal(xgb.attr(bst, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) - pred <- predict(bst, dtest, ntreelimit = bst$best_ntreelimit) + pred <- predict(bst, dtest, ntreelimit = xgb.attr(bst, "best_ntreelimit")) expect_equal(length(pred), 1611) logloss_pred <- sum(-ltest * log(pred) - (1 - ltest) * log(1 - pred)) / length(ltest) - logloss_log <- bst$evaluation_log[bst$best_iteration, test_logloss] + logloss_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration"), test_logloss] expect_equal(logloss_log, logloss_pred, tolerance = 1e-5) }) diff --git a/R-package/tests/testthat/test_custom_objective.R b/R-package/tests/testthat/test_custom_objective.R index ff8eb1d6d..c65031246 100644 --- a/R-package/tests/testthat/test_custom_objective.R +++ b/R-package/tests/testthat/test_custom_objective.R @@ -35,9 +35,9 @@ num_round <- 2 test_that("custom objective works", { bst <- xgb.train(param, dtrain, num_round, watchlist) expect_equal(class(bst), "xgb.Booster") - expect_false(is.null(bst$evaluation_log)) - expect_false(is.null(bst$evaluation_log$eval_error)) - expect_lt(bst$evaluation_log[num_round, eval_error], 0.03) + expect_false(is.null(attributes(bst)$evaluation_log)) + expect_false(is.null(attributes(bst)$evaluation_log$eval_error)) + expect_lt(attributes(bst)$evaluation_log[num_round, eval_error], 0.03) }) test_that("custom objective in CV works", { @@ -50,7 +50,7 @@ test_that("custom objective in CV works", { test_that("custom objective with early stop works", { bst <- xgb.train(param, dtrain, 10, watchlist) expect_equal(class(bst), "xgb.Booster") - train_log <- bst$evaluation_log$train_error + train_log <- attributes(bst)$evaluation_log$train_error expect_true(all(diff(train_log) <= 0)) }) diff --git a/R-package/tests/testthat/test_glm.R b/R-package/tests/testthat/test_glm.R index 9e0a3551f..ae698d98f 100644 --- a/R-package/tests/testthat/test_glm.R +++ b/R-package/tests/testthat/test_glm.R @@ -24,28 +24,28 @@ test_that("gblinear works", { bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'shuffle') ypred <- predict(bst, dtest) expect_equal(length(getinfo(dtest, 'label')), 1611) - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'cyclic', callbacks = list(cb.gblinear.history())) - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) h <- xgb.gblinear.history(bst) expect_equal(dim(h), c(n, ncol(dtrain) + 1)) expect_is(h, "matrix") param$updater <- 'coord_descent' bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'cyclic') - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'shuffle') - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) bst <- xgb.train(param, dtrain, 2, watchlist, verbose = VERB, feature_selector = 'greedy') - expect_lt(bst$evaluation_log$eval_error[2], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[2], ERR_UL) bst <- xgb.train(param, dtrain, n, watchlist, verbose = VERB, feature_selector = 'thrifty', top_k = 50, callbacks = list(cb.gblinear.history(sparse = TRUE))) - expect_lt(bst$evaluation_log$eval_error[n], ERR_UL) + expect_lt(attributes(bst)$evaluation_log$eval_error[n], ERR_UL) h <- xgb.gblinear.history(bst) expect_equal(dim(h), c(n, ncol(dtrain) + 1)) expect_s4_class(h, "dgCMatrix") @@ -72,10 +72,10 @@ test_that("gblinear early stopping works", { booster <- xgb.train( param, dtrain, n, list(eval = dtest, train = dtrain), early_stopping_rounds = es_round ) - expect_equal(booster$best_iteration, 5) + expect_equal(xgb.attr(booster, "best_iteration"), 5) predt_es <- predict(booster, dtrain) - n <- booster$best_iteration + es_round + n <- xgb.attr(booster, "best_iteration") + es_round booster <- xgb.train( param, dtrain, n, list(eval = dtest, train = dtrain), early_stopping_rounds = es_round ) diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index 62a8c44bc..372f2520c 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -49,6 +49,9 @@ mbst.GLM <- xgb.train(data = xgb.DMatrix(as.matrix(iris[, -5]), label = mlabel), booster = "gblinear", eta = 0.1, nthread = 1, nrounds = nrounds, objective = "multi:softprob", num_class = nclass, base_score = 0) +# without feature names +bst.Tree.unnamed <- xgb.copy.Booster(bst.Tree) +setinfo(bst.Tree.unnamed, "feature_name", NULL) test_that("xgb.dump works", { .skip_if_vcd_not_available() @@ -204,7 +207,7 @@ test_that("xgb-attribute functionality", { list.ch <- list.val[order(names(list.val))] list.ch <- lapply(list.ch, as.character) # note: iter is 0-index in xgb attributes - list.default <- list(niter = as.character(nrounds - 1)) + list.default <- list() list.ch <- c(list.ch, list.default) # proper input: expect_error(xgb.attr(bst.Tree, NULL)) @@ -212,24 +215,25 @@ test_that("xgb-attribute functionality", { # set & get: expect_null(xgb.attr(bst.Tree, "asdf")) expect_equal(xgb.attributes(bst.Tree), list.default) - xgb.attr(bst.Tree, "my_attr") <- val - expect_equal(xgb.attr(bst.Tree, "my_attr"), val) - xgb.attributes(bst.Tree) <- list.val - expect_equal(xgb.attributes(bst.Tree), list.ch) + bst.Tree.copy <- xgb.copy.Booster(bst.Tree) + xgb.attr(bst.Tree.copy, "my_attr") <- val + expect_equal(xgb.attr(bst.Tree.copy, "my_attr"), val) + xgb.attributes(bst.Tree.copy) <- list.val + expect_equal(xgb.attributes(bst.Tree.copy), list.ch) # serializing: - fname <- file.path(tempdir(), "xgb.model") - xgb.save(bst.Tree, fname) + fname <- file.path(tempdir(), "xgb.ubj") + xgb.save(bst.Tree.copy, fname) bst <- xgb.load(fname) expect_equal(xgb.attr(bst, "my_attr"), val) expect_equal(xgb.attributes(bst), list.ch) # deletion: xgb.attr(bst, "my_attr") <- NULL expect_null(xgb.attr(bst, "my_attr")) - expect_equal(xgb.attributes(bst), list.ch[c("a", "b", "niter")]) + expect_equal(xgb.attributes(bst), list.ch[c("a", "b")]) xgb.attributes(bst) <- list(a = NULL, b = NULL) expect_equal(xgb.attributes(bst), list.default) xgb.attributes(bst) <- list(niter = NULL) - expect_null(xgb.attributes(bst)) + expect_equal(xgb.attributes(bst), list()) }) if (grepl('Windows', Sys.info()[['sysname']], fixed = TRUE) || @@ -262,21 +266,17 @@ test_that("xgb.Booster serializing as R object works", { dtrain <- xgb.DMatrix(sparse_matrix, label = label, nthread = 2) expect_equal(predict(bst.Tree, dtrain), predict(bst, dtrain), tolerance = float_tolerance) expect_equal(xgb.dump(bst.Tree), xgb.dump(bst)) + fname_bin <- file.path(tempdir(), "xgb.model") xgb.save(bst, fname_bin) bst <- readRDS(fname_rds) - nil_ptr <- new("externalptr") - class(nil_ptr) <- "xgb.Booster.handle" - expect_true(identical(bst$handle, nil_ptr)) - bst <- xgb.Booster.complete(bst) - expect_true(!identical(bst$handle, nil_ptr)) expect_equal(predict(bst.Tree, dtrain), predict(bst, dtrain), tolerance = float_tolerance) }) test_that("xgb.model.dt.tree works with and without feature names", { .skip_if_vcd_not_available() names.dt.trees <- c("Tree", "Node", "ID", "Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") - dt.tree <- xgb.model.dt.tree(feature_names = feature.names, model = bst.Tree) + dt.tree <- xgb.model.dt.tree(model = bst.Tree) expect_equal(names.dt.trees, names(dt.tree)) if (!flag_32bit) expect_equal(dim(dt.tree), c(188, 10)) @@ -286,9 +286,7 @@ test_that("xgb.model.dt.tree works with and without feature names", { expect_equal(dt.tree, dt.tree.0) # when model contains no feature names: - bst.Tree.x <- bst.Tree - bst.Tree.x$feature_names <- NULL - dt.tree.x <- xgb.model.dt.tree(model = bst.Tree.x) + dt.tree.x <- xgb.model.dt.tree(model = bst.Tree.unnamed) expect_output(str(dt.tree.x), 'Feature.*\\"3\\"') expect_equal(dt.tree[, -4, with = FALSE], dt.tree.x[, -4, with = FALSE]) @@ -316,9 +314,7 @@ test_that("xgb.importance works with and without feature names", { expect_equal(importance.Tree, importance.Tree.0, tolerance = float_tolerance) # when model contains no feature names: - bst.Tree.x <- bst.Tree - bst.Tree.x$feature_names <- NULL - importance.Tree.x <- xgb.importance(model = bst.Tree) + importance.Tree.x <- xgb.importance(model = bst.Tree.unnamed) expect_equal(importance.Tree[, -1, with = FALSE], importance.Tree.x[, -1, with = FALSE], tolerance = float_tolerance) @@ -334,7 +330,7 @@ test_that("xgb.importance works with and without feature names", { importance <- xgb.importance(feature_names = feature.names, model = bst.Tree, trees = trees) importance_from_dump <- function() { - model_text_dump <- xgb.dump(model = bst.Tree, with_stats = TRUE, trees = trees) + model_text_dump <- xgb.dump(model = bst.Tree.unnamed, with_stats = TRUE, trees = trees) imp <- xgb.model.dt.tree( feature_names = feature.names, text = model_text_dump, @@ -414,13 +410,13 @@ test_that("xgb.plot.importance de-duplicates features", { test_that("xgb.plot.tree works with and without feature names", { .skip_if_vcd_not_available() - expect_silent(xgb.plot.tree(feature_names = feature.names, model = bst.Tree)) + expect_silent(xgb.plot.tree(feature_names = feature.names, model = bst.Tree.unnamed)) expect_silent(xgb.plot.tree(model = bst.Tree)) }) test_that("xgb.plot.multi.trees works with and without feature names", { .skip_if_vcd_not_available() - xgb.plot.multi.trees(model = bst.Tree, feature_names = feature.names, features_keep = 3) + xgb.plot.multi.trees(model = bst.Tree.unnamed, feature_names = feature.names, features_keep = 3) xgb.plot.multi.trees(model = bst.Tree, features_keep = 3) }) diff --git a/R-package/tests/testthat/test_io.R b/R-package/tests/testthat/test_io.R index 3c64ddc72..36a6d7572 100644 --- a/R-package/tests/testthat/test_io.R +++ b/R-package/tests/testthat/test_io.R @@ -17,8 +17,8 @@ test_that("load/save raw works", { ubj_bytes <- xgb.save.raw(booster, raw_format = "ubj") old_bytes <- xgb.save.raw(booster, raw_format = "deprecated") - from_json <- xgb.load.raw(json_bytes, as_booster = TRUE) - from_ubj <- xgb.load.raw(ubj_bytes, as_booster = TRUE) + from_json <- xgb.load.raw(json_bytes) + from_ubj <- xgb.load.raw(ubj_bytes) json2old <- xgb.save.raw(from_json, raw_format = "deprecated") ubj2old <- xgb.save.raw(from_ubj, raw_format = "deprecated") @@ -26,3 +26,46 @@ test_that("load/save raw works", { expect_equal(json2old, ubj2old) expect_equal(json2old, old_bytes) }) + +test_that("saveRDS preserves C and R attributes", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list(nthread = 1, max_depth = 2), + nrounds = 5 + ) + attributes(model)$my_attr <- "qwerty" + xgb.attr(model, "c_attr") <- "asdf" + + fname <- file.path(tempdir(), "xgb_model.Rds") + saveRDS(model, fname) + model_new <- readRDS(fname) + + expect_equal(attributes(model_new)$my_attr, attributes(model)$my_attr) + expect_equal(xgb.attr(model, "c_attr"), xgb.attr(model_new, "c_attr")) +}) + +test_that("R serializers keep C config", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + model <- xgb.train( + data = dm, + params = list( + tree_method = "approx", + nthread = 1, + max_depth = 2 + ), + nrounds = 3 + ) + model_new <- unserialize(serialize(model, NULL)) + expect_equal( + xgb.config(model)$learner$gradient_booster$gbtree_train_param$tree_method, + xgb.config(model_new)$learner$gradient_booster$gbtree_train_param$tree_method + ) + expect_equal(variable.names(model), variable.names(model_new)) +}) diff --git a/R-package/tests/testthat/test_model_compatibility.R b/R-package/tests/testthat/test_model_compatibility.R index ce1725dc9..613ba066f 100644 --- a/R-package/tests/testthat/test_model_compatibility.R +++ b/R-package/tests/testthat/test_model_compatibility.R @@ -23,11 +23,7 @@ get_num_tree <- function(booster) { } run_booster_check <- function(booster, name) { - # If given a handle, we need to call xgb.Booster.complete() prior to using xgb.config(). - if (inherits(booster, "xgb.Booster") && xgboost:::is.null.handle(booster$handle)) { - booster <- xgb.Booster.complete(booster) - } - config <- jsonlite::fromJSON(xgb.config(booster)) + config <- xgb.config(booster) run_model_param_check(config) if (name == 'cls') { testthat::expect_equal(get_num_tree(booster), @@ -76,6 +72,10 @@ test_that("Models from previous versions of XGBoost can be loaded", { name <- m[3] is_rds <- endsWith(model_file, '.rds') is_json <- endsWith(model_file, '.json') + # TODO: update this test for new RDS format + if (is_rds) { + return(NULL) + } # Expect an R warning when a model is loaded from RDS and it was generated by version < 1.1.x if (is_rds && compareVersion(model_xgb_ver, '1.1.1.1') < 0) { booster <- readRDS(model_file) diff --git a/R-package/tests/testthat/test_parameter_exposure.R b/R-package/tests/testthat/test_parameter_exposure.R index 5b12fde01..ed5c28ca5 100644 --- a/R-package/tests/testthat/test_parameter_exposure.R +++ b/R-package/tests/testthat/test_parameter_exposure.R @@ -19,12 +19,12 @@ bst <- xgb.train(data = dtrain, objective = "binary:logistic") test_that("call is exposed to R", { - expect_false(is.null(bst$call)) - expect_is(bst$call, "call") + expect_false(is.null(attributes(bst)$call)) + expect_is(attributes(bst)$call, "call") }) test_that("params is exposed to R", { - model_params <- bst$params + model_params <- attributes(bst)$params expect_is(model_params, "list") expect_equal(model_params$eta, 1) expect_equal(model_params$max_depth, 2) diff --git a/R-package/tests/testthat/test_ranking.R b/R-package/tests/testthat/test_ranking.R index d4102dfce..277c8f288 100644 --- a/R-package/tests/testthat/test_ranking.R +++ b/R-package/tests/testthat/test_ranking.R @@ -17,8 +17,8 @@ test_that('Test ranking with unweighted data', { eval_metric = 'auc', eval_metric = 'aucpr', nthread = n_threads) bst <- xgb.train(params, dtrain, nrounds = 10, watchlist = list(train = dtrain)) # Check if the metric is monotone increasing - expect_true(all(diff(bst$evaluation_log$train_auc) >= 0)) - expect_true(all(diff(bst$evaluation_log$train_aucpr) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_auc) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_aucpr) >= 0)) }) test_that('Test ranking with weighted data', { @@ -41,8 +41,8 @@ test_that('Test ranking with weighted data', { ) bst <- xgb.train(params, dtrain, nrounds = 10, watchlist = list(train = dtrain)) # Check if the metric is monotone increasing - expect_true(all(diff(bst$evaluation_log$train_auc) >= 0)) - expect_true(all(diff(bst$evaluation_log$train_aucpr) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_auc) >= 0)) + expect_true(all(diff(attributes(bst)$evaluation_log$train_aucpr) >= 0)) for (i in 1:10) { pred <- predict(bst, newdata = dtrain, ntreelimit = i) # is_sorted[i]: is i-th group correctly sorted by the ranking predictor? diff --git a/R-package/tests/testthat/test_update.R b/R-package/tests/testthat/test_update.R index f37bb0d21..3c88178e0 100644 --- a/R-package/tests/testthat/test_update.R +++ b/R-package/tests/testthat/test_update.R @@ -40,7 +40,12 @@ test_that("updating the model works", { bst1r <- xgb.train(p1r, dtrain, nrounds = 10, watchlist, verbose = 0) tr1r <- xgb.model.dt.tree(model = bst1r) # all should be the same when no subsampling - expect_equal(bst1$evaluation_log, bst1r$evaluation_log) + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1r)$evaluation_log) + expect_equal( + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1, raw_format = "json"))), + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1r, raw_format = "json"))), + tolerance = 1e-6 + ) if (!win32_flag) { expect_equal(tr1, tr1r, tolerance = 0.00001, check.attributes = FALSE) } @@ -51,7 +56,7 @@ test_that("updating the model works", { bst2r <- xgb.train(p2r, dtrain, nrounds = 10, watchlist, verbose = 0) tr2r <- xgb.model.dt.tree(model = bst2r) # should be the same evaluation but different gains and larger cover - expect_equal(bst2$evaluation_log, bst2r$evaluation_log) + expect_equal(attributes(bst2)$evaluation_log, attributes(bst2r)$evaluation_log) if (!win32_flag) { expect_equal(tr2[Feature == 'Leaf']$Gain, tr2r[Feature == 'Leaf']$Gain) } @@ -59,11 +64,25 @@ test_that("updating the model works", { expect_gt(sum(tr2r$Cover) / sum(tr2$Cover), 1.5) # process type 'update' for no-subsampling model, refreshing the tree stats AND leaves from training data: + set.seed(123) p1u <- modifyList(p1, list(process_type = 'update', updater = 'refresh', refresh_leaf = TRUE)) bst1u <- xgb.train(p1u, dtrain, nrounds = 10, watchlist, verbose = 0, xgb_model = bst1) tr1u <- xgb.model.dt.tree(model = bst1u) # all should be the same when no subsampling - expect_equal(bst1$evaluation_log, bst1u$evaluation_log) + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1u)$evaluation_log) + expect_equal( + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1, raw_format = "json"))), + jsonlite::fromJSON(rawToChar(xgb.save.raw(bst1u, raw_format = "json"))), + tolerance = 1e-6 + ) + expect_equal(tr1, tr1u, tolerance = 0.00001, check.attributes = FALSE) + + # same thing but with a serialized model + set.seed(123) + bst1u <- xgb.train(p1u, dtrain, nrounds = 10, watchlist, verbose = 0, xgb_model = xgb.save.raw(bst1)) + tr1u <- xgb.model.dt.tree(model = bst1u) + # all should be the same when no subsampling + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1u)$evaluation_log) expect_equal(tr1, tr1u, tolerance = 0.00001, check.attributes = FALSE) # process type 'update' for model with subsampling, refreshing only the tree stats from training data: @@ -71,12 +90,12 @@ test_that("updating the model works", { bst2u <- xgb.train(p2u, dtrain, nrounds = 10, watchlist, verbose = 0, xgb_model = bst2) tr2u <- xgb.model.dt.tree(model = bst2u) # should be the same evaluation but different gains and larger cover - expect_equal(bst2$evaluation_log, bst2u$evaluation_log) + expect_equal(attributes(bst2)$evaluation_log, attributes(bst2u)$evaluation_log) expect_equal(tr2[Feature == 'Leaf']$Gain, tr2u[Feature == 'Leaf']$Gain) expect_gt(sum(abs(tr2[Feature != 'Leaf']$Gain - tr2u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr2u$Cover) / sum(tr2$Cover), 1.5) # the results should be the same as for the model with an extra 'refresh' updater - expect_equal(bst2r$evaluation_log, bst2u$evaluation_log) + expect_equal(attributes(bst2r)$evaluation_log, attributes(bst2u)$evaluation_log) if (!win32_flag) { expect_equal(tr2r, tr2u, tolerance = 0.00001, check.attributes = FALSE) } @@ -86,7 +105,7 @@ test_that("updating the model works", { bst1ut <- xgb.train(p1ut, dtest, nrounds = 10, watchlist, verbose = 0, xgb_model = bst1) tr1ut <- xgb.model.dt.tree(model = bst1ut) # should be the same evaluations but different gains and smaller cover (test data is smaller) - expect_equal(bst1$evaluation_log, bst1ut$evaluation_log) + expect_equal(attributes(bst1)$evaluation_log, attributes(bst1ut)$evaluation_log) expect_equal(tr1[Feature == 'Leaf']$Gain, tr1ut[Feature == 'Leaf']$Gain) expect_gt(sum(abs(tr1[Feature != 'Leaf']$Gain - tr1ut[Feature != 'Leaf']$Gain)), 100) expect_lt(sum(tr1ut$Cover) / sum(tr1$Cover), 0.5) @@ -106,11 +125,12 @@ test_that("updating works for multiclass & multitree", { # run update process for an original model with subsampling p0u <- modifyList(p0, list(process_type = 'update', updater = 'refresh', refresh_leaf = FALSE)) - bst0u <- xgb.train(p0u, dtr, nrounds = bst0$niter, watchlist, xgb_model = bst0, verbose = 0) + bst0u <- xgb.train(p0u, dtr, nrounds = xgb.get.num.boosted.rounds(bst0), + watchlist, xgb_model = bst0, verbose = 0) tr0u <- xgb.model.dt.tree(model = bst0u) # should be the same evaluation but different gains and larger cover - expect_equal(bst0$evaluation_log, bst0u$evaluation_log) + expect_equal(attributes(bst0)$evaluation_log, attributes(bst0u)$evaluation_log) expect_equal(tr0[Feature == 'Leaf']$Gain, tr0u[Feature == 'Leaf']$Gain) expect_gt(sum(abs(tr0[Feature != 'Leaf']$Gain - tr0u[Feature != 'Leaf']$Gain)), 100) expect_gt(sum(tr0u$Cover) / sum(tr0$Cover), 1.5) diff --git a/R-package/vignettes/xgboost.Rnw b/R-package/vignettes/xgboost.Rnw deleted file mode 100644 index 7edf4ace3..000000000 --- a/R-package/vignettes/xgboost.Rnw +++ /dev/null @@ -1,223 +0,0 @@ -\documentclass{article} -\RequirePackage{url} -\usepackage{hyperref} -\RequirePackage{amsmath} -\RequirePackage{natbib} -\RequirePackage[a4paper,lmargin={1.25in},rmargin={1.25in},tmargin={1in},bmargin={1in}]{geometry} - -\makeatletter -% \VignetteIndexEntry{xgboost: eXtreme Gradient Boosting} -%\VignetteKeywords{xgboost, gbm, gradient boosting machines} -%\VignettePackage{xgboost} -% \VignetteEngine{knitr::knitr} -\makeatother - -\begin{document} -%\SweaveOpts{concordance=TRUE} - -<>= -if (require('knitr')) opts_chunk$set(fig.width = 5, fig.height = 5, fig.align = 'center', tidy = FALSE, warning = FALSE, cache = TRUE) -@ - -% -<>= -xgboost.version <- packageDescription("xgboost")$Version - -@ -% - - \begin{center} - \vspace*{6\baselineskip} - \rule{\textwidth}{1.6pt}\vspace*{-\baselineskip}\vspace*{2pt} - \rule{\textwidth}{0.4pt}\\[2\baselineskip] - {\LARGE \textbf{xgboost: eXtreme Gradient Boosting}}\\[1.2\baselineskip] - \rule{\textwidth}{0.4pt}\vspace*{-\baselineskip}\vspace{3.2pt} - \rule{\textwidth}{1.6pt}\\[2\baselineskip] - {\Large Tianqi Chen, Tong He}\\[\baselineskip] - {\large Package Version: \Sexpr{xgboost.version}}\\[\baselineskip] - {\large \today}\par - \vfill - \end{center} - -\thispagestyle{empty} - -\clearpage - -\setcounter{page}{1} - -\section{Introduction} - -This is an introductory document of using the \verb@xgboost@ package in R. - -\verb@xgboost@ is short for eXtreme Gradient Boosting package. It is an efficient - and scalable implementation of gradient boosting framework by \citep{friedman2001greedy} \citep{friedman2000additive}. -The package includes efficient linear model solver and tree learning algorithm. -It supports various objective functions, including regression, classification -and ranking. The package is made to be extendible, so that users are also allowed to define their own objectives easily. It has several features: -\begin{enumerate} - \item{Speed: }{\verb@xgboost@ can automatically do parallel computation on - Windows and Linux, with openmp. It is generally over 10 times faster than - \verb@gbm@.} - \item{Input Type: }{\verb@xgboost@ takes several types of input data:} - \begin{itemize} - \item{Dense Matrix: }{R's dense matrix, i.e. \verb@matrix@} - \item{Sparse Matrix: }{R's sparse matrix \verb@Matrix::dgCMatrix@} - \item{Data File: }{Local data files} - \item{xgb.DMatrix: }{\verb@xgboost@'s own class. Recommended.} - \end{itemize} - \item{Sparsity: }{\verb@xgboost@ accepts sparse input for both tree booster - and linear booster, and is optimized for sparse input.} - \item{Customization: }{\verb@xgboost@ supports customized objective function - and evaluation function} - \item{Performance: }{\verb@xgboost@ has better performance on several different - datasets.} -\end{enumerate} - - -\section{Example with Mushroom data} - -In this section, we will illustrate some common usage of \verb@xgboost@. The -Mushroom data is cited from UCI Machine Learning Repository. \citep{Bache+Lichman:2013} - -<>= -library(xgboost) -data(agaricus.train, package='xgboost') -data(agaricus.test, package='xgboost') -train <- agaricus.train -test <- agaricus.test -bst <- xgboost(data = train$data, label = train$label, max_depth = 2, eta = 1, - nrounds = 2, objective = "binary:logistic", nthread = 2) -xgb.save(bst, 'model.save') -bst = xgb.load('model.save') -xgb.parameters(bst) <- list(nthread = 2) -pred <- predict(bst, test$data) -@ - -\verb@xgboost@ is the main function to train a \verb@Booster@, i.e. a model. -\verb@predict@ does prediction on the model. - -Here we can save the model to a binary local file, and load it when needed. -We can't inspect the trees inside. However we have another function to save the -model in plain text. -<>= -xgb.dump(bst, 'model.dump') -@ - -The output looks like - -\begin{verbatim} -booster[0]: -0:[f28<1.00001] yes=1,no=2,missing=2 - 1:[f108<1.00001] yes=3,no=4,missing=4 - 3:leaf=1.85965 - 4:leaf=-1.94071 - 2:[f55<1.00001] yes=5,no=6,missing=6 - 5:leaf=-1.70044 - 6:leaf=1.71218 -booster[1]: -0:[f59<1.00001] yes=1,no=2,missing=2 - 1:leaf=-6.23624 - 2:[f28<1.00001] yes=3,no=4,missing=4 - 3:leaf=-0.96853 - 4:leaf=0.784718 -\end{verbatim} - -It is important to know \verb@xgboost@'s own data type: \verb@xgb.DMatrix@. -It speeds up \verb@xgboost@, and is needed for advanced features such as -training from initial prediction value, weighted training instance. - -We can use \verb@xgb.DMatrix@ to construct an \verb@xgb.DMatrix@ object: -<>= -dtrain <- xgb.DMatrix(train$data, label = train$label, nthread = 2) -class(dtrain) -head(getinfo(dtrain,'label')) -@ - -We can also save the matrix to a binary file. Then load it simply with -\verb@xgb.DMatrix@ -<>= -xgb.DMatrix.save(dtrain, 'xgb.DMatrix') -dtrain = xgb.DMatrix('xgb.DMatrix') -@ - -\section{Advanced Examples} - -The function \verb@xgboost@ is a simple function with less parameter, in order -to be R-friendly. The core training function is wrapped in \verb@xgb.train@. It is more flexible than \verb@xgboost@, but it requires users to read the document a bit more carefully. - -\verb@xgb.train@ only accept a \verb@xgb.DMatrix@ object as its input, while it supports advanced features as custom objective and evaluation functions. - -<>= -logregobj <- function(preds, dtrain) { - labels <- getinfo(dtrain, "label") - preds <- 1/(1 + exp(-preds)) - grad <- preds - labels - hess <- preds * (1 - preds) - return(list(grad = grad, hess = hess)) -} - -evalerror <- function(preds, dtrain) { - labels <- getinfo(dtrain, "label") - err <- sqrt(mean((preds-labels)^2)) - return(list(metric = "MSE", value = err)) -} - -dtest <- xgb.DMatrix(test$data, label = test$label, nthread = 2) -watchlist <- list(eval = dtest, train = dtrain) -param <- list(max_depth = 2, eta = 1, nthread = 2) - -bst <- xgb.train(param, dtrain, nrounds = 2, watchlist, logregobj, evalerror, maximize = FALSE) -@ - -The gradient and second order gradient is required for the output of customized -objective function. - -We also have \verb@slice@ for row extraction. It is useful in -cross-validation. - -For a walkthrough demo, please see \verb@R-package/demo/@ for further -details. - -\section{The Higgs Boson competition} - -We have made a demo for \href{http://www.kaggle.com/c/higgs-boson}{the Higgs -Boson Machine Learning Challenge}. - -Here are the instructions to make a submission -\begin{enumerate} - \item Download the \href{http://www.kaggle.com/c/higgs-boson/data}{datasets} - and extract them to \verb@data/@. - \item Run scripts under \verb@xgboost/demo/kaggle-higgs/@: - \href{https://github.com/tqchen/xgboost/blob/master/demo/kaggle-higgs/higgs-train.R}{higgs-train.R} - and \href{https://github.com/tqchen/xgboost/blob/master/demo/kaggle-higgs/higgs-pred.R}{higgs-pred.R}. - The computation will take less than a minute on Intel i7. - \item Go to the \href{http://www.kaggle.com/c/higgs-boson/submissions/attach}{submission page} - and submit your result. -\end{enumerate} - -We provide \href{https://github.com/tqchen/xgboost/blob/master/demo/kaggle-higgs/speedtest.R}{a script} -to compare the time cost on the higgs dataset with \verb@gbm@ and \verb@xgboost@. -The training set contains 350000 records and 30 features. - -\verb@xgboost@ can automatically do parallel computation. On a machine with Intel -i7-4700MQ and 24GB memories, we found that \verb@xgboost@ costs about 35 seconds, which is about 20 times faster -than \verb@gbm@. When we limited \verb@xgboost@ to use only one thread, it was -still about two times faster than \verb@gbm@. - -Meanwhile, the result from \verb@xgboost@ reaches -\href{http://www.kaggle.com/c/higgs-boson/details/evaluation}{3.60@AMS} with a -single model. This results stands in the -\href{http://www.kaggle.com/c/higgs-boson/leaderboard}{top 30\%} of the -competition. - -\bibliographystyle{jss} -\nocite{*} % list uncited references -\bibliography{xgboost} - -\end{document} - -<>= -file.remove("xgb.DMatrix") -file.remove("model.dump") -file.remove("model.save") -@ diff --git a/R-package/vignettes/xgboostPresentation.Rmd b/R-package/vignettes/xgboostPresentation.Rmd index 90393060f..efafc624d 100644 --- a/R-package/vignettes/xgboostPresentation.Rmd +++ b/R-package/vignettes/xgboostPresentation.Rmd @@ -107,7 +107,7 @@ train <- agaricus.train test <- agaricus.test ``` -> In the real world, it would be up to you to make this division between `train` and `test` data. The way to do it is out of the purpose of this article, however `caret` package may [help](http://topepo.github.io/caret/data-splitting.html). +> In the real world, it would be up to you to make this division between `train` and `test` data. Each variable is a `list` containing two things, `label` and `data`: @@ -155,11 +155,13 @@ We will train decision tree model using the following parameters: bstSparse <- xgboost( data = train$data , label = train$label - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" ) ``` @@ -175,11 +177,13 @@ Alternatively, you can put your dataset in a *dense* matrix, i.e. a basic **R** bstDense <- xgboost( data = as.matrix(train$data), label = train$label, - max_depth = 2, - eta = 1, - nthread = 2, - nrounds = 2, - objective = "binary:logistic" + params = list( + max_depth = 2, + eta = 1, + nthread = 2, + objective = "binary:logistic" + ), + nrounds = 2 ) ``` @@ -191,11 +195,13 @@ bstDense <- xgboost( dtrain <- xgb.DMatrix(data = train$data, label = train$label, nthread = 2) bstDMatrix <- xgboost( data = dtrain, - max_depth = 2, - eta = 1, - nthread = 2, - nrounds = 2, - objective = "binary:logistic" + params = list( + max_depth = 2, + eta = 1, + nthread = 2, + objective = "binary:logistic" + ), + nrounds = 2 ) ``` @@ -209,11 +215,13 @@ One of the simplest way to see the training progress is to set the `verbose` opt # verbose = 0, no message bst <- xgboost( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" , verbose = 0 ) ``` @@ -222,11 +230,13 @@ bst <- xgboost( # verbose = 1, print evaluation metric bst <- xgboost( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" , verbose = 1 ) ``` @@ -235,11 +245,13 @@ bst <- xgboost( # verbose = 2, also print information about tree bst <- xgboost( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 - , objective = "binary:logistic" , verbose = 2 ) ``` @@ -336,12 +348,14 @@ watchlist <- list(train = dtrain, test = dtest) bst <- xgb.train( data = dtrain - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 , watchlist = watchlist - , objective = "binary:logistic" ) ``` @@ -349,7 +363,7 @@ bst <- xgb.train( Both training and test error related metrics are very similar, and in some way, it makes sense: what we have learned from the training dataset matches the observations from the test dataset. -If with your own dataset you have not such results, you should think about how you divided your dataset in training and test. May be there is something to fix. Again, `caret` package may [help](http://topepo.github.io/caret/data-splitting.html). +If with your own dataset you have not such results, you should think about how you divided your dataset in training and test. May be there is something to fix. For a better understanding of the learning progression, you may want to have some specific metric or even use multiple evaluation metrics. @@ -357,13 +371,15 @@ For a better understanding of the learning progression, you may want to have som bst <- xgb.train( data = dtrain , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + eta = 1 + , nthread = 2 + , objective = "binary:logistic" + , eval_metric = "error" + , eval_metric = "logloss" + ) , nrounds = 2 , watchlist = watchlist - , eval_metric = "error" - , eval_metric = "logloss" - , objective = "binary:logistic" ) ``` @@ -377,14 +393,15 @@ Until now, all the learnings we have performed were based on boosting trees. **X ```{r linearBoosting, message=F, warning=F} bst <- xgb.train( data = dtrain - , booster = "gblinear" - , max_depth = 2 - , nthread = 2 + , params = list( + booster = "gblinear" + , nthread = 2 + , objective = "binary:logistic" + , eval_metric = "error" + , eval_metric = "logloss" + ) , nrounds = 2 , watchlist = watchlist - , eval_metric = "error" - , eval_metric = "logloss" - , objective = "binary:logistic" ) ``` @@ -406,12 +423,14 @@ xgb.DMatrix.save(dtrain, fname) dtrain2 <- xgb.DMatrix(fname) bst <- xgb.train( data = dtrain2 - , max_depth = 2 - , eta = 1 - , nthread = 2 + , params = list( + max_depth = 2 + , eta = 1 + , nthread = 2 + , objective = "binary:logistic" + ) , nrounds = 2 , watchlist = watchlist - , objective = "binary:logistic" ) ``` @@ -492,17 +511,17 @@ file.remove(fname) > result is `0`? We are good! -In some very specific cases, like when you want to pilot **XGBoost** from `caret` package, you will want to save the model as a *R* binary vector. See below how to do it. +In some very specific cases, you will want to save the model as a *R* binary vector. See below how to do it. ```{r saveLoadRBinVectorModel, message=F, warning=F} # save model to R's raw vector -rawVec <- xgb.serialize(bst) +rawVec <- xgb.save.raw(bst) # print class print(class(rawVec)) # load binary model to R -bst3 <- xgb.load(rawVec) +bst3 <- xgb.load.raw(rawVec) xgb.parameters(bst3) <- list(nthread = 2) pred3 <- predict(bst3, test$data) diff --git a/R-package/vignettes/xgboostfromJSON.Rmd b/R-package/vignettes/xgboostfromJSON.Rmd index f5bc3ad9b..e5331b0ff 100644 --- a/R-package/vignettes/xgboostfromJSON.Rmd +++ b/R-package/vignettes/xgboostfromJSON.Rmd @@ -53,11 +53,10 @@ labels <- c(1, 1, 1, data <- data.frame(dates = dates, labels = labels) bst <- xgb.train( - data = xgb.DMatrix(as.matrix(data$dates), label = labels), + data = xgb.DMatrix(as.matrix(data$dates), label = labels, missing = NA), nthread = 2, nrounds = 1, objective = "binary:logistic", - missing = NA, max_depth = 1 ) ``` From 73b3955dd48c85e41a701c8a9869d27a4d5b1101 Mon Sep 17 00:00:00 2001 From: rpopescu Date: Thu, 11 Jan 2024 05:10:21 +0100 Subject: [PATCH 091/109] Fix FieldEntry ctor specialisation syntax error (#9980) Co-authored-by: Radu Popescu --- include/xgboost/parameter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xgboost/parameter.h b/include/xgboost/parameter.h index fca05cef4..063f1aca7 100644 --- a/include/xgboost/parameter.h +++ b/include/xgboost/parameter.h @@ -53,7 +53,7 @@ namespace parameter { \ template <> \ class FieldEntry : public FieldEntry { \ public: \ - FieldEntry() { \ + FieldEntry() { \ static_assert( \ std::is_same::type>::value, \ "enum class must be backed by int"); \ From f88c43801fd76551f13dcbfe09be84c41b2e2a44 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Fri, 12 Jan 2024 06:04:32 +0800 Subject: [PATCH 092/109] [jvm-packages] update rapids dep to 23.12.1 (#9951) With this PR, XGBoost GPU can support scala 2.13 --- jvm-packages/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index 1acff2240..7655a7170 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -43,9 +43,9 @@ 5 OFF OFF - 23.10.0 - 23.10.0 - cuda11 + 23.12.1 + 23.12.1 + cuda12 3.2.17 2.11.0 From c42c7d99f159a4f4f07c0e0b87417e056b302f01 Mon Sep 17 00:00:00 2001 From: Hui Liu <96135754+hliuca@users.noreply.github.com> Date: Thu, 11 Jan 2024 14:10:30 -0800 Subject: [PATCH 093/109] fix memoryType --- cmake/Utils.cmake | 4 ++-- rocgputreeshap | 2 +- src/data/array_interface.cu | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/cmake/Utils.cmake b/cmake/Utils.cmake index 19ccdac8a..f295d1446 100644 --- a/cmake/Utils.cmake +++ b/cmake/Utils.cmake @@ -214,12 +214,12 @@ function(xgboost_link_rccl target) endif() if(BUILD_STATIC_LIB) - target_include_directories(${target} PUBLIC ${RCCL_INCLUDE_DIR}) + target_include_directories(${target} PUBLIC ${RCCL_INCLUDE_DIR}/rccl) target_compile_definitions(${target} PUBLIC ${xgboost_rccl_flags}) target_link_directories(${target} PUBLIC ${HIP_LIB_INSTALL_DIR}) target_link_libraries(${target} PUBLIC ${RCCL_LIBRARY}) else() - target_include_directories(${target} PRIVATE ${RCCL_INCLUDE_DIR}) + target_include_directories(${target} PRIVATE ${RCCL_INCLUDE_DIR}/rccl) target_compile_definitions(${target} PRIVATE ${xgboost_rccl_flags}) target_link_directories(${target} PUBLIC ${HIP_LIB_INSTALL_DIR}) if(NOT USE_DLOPEN_RCCL) diff --git a/rocgputreeshap b/rocgputreeshap index 6ceffde02..2fea6734e 160000 --- a/rocgputreeshap +++ b/rocgputreeshap @@ -1 +1 @@ -Subproject commit 6ceffde024f8752954550ebcca98caa24b5d158d +Subproject commit 2fea6734e83cf147c1bbe580ac4713cd50abcad5 diff --git a/src/data/array_interface.cu b/src/data/array_interface.cu index b29987ff4..569196407 100644 --- a/src/data/array_interface.cu +++ b/src/data/array_interface.cu @@ -20,7 +20,6 @@ void ArrayInterfaceHandler::SyncCudaStream(std::int64_t stream) { * case where 0 might be given should either use None, 1, or 2 instead for * clarity. */ - /* ignored for HIP */ #if !defined(XGBOOST_USE_HIP) LOG(FATAL) << "Invalid stream ID in array interface: " << stream; #endif @@ -42,7 +41,7 @@ bool ArrayInterfaceHandler::IsCudaPtr(void const* ptr) { return false; } -#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) +#if defined(XGBOOST_USE_CUDA) cudaPointerAttributes attr; auto err = cudaPointerGetAttributes(&attr, ptr); // reset error @@ -64,6 +63,35 @@ bool ArrayInterfaceHandler::IsCudaPtr(void const* ptr) { // other errors, `cudaErrorNoDevice`, `cudaErrorInsufficientDriver` etc. return false; } +#elif defined(XGBOOST_USE_HIP) + hipPointerAttribute_t attr; + auto err = hipPointerGetAttributes(&attr, ptr); + // reset error + CHECK_EQ(err, hipGetLastError()); + if (err == hipErrorInvalidValue) { + return false; + } else if (err == hipSuccess) { +#if HIP_VERSION_MAJOR < 6 + switch (attr.memoryType) { + case hipMemoryTypeUnified: + case hipMemoryTypeHost: + return false; + default: + return true; + } +#else + switch (attr.type) { + case hipMemoryTypeUnified: + case hipMemoryTypeHost: + return false; + default: + return true; + } +#endif + return true; + } else { + return false; + } #endif } } // namespace xgboost From 1168a688726aca01e6d52dd931bb63df06e3f110 Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Fri, 12 Jan 2024 10:37:55 -0800 Subject: [PATCH 094/109] [jvm-packages] Update release scripts (#9983) * [jvm-packages] Add Scala version suffix to xgboost-jvm package (#9776) * Update JVM script (#9714) * Revamp pom.xml * Update instructions in prepare_jvm_release.py * Fix formatting * [jvm-packages] Fix POM for xgboost-jvm metapackage (#9893) * [jvm-packages] Fix POM for xgboost-jvm metapackage * Add script for updating the Scala version * Update change_scala_version.py to also change scala.version property (#9897) * Remove 'release-cpu-only' profile * Remove scala-2.13 profile; enable gpu package for Scala 2.13 --- dev/change_scala_version.py | 79 ++++++++++++++++++++++++ dev/prepare_jvm_release.py | 36 ++++++++--- jvm-packages/create_jni.py | 65 ++++++++++++------- jvm-packages/pom.xml | 10 +-- jvm-packages/xgboost4j-example/pom.xml | 8 +-- jvm-packages/xgboost4j-flink/pom.xml | 6 +- jvm-packages/xgboost4j-gpu/pom.xml | 4 +- jvm-packages/xgboost4j-spark-gpu/pom.xml | 6 +- jvm-packages/xgboost4j-spark/pom.xml | 6 +- jvm-packages/xgboost4j/pom.xml | 4 +- tests/buildkite/build-jvm-packages.sh | 7 ++- tests/ci_build/build_jvm_packages.sh | 7 ++- tests/ci_build/deploy_jvm_packages.sh | 5 +- tests/ci_build/test_jvm_cross.sh | 9 +-- 14 files changed, 184 insertions(+), 68 deletions(-) create mode 100644 dev/change_scala_version.py diff --git a/dev/change_scala_version.py b/dev/change_scala_version.py new file mode 100644 index 000000000..d9438f76a --- /dev/null +++ b/dev/change_scala_version.py @@ -0,0 +1,79 @@ +import argparse +import pathlib +import re +import shutil + + +def main(args): + if args.scala_version == "2.12": + scala_ver = "2.12" + scala_patchver = "2.12.18" + elif args.scala_version == "2.13": + scala_ver = "2.13" + scala_patchver = "2.13.11" + else: + raise ValueError(f"Unsupported Scala version: {args.scala_version}") + + # Clean artifacts + if args.purge_artifacts: + for target in pathlib.Path("jvm-packages/").glob("**/target"): + if target.is_dir(): + print(f"Removing {target}...") + shutil.rmtree(target) + + # Update pom.xml + for pom in pathlib.Path("jvm-packages/").glob("**/pom.xml"): + print(f"Updating {pom}...") + with open(pom, "r", encoding="utf-8") as f: + lines = f.readlines() + with open(pom, "w", encoding="utf-8") as f: + replaced_scalaver = False + replaced_scala_binver = False + for line in lines: + for artifact in [ + "xgboost-jvm", + "xgboost4j", + "xgboost4j-gpu", + "xgboost4j-spark", + "xgboost4j-spark-gpu", + "xgboost4j-flink", + "xgboost4j-example", + ]: + line = re.sub( + f"{artifact}_[0-9\\.]*", + f"{artifact}_{scala_ver}", + line, + ) + # Only replace the first occurrence of scala.version + if not replaced_scalaver: + line, nsubs = re.subn( + r"[0-9\.]*", + f"{scala_patchver}", + line, + ) + if nsubs > 0: + replaced_scalaver = True + # Only replace the first occurrence of scala.binary.version + if not replaced_scala_binver: + line, nsubs = re.subn( + r"[0-9\.]*", + f"{scala_ver}", + line, + ) + if nsubs > 0: + replaced_scala_binver = True + f.write(line) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--purge-artifacts", action="store_true") + parser.add_argument( + "--scala-version", + type=str, + required=True, + help="Version of Scala to use in the JVM packages", + choices=["2.12", "2.13"], + ) + parsed_args = parser.parse_args() + main(parsed_args) diff --git a/dev/prepare_jvm_release.py b/dev/prepare_jvm_release.py index 0cf5796a2..5d4d2e66f 100644 --- a/dev/prepare_jvm_release.py +++ b/dev/prepare_jvm_release.py @@ -2,7 +2,6 @@ import argparse import errno import glob import os -import platform import re import shutil import subprocess @@ -88,10 +87,6 @@ def main(): help="Version of the release being prepared", ) args = parser.parse_args() - - if sys.platform != "darwin" or platform.machine() != "arm64": - raise NotImplementedError("Please run this script using an M1 Mac") - version = args.release_version expected_git_tag = "v" + version current_git_tag = get_current_git_tag() @@ -141,6 +136,7 @@ def main(): ("linux", "x86_64"), ("windows", "x86_64"), ("macos", "x86_64"), + ("macos", "aarch64"), ]: output_dir = f"xgboost4j/src/main/resources/lib/{os_ident}/{arch}" maybe_makedirs(output_dir) @@ -164,6 +160,10 @@ def main(): url=f"{nightly_bucket_prefix}/{git_branch}/libxgboost4j/libxgboost4j_{commit_hash}.dylib", filename="xgboost4j/src/main/resources/lib/macos/x86_64/libxgboost4j.dylib", ) + retrieve( + url=f"{nightly_bucket_prefix}/{git_branch}/libxgboost4j/libxgboost4j_m1_{commit_hash}.dylib", + filename="xgboost4j/src/main/resources/lib/macos/aarch64/libxgboost4j.dylib", + ) with tempfile.TemporaryDirectory() as tempdir: # libxgboost4j.so for Linux x86_64, CPU only @@ -210,13 +210,31 @@ def main(): "2. Store the Sonatype credentials in .m2/settings.xml. See insturctions in " "https://central.sonatype.org/publish/publish-maven/" ) - print("3. Now on a Mac machine, run:") - print(" GPG_TTY=$(tty) mvn deploy -Prelease -DskipTests") + print( + "3. Now on a Linux machine, run the following to build Scala 2.12 artifacts. " + "Make sure to use an Internet connection with fast upload speed:" + ) + print( + " # Skip native build, since we have all needed native binaries from CI\n" + " export MAVEN_SKIP_NATIVE_BUILD=1\n" + " GPG_TTY=$(tty) mvn deploy -Prelease -DskipTests" + ) print( "4. Log into https://oss.sonatype.org/. On the left menu panel, click Staging " - "Repositories. Visit the URL https://oss.sonatype.org/content/repositories/mldmlc-1085 " + "Repositories. Visit the URL https://oss.sonatype.org/content/repositories/mldmlc-xxxx " "to inspect the staged JAR files. Finally, press Release button to publish the " - "artifacts to the Maven Central repository." + "artifacts to the Maven Central repository. The top-level metapackage should be " + "named xgboost-jvm_2.12." + ) + print( + "5. Remove the Scala 2.12 artifacts and build Scala 2.13 artifacts:\n" + " export MAVEN_SKIP_NATIVE_BUILD=1\n" + " python dev/change_scala_version.py --scala-version 2.13 --purge-artifacts\n" + " GPG_TTY=$(tty) mvn deploy -Prelease -DskipTests" + ) + print( + "6. Go to https://oss.sonatype.org/ to release the Scala 2.13 artifacts. " + "The top-level metapackage should be named xgboost-jvm_2.13." ) diff --git a/jvm-packages/create_jni.py b/jvm-packages/create_jni.py index 3692cb13c..c39d354cf 100755 --- a/jvm-packages/create_jni.py +++ b/jvm-packages/create_jni.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import errno import argparse +import errno import glob import os import platform @@ -19,11 +19,10 @@ CONFIG = { "USE_HDFS": "OFF", "USE_AZURE": "OFF", "USE_S3": "OFF", - "USE_CUDA": "OFF", "USE_NCCL": "OFF", "JVM_BINDINGS": "ON", - "LOG_CAPI_INVOCATION": "OFF" + "LOG_CAPI_INVOCATION": "OFF", } @@ -70,26 +69,22 @@ def normpath(path): return normalized -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--log-capi-invocation', type=str, choices=['ON', 'OFF'], default='OFF') - parser.add_argument('--use-cuda', type=str, choices=['ON', 'OFF'], default='OFF') - cli_args = parser.parse_args() - +def native_build(args): if sys.platform == "darwin": # Enable of your compiler supports OpenMP. CONFIG["USE_OPENMP"] = "OFF" - os.environ["JAVA_HOME"] = subprocess.check_output( - "/usr/libexec/java_home").strip().decode() + os.environ["JAVA_HOME"] = ( + subprocess.check_output("/usr/libexec/java_home").strip().decode() + ) print("building Java wrapper") with cd(".."): - build_dir = 'build-gpu' if cli_args.use_cuda == 'ON' else 'build' + build_dir = "build-gpu" if cli_args.use_cuda == "ON" else "build" maybe_makedirs(build_dir) with cd(build_dir): if sys.platform == "win32": # Force x64 build on Windows. - maybe_generator = ' -A x64' + maybe_generator = " -A x64" else: maybe_generator = "" if sys.platform == "linux": @@ -97,12 +92,12 @@ if __name__ == "__main__": else: maybe_parallel_build = "" - if cli_args.log_capi_invocation == 'ON': - CONFIG['LOG_CAPI_INVOCATION'] = 'ON' + if cli_args.log_capi_invocation == "ON": + CONFIG["LOG_CAPI_INVOCATION"] = "ON" - if cli_args.use_cuda == 'ON': - CONFIG['USE_CUDA'] = 'ON' - CONFIG['USE_NCCL'] = 'ON' + if cli_args.use_cuda == "ON": + CONFIG["USE_CUDA"] = "ON" + CONFIG["USE_NCCL"] = "ON" CONFIG["USE_DLOPEN_NCCL"] = "OFF" args = ["-D{0}:BOOL={1}".format(k, v) for k, v in CONFIG.items()] @@ -116,7 +111,7 @@ if __name__ == "__main__": if gpu_arch_flag is not None: args.append("%s" % gpu_arch_flag) - lib_dir = os.path.join(os.pardir, 'lib') + lib_dir = os.path.join(os.pardir, "lib") if os.path.exists(lib_dir): shutil.rmtree(lib_dir) run("cmake .. " + " ".join(args) + maybe_generator) @@ -126,8 +121,10 @@ if __name__ == "__main__": run(f'"{sys.executable}" mapfeat.py') run(f'"{sys.executable}" mknfold.py machine.txt 1') - xgboost4j = 'xgboost4j-gpu' if cli_args.use_cuda == 'ON' else 'xgboost4j' - xgboost4j_spark = 'xgboost4j-spark-gpu' if cli_args.use_cuda == 'ON' else 'xgboost4j-spark' + xgboost4j = "xgboost4j-gpu" if cli_args.use_cuda == "ON" else "xgboost4j" + xgboost4j_spark = ( + "xgboost4j-spark-gpu" if cli_args.use_cuda == "ON" else "xgboost4j-spark" + ) print("copying native library") library_name, os_folder = { @@ -142,14 +139,19 @@ if __name__ == "__main__": "i86pc": "x86_64", # on Solaris x86_64 "sun4v": "sparc", # on Solaris sparc "arm64": "aarch64", # on macOS & Windows ARM 64-bit - "aarch64": "aarch64" + "aarch64": "aarch64", }[platform.machine().lower()] - output_folder = "{}/src/main/resources/lib/{}/{}".format(xgboost4j, os_folder, arch_folder) + output_folder = "{}/src/main/resources/lib/{}/{}".format( + xgboost4j, os_folder, arch_folder + ) maybe_makedirs(output_folder) cp("../lib/" + library_name, output_folder) print("copying pure-Python tracker") - cp("../python-package/xgboost/tracker.py", "{}/src/main/resources".format(xgboost4j)) + cp( + "../python-package/xgboost/tracker.py", + "{}/src/main/resources".format(xgboost4j), + ) print("copying train/test files") maybe_makedirs("{}/src/test/resources".format(xgboost4j_spark)) @@ -165,3 +167,18 @@ if __name__ == "__main__": maybe_makedirs("{}/src/test/resources".format(xgboost4j)) for file in glob.glob("../demo/data/agaricus.*"): cp(file, "{}/src/test/resources".format(xgboost4j)) + + +if __name__ == "__main__": + if "MAVEN_SKIP_NATIVE_BUILD" in os.environ: + print("MAVEN_SKIP_NATIVE_BUILD is set. Skipping native build...") + else: + parser = argparse.ArgumentParser() + parser.add_argument( + "--log-capi-invocation", type=str, choices=["ON", "OFF"], default="OFF" + ) + parser.add_argument( + "--use-cuda", type=str, choices=["ON", "OFF"], default="OFF" + ) + cli_args = parser.parse_args() + native_build(cli_args) diff --git a/jvm-packages/pom.xml b/jvm-packages/pom.xml index 7655a7170..23ab70734 100644 --- a/jvm-packages/pom.xml +++ b/jvm-packages/pom.xml @@ -5,7 +5,7 @@ 4.0.0 ml.dmlc - xgboost-jvm + xgboost-jvm_2.12 2.1.0-SNAPSHOT pom XGBoost JVM Package @@ -90,14 +90,6 @@ - - scala-2.13 - - 2.13 - 2.13.11 - - - gpu diff --git a/jvm-packages/xgboost4j-example/pom.xml b/jvm-packages/xgboost4j-example/pom.xml index 3a56615d6..431c6766a 100644 --- a/jvm-packages/xgboost4j-example/pom.xml +++ b/jvm-packages/xgboost4j-example/pom.xml @@ -5,11 +5,11 @@ 4.0.0 ml.dmlc - xgboost-jvm + xgboost-jvm_2.12 2.1.0-SNAPSHOT xgboost4j-example - xgboost4j-example_${scala.binary.version} + xgboost4j-example_2.12 2.1.0-SNAPSHOT jar @@ -26,7 +26,7 @@ ml.dmlc - xgboost4j-spark_${scala.binary.version} + xgboost4j-spark_2.12 ${project.version} @@ -37,7 +37,7 @@ ml.dmlc - xgboost4j-flink_${scala.binary.version} + xgboost4j-flink_2.12 ${project.version} diff --git a/jvm-packages/xgboost4j-flink/pom.xml b/jvm-packages/xgboost4j-flink/pom.xml index 6f700ca0a..e3dfb3830 100644 --- a/jvm-packages/xgboost4j-flink/pom.xml +++ b/jvm-packages/xgboost4j-flink/pom.xml @@ -5,12 +5,12 @@ 4.0.0 ml.dmlc - xgboost-jvm + xgboost-jvm_2.12 2.1.0-SNAPSHOT xgboost4j-flink - xgboost4j-flink_${scala.binary.version} + xgboost4j-flink_2.12 2.1.0-SNAPSHOT 2.2.0 @@ -30,7 +30,7 @@ ml.dmlc - xgboost4j_${scala.binary.version} + xgboost4j_2.12 ${project.version} diff --git a/jvm-packages/xgboost4j-gpu/pom.xml b/jvm-packages/xgboost4j-gpu/pom.xml index 13f9797cd..fc55dd156 100644 --- a/jvm-packages/xgboost4j-gpu/pom.xml +++ b/jvm-packages/xgboost4j-gpu/pom.xml @@ -5,10 +5,10 @@ 4.0.0 ml.dmlc - xgboost-jvm + xgboost-jvm_2.12 2.1.0-SNAPSHOT - xgboost4j-gpu_${scala.binary.version} + xgboost4j-gpu_2.12 xgboost4j-gpu 2.1.0-SNAPSHOT jar diff --git a/jvm-packages/xgboost4j-spark-gpu/pom.xml b/jvm-packages/xgboost4j-spark-gpu/pom.xml index a29b4e056..149f2f3a3 100644 --- a/jvm-packages/xgboost4j-spark-gpu/pom.xml +++ b/jvm-packages/xgboost4j-spark-gpu/pom.xml @@ -5,11 +5,11 @@ 4.0.0 ml.dmlc - xgboost-jvm + xgboost-jvm_2.12 2.1.0-SNAPSHOT xgboost4j-spark-gpu - xgboost4j-spark-gpu_${scala.binary.version} + xgboost4j-spark-gpu_2.12 @@ -24,7 +24,7 @@ ml.dmlc - xgboost4j-gpu_${scala.binary.version} + xgboost4j-gpu_2.12 ${project.version} diff --git a/jvm-packages/xgboost4j-spark/pom.xml b/jvm-packages/xgboost4j-spark/pom.xml index 179b1c762..6f16335f0 100644 --- a/jvm-packages/xgboost4j-spark/pom.xml +++ b/jvm-packages/xgboost4j-spark/pom.xml @@ -5,11 +5,11 @@ 4.0.0 ml.dmlc - xgboost-jvm + xgboost-jvm_2.12 2.1.0-SNAPSHOT xgboost4j-spark - xgboost4j-spark_${scala.binary.version} + xgboost4j-spark_2.12 @@ -24,7 +24,7 @@ ml.dmlc - xgboost4j_${scala.binary.version} + xgboost4j_2.12 ${project.version} diff --git a/jvm-packages/xgboost4j/pom.xml b/jvm-packages/xgboost4j/pom.xml index e05bbcf48..7eb186919 100644 --- a/jvm-packages/xgboost4j/pom.xml +++ b/jvm-packages/xgboost4j/pom.xml @@ -5,11 +5,11 @@ 4.0.0 ml.dmlc - xgboost-jvm + xgboost-jvm_2.12 2.1.0-SNAPSHOT xgboost4j - xgboost4j_${scala.binary.version} + xgboost4j_2.12 2.1.0-SNAPSHOT jar diff --git a/tests/buildkite/build-jvm-packages.sh b/tests/buildkite/build-jvm-packages.sh index 12393c561..1998385c5 100755 --- a/tests/buildkite/build-jvm-packages.sh +++ b/tests/buildkite/build-jvm-packages.sh @@ -8,13 +8,18 @@ echo "--- Build XGBoost JVM packages scala 2.12" tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_packages.sh \ ${SPARK_VERSION} +echo "--- Stash XGBoost4J JARs (Scala 2.12)" +buildkite-agent artifact upload "jvm-packages/xgboost4j/target/*.jar" +buildkite-agent artifact upload "jvm-packages/xgboost4j-spark/target/*.jar" +buildkite-agent artifact upload "jvm-packages/xgboost4j-flink/target/*.jar" +buildkite-agent artifact upload "jvm-packages/xgboost4j-example/target/*.jar" echo "--- Build XGBoost JVM packages scala 2.13" tests/ci_build/ci_build.sh jvm tests/ci_build/build_jvm_packages.sh \ ${SPARK_VERSION} "" "" "true" -echo "--- Stash XGBoost4J JARs" +echo "--- Stash XGBoost4J JARs (Scala 2.13)" buildkite-agent artifact upload "jvm-packages/xgboost4j/target/*.jar" buildkite-agent artifact upload "jvm-packages/xgboost4j-spark/target/*.jar" buildkite-agent artifact upload "jvm-packages/xgboost4j-flink/target/*.jar" diff --git a/tests/ci_build/build_jvm_packages.sh b/tests/ci_build/build_jvm_packages.sh index 5797a1f61..84b41f2b1 100755 --- a/tests/ci_build/build_jvm_packages.sh +++ b/tests/ci_build/build_jvm_packages.sh @@ -24,12 +24,13 @@ if [ "x$gpu_arch" != "x" ]; then export GPU_ARCH_FLAG=$gpu_arch fi -mvn_profile_string="" if [ "x$use_scala213" != "x" ]; then - export mvn_profile_string="-Pdefault,scala-2.13" + cd .. + python dev/change_scala_version.py --scala-version 2.13 --purge-artifacts + cd jvm-packages fi -mvn --no-transfer-progress package $mvn_profile_string -Dspark.version=${spark_version} $gpu_options +mvn --no-transfer-progress package -Dspark.version=${spark_version} $gpu_options set +x set +e diff --git a/tests/ci_build/deploy_jvm_packages.sh b/tests/ci_build/deploy_jvm_packages.sh index 5f448ee2a..9531d79a9 100755 --- a/tests/ci_build/deploy_jvm_packages.sh +++ b/tests/ci_build/deploy_jvm_packages.sh @@ -27,7 +27,10 @@ rm -rf ../build/ # Deploy to S3 bucket xgboost-maven-repo mvn --no-transfer-progress package deploy -P default,gpu,release-to-s3 -Dspark.version=${spark_version} -DskipTests # Deploy scala 2.13 to S3 bucket xgboost-maven-repo -mvn --no-transfer-progress package deploy -P release-to-s3,default,scala-2.13 -Dspark.version=${spark_version} -DskipTests +cd .. +python dev/change_scala_version.py --scala-version 2.13 --purge-artifacts +cd jvm-packages/ +mvn --no-transfer-progress package deploy -P default,gpu,release-to-s3 -Dspark.version=${spark_version} -DskipTests set +x diff --git a/tests/ci_build/test_jvm_cross.sh b/tests/ci_build/test_jvm_cross.sh index 18265cf01..4e049fce1 100755 --- a/tests/ci_build/test_jvm_cross.sh +++ b/tests/ci_build/test_jvm_cross.sh @@ -20,10 +20,11 @@ if [ ! -z "$RUN_INTEGRATION_TEST" ]; then cd $jvm_packages_dir fi -# including maven profiles for different scala versions: 2.12 is the default at the moment. -for _maven_profile_string in "" "-Pdefault,scala-2.13"; do - scala_version=$(mvn help:evaluate $_maven_profile_string -Dexpression=scala.version -q -DforceStdout) - scala_binary_version=$(mvn help:evaluate $_maven_profile_string -Dexpression=scala.binary.version -q -DforceStdout) +for scala_binary_version in "2.12" "2.13"; do + cd .. + python dev/change_scala_version.py --scala-version ${scala_binary_version} + cd jvm-packages + scala_version=$(mvn help:evaluate -Dexpression=scala.version -q -DforceStdout) # Install XGBoost4J JAR into local Maven repository mvn --no-transfer-progress install:install-file -Dfile=./xgboost4j/target/xgboost4j_${scala_binary_version}-${xgboost4j_version}.jar -DgroupId=ml.dmlc -DartifactId=xgboost4j_${scala_binary_version} -Dversion=${xgboost4j_version} -Dpackaging=jar From 9759e28e6aa487c9ecc82e6452d875023eeefaab Mon Sep 17 00:00:00 2001 From: Hui Liu <96135754+hliuca@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:09:01 -0800 Subject: [PATCH 095/109] compiler errors fix --- src/c_api/c_api.cu | 2 + src/collective/nccl_stub.cc | 12 ++- src/collective/nccl_stub.h | 9 +- src/common/algorithm.cuh | 47 ++++++++++ src/common/device_helpers.hip.h | 90 ++++--------------- src/data/array_interface.cc | 2 +- src/objective/hinge.cu | 2 +- .../cpp/objective/test_multiclass_obj_gpu.hip | 2 +- .../cpp/objective/test_regression_obj_cpu.cc | 4 +- .../cpp/objective/test_regression_obj_gpu.hip | 2 +- 10 files changed, 90 insertions(+), 82 deletions(-) diff --git a/src/c_api/c_api.cu b/src/c_api/c_api.cu index 471e09fc9..ebcea1c2f 100644 --- a/src/c_api/c_api.cu +++ b/src/c_api/c_api.cu @@ -17,6 +17,8 @@ #include "xgboost/learner.h" #if defined(XGBOOST_USE_NCCL) #include +#elif defined(XGBOOST_USE_RCCL) +#include #endif namespace xgboost { diff --git a/src/collective/nccl_stub.cc b/src/collective/nccl_stub.cc index 5101234a4..408432438 100644 --- a/src/collective/nccl_stub.cc +++ b/src/collective/nccl_stub.cc @@ -1,15 +1,25 @@ /** * Copyright 2023, XGBoost Contributors */ -#if defined(XGBOOST_USE_NCCL) || (defined(XGBOOST_USE_RCCL) && 0) +#if defined(XGBOOST_USE_NCCL) || defined(XGBOOST_USE_RCCL) #include "nccl_stub.h" +#if defined(XGBOOST_USE_NCCL) #include // for CUDA_VERSION #include // for cudaPeekAtLastError #include // for dlclose, dlsym, dlopen #include #include // for cuda_category #include // for system_error +#elif defined(XGBOOST_USE_RCCL) +#include "../common/cuda_to_hip.h" +#include "../common/device_helpers.hip.h" +#include // for cudaPeekAtLastError +#include // for dlclose, dlsym, dlopen +#include +#include // for cuda_category +#include // for system_error +#endif #include // for int32_t #include // for stringstream diff --git a/src/collective/nccl_stub.h b/src/collective/nccl_stub.h index 6bf2ecae6..978f34028 100644 --- a/src/collective/nccl_stub.h +++ b/src/collective/nccl_stub.h @@ -2,10 +2,17 @@ * Copyright 2023, XGBoost Contributors */ #pragma once -#if defined(XGBOOST_USE_NCCL) || (defined(XGBOOST_USE_RCCL) && 0) +#if defined(XGBOOST_USE_NCCL) || defined(XGBOOST_USE_RCCL) +#if defined(XGBOOST_USE_NCCL) #include #include +#elif defined(XGBOOST_USE_RCCL) +#include "../common/cuda_to_hip.h" +#include "../common/device_helpers.cuh" +#include +#include +#endif #include // for string diff --git a/src/common/algorithm.cuh b/src/common/algorithm.cuh index bce9ba5de..e1e9c8bf4 100644 --- a/src/common/algorithm.cuh +++ b/src/common/algorithm.cuh @@ -226,6 +226,7 @@ void SegmentedArgMergeSort(Context const *ctx, SegIt seg_begin, SegIt seg_end, V }); } +#if defined(XGBOOST_USE_CUDA) template void ArgSort(xgboost::Context const *ctx, xgboost::common::Span keys, xgboost::common::Span sorted_idx) { @@ -295,5 +296,51 @@ void ArgSort(xgboost::Context const *ctx, xgboost::common::Span keys, sorted_idx.size_bytes(), cudaMemcpyDeviceToDevice, cuctx->Stream())); } +#elif defined(XGBOOST_USE_HIP) +template +void ArgSort(xgboost::Context const *ctx, xgboost::common::Span keys, + xgboost::common::Span sorted_idx) { + std::size_t bytes = 0; + auto cuctx = ctx->CUDACtx(); + dh::Iota(sorted_idx, cuctx->Stream()); + + using KeyT = typename decltype(keys)::value_type; + using ValueT = std::remove_const_t; + + dh::TemporaryArray out(keys.size()); + dh::TemporaryArray sorted_idx_out(sorted_idx.size()); + + // track https://github.com/NVIDIA/cub/pull/340 for 64bit length support + using OffsetT = std::conditional_t; + CHECK_LE(sorted_idx.size(), std::numeric_limits::max()); + if (accending) { + void *d_temp_storage = nullptr; + + dh::safe_cuda((rocprim::radix_sort_pairs(d_temp_storage, + bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, + sizeof(KeyT) * 8, cuctx->Stream(), false))); + + dh::TemporaryArray storage(bytes); + d_temp_storage = storage.data().get(); + dh::safe_cuda((rocprim::radix_sort_pairs(d_temp_storage, + bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, + sizeof(KeyT) * 8, cuctx->Stream(), false))); + } else { + void *d_temp_storage = nullptr; + + dh::safe_cuda((rocprim::radix_sort_pairs_desc(d_temp_storage, + bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, + sizeof(KeyT) * 8, cuctx->Stream(), false))); + dh::TemporaryArray storage(bytes); + d_temp_storage = storage.data().get(); + dh::safe_cuda((rocprim::radix_sort_pairs_desc(d_temp_storage, + bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, + sizeof(KeyT) * 8, cuctx->Stream(), false))); + } + + dh::safe_cuda(hipMemcpyAsync(sorted_idx.data(), sorted_idx_out.data().get(), + sorted_idx.size_bytes(), hipMemcpyDeviceToDevice, cuctx->Stream())); +} +#endif } // namespace xgboost::common #endif // XGBOOST_COMMON_ALGORITHM_CUH_ diff --git a/src/common/device_helpers.hip.h b/src/common/device_helpers.hip.h index fcfe2bdd4..79f2f3390 100644 --- a/src/common/device_helpers.hip.h +++ b/src/common/device_helpers.hip.h @@ -40,10 +40,6 @@ #include "xgboost/logging.h" #include "xgboost/span.h" -#ifdef XGBOOST_USE_RCCL -#include "rccl.h" -#endif // XGBOOST_USE_RCCL - #if defined(XGBOOST_USE_RMM) && XGBOOST_USE_RMM == 1 #include "rmm/mr/device/per_device_resource.hpp" #include "rmm/mr/device/thrust_allocator_adaptor.hpp" @@ -98,30 +94,6 @@ XGBOOST_DEV_INLINE T atomicAdd(T *addr, T v) { // NOLINT } namespace dh { -#ifdef XGBOOST_USE_RCCL -#define safe_nccl(ans) ThrowOnNcclError((ans), __FILE__, __LINE__) - -inline ncclResult_t ThrowOnNcclError(ncclResult_t code, const char *file, int line) { - if (code != ncclSuccess) { - std::stringstream ss; - ss << "RCCL failure: " << ncclGetErrorString(code) << "."; - ss << " " << file << "(" << line << ")\n"; - if (code == ncclUnhandledCudaError) { - // nccl usually preserves the last error so we can get more details. - auto err = hipPeekAtLastError(); - ss << " CUDA error: " << thrust::system_error(err, thrust::hip_category()).what() << "\n"; - } else if (code == ncclSystemError) { - ss << " This might be caused by a network configuration issue. Please consider specifying " - "the network interface for RCCL via environment variables listed in its reference: " - "`https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html`.\n"; - } - LOG(FATAL) << ss.str(); - } - - return code; -} -#endif - inline int32_t CudaGetPointerDevice(void const *ptr) { int32_t device = -1; hipPointerAttribute_t attr; @@ -298,8 +270,8 @@ inline void LaunchN(size_t n, L lambda) { } template -void Iota(Container array) { - LaunchN(array.size(), [=] __device__(size_t i) { array[i] = i; }); +void Iota(Container array, cudaStream_t stream) { + LaunchN(array.size(), stream, [=] __device__(size_t i) { array[i] = i; }); } namespace detail { @@ -465,7 +437,8 @@ struct XGBCachingDeviceAllocatorImpl : XGBBaseDeviceAllocator { hipcub::CachingDeviceAllocator& GetGlobalCachingAllocator() { // Configure allocator with maximum cached bin size of ~1GB and no limit on // maximum cached bytes - static hipcub::CachingDeviceAllocator *allocator = new hipcub::CachingDeviceAllocator(2, 9, 29); + thread_local std::unique_ptr allocator{ + std::make_unique(2, 9, 29)}; return *allocator; } pointer allocate(size_t n) { // NOLINT @@ -581,6 +554,16 @@ class DoubleBuffer { T *Other() { return buff.Alternate(); } }; +template +xgboost::common::Span LazyResize(xgboost::Context const *ctx, + xgboost::HostDeviceVector *buffer, std::size_t n) { + buffer->SetDevice(ctx->Device()); + if (buffer->Size() < n) { + buffer->Resize(n); + } + return buffer->DeviceSpan().subspan(0, n); +} + /** * \brief Copies device span to std::vector. * @@ -1017,49 +1000,6 @@ void InclusiveSum(InputIteratorT d_in, OutputIteratorT d_out, OffsetT num_items) InclusiveScan(d_in, d_out, hipcub::Sum(), num_items); } -template -void ArgSort(xgboost::common::Span keys, xgboost::common::Span sorted_idx) { - size_t bytes = 0; - Iota(sorted_idx); - - using KeyT = typename decltype(keys)::value_type; - using ValueT = std::remove_const_t; - - TemporaryArray out(keys.size()); - TemporaryArray sorted_idx_out(sorted_idx.size()); - - // track https://github.com/NVIDIA/cub/pull/340 for 64bit length support - using OffsetT = std::conditional_t; - CHECK_LE(sorted_idx.size(), std::numeric_limits::max()); - if (accending) { - void *d_temp_storage = nullptr; - - safe_cuda((rocprim::radix_sort_pairs(d_temp_storage, - bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, - sizeof(KeyT) * 8))); - - TemporaryArray storage(bytes); - d_temp_storage = storage.data().get(); - safe_cuda((rocprim::radix_sort_pairs(d_temp_storage, - bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, - sizeof(KeyT) * 8))); - } else { - void *d_temp_storage = nullptr; - - safe_cuda((rocprim::radix_sort_pairs_desc(d_temp_storage, - bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, - sizeof(KeyT) * 8))); - TemporaryArray storage(bytes); - d_temp_storage = storage.data().get(); - safe_cuda((rocprim::radix_sort_pairs_desc(d_temp_storage, - bytes, keys.data(), out.data().get(), sorted_idx.data(), sorted_idx_out.data().get(), sorted_idx.size(), 0, - sizeof(KeyT) * 8))); - } - - safe_cuda(hipMemcpyAsync(sorted_idx.data(), sorted_idx_out.data().get(), - sorted_idx.size_bytes(), hipMemcpyDeviceToDevice)); -} - class CUDAStreamView; class CUDAEvent { @@ -1105,6 +1045,8 @@ inline void CUDAEvent::Record(CUDAStreamView stream) { // NOLINT dh::safe_cuda(hipEventRecord(event_, hipStream_t{stream})); } +// Changing this has effect on prediction return, where we need to pass the pointer to +// third-party libraries like cuPy inline CUDAStreamView DefaultStream() { #ifdef HIP_API_PER_THREAD_DEFAULT_STREAM return CUDAStreamView{hipStreamPerThread}; diff --git a/src/data/array_interface.cc b/src/data/array_interface.cc index 06b9ed00c..c6d9eda74 100644 --- a/src/data/array_interface.cc +++ b/src/data/array_interface.cc @@ -6,7 +6,7 @@ #include "../common/common.h" // for AssertGPUSupport namespace xgboost { -#if !defined(XGBOOST_USE_CUDA) +#if !defined(XGBOOST_USE_CUDA) && !defined(XGBOOST_USE_HIP) void ArrayInterfaceHandler::SyncCudaStream(int64_t) { common::AssertGPUSupport(); } bool ArrayInterfaceHandler::IsCudaPtr(void const *) { return false; } #endif // !defined(XGBOOST_USE_CUDA) diff --git a/src/objective/hinge.cu b/src/objective/hinge.cu index 589b91acc..37e88f838 100644 --- a/src/objective/hinge.cu +++ b/src/objective/hinge.cu @@ -9,7 +9,7 @@ #include // for int32_t #include "../common/common.h" // for Range -#if defined(XGBOOST_USE_CUDA) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) #include "../common/linalg_op.cuh" #endif #include "../common/linalg_op.h" diff --git a/tests/cpp/objective/test_multiclass_obj_gpu.hip b/tests/cpp/objective/test_multiclass_obj_gpu.hip index 6bf3f66b0..938ddd9d8 100644 --- a/tests/cpp/objective/test_multiclass_obj_gpu.hip +++ b/tests/cpp/objective/test_multiclass_obj_gpu.hip @@ -1,2 +1,2 @@ -#include "test_multiclass_obj.cc" +#include "test_multiclass_obj_gpu.cu" diff --git a/tests/cpp/objective/test_regression_obj_cpu.cc b/tests/cpp/objective/test_regression_obj_cpu.cc index 3613d0d90..afc8cbb73 100644 --- a/tests/cpp/objective/test_regression_obj_cpu.cc +++ b/tests/cpp/objective/test_regression_obj_cpu.cc @@ -193,7 +193,7 @@ TEST(Objective, DeclareUnifiedTest(TweedieRegressionGPair)) { ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"tweedie-nloglik@1.1"}); } -#if defined(__CUDACC__) +#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) TEST(Objective, CPU_vs_CUDA) { Context ctx = MakeCUDACtx(GPUIDX); @@ -271,7 +271,7 @@ TEST(Objective, DeclareUnifiedTest(TweedieRegressionBasic)) { } // CoxRegression not implemented in GPU code, no need for testing. -#if !defined(__CUDACC__) +#if !defined(__CUDACC__) && !defined(__HIP_PLATFORM_AMD__) TEST(Objective, CoxRegressionGPair) { Context ctx = MakeCUDACtx(GPUIDX); std::vector> args; diff --git a/tests/cpp/objective/test_regression_obj_gpu.hip b/tests/cpp/objective/test_regression_obj_gpu.hip index b5a636e26..62154585e 100644 --- a/tests/cpp/objective/test_regression_obj_gpu.hip +++ b/tests/cpp/objective/test_regression_obj_gpu.hip @@ -1,2 +1,2 @@ -#include "test_regression_obj.cc" +#include "test_regression_obj_gpu.cu" From 547abb8c126991e0fc24219616e1e7298e266723 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Mon, 15 Jan 2024 10:16:30 +0100 Subject: [PATCH 096/109] [R] Remove unusable 'feature_names' argument and make 'model' first argument in inspection functions (#9939) --- R-package/DESCRIPTION | 2 +- R-package/R/xgb.importance.R | 9 +------ R-package/R/xgb.model.dt.tree.R | 35 +++++++------------------ R-package/R/xgb.plot.multi.trees.R | 4 +-- R-package/R/xgb.plot.tree.R | 14 +++------- R-package/man/xgb.importance.Rd | 6 ++--- R-package/man/xgb.model.dt.tree.Rd | 12 +++------ R-package/man/xgb.plot.multi.trees.Rd | 7 ++--- R-package/man/xgb.plot.tree.Rd | 7 ++--- R-package/tests/testthat/test_helpers.R | 8 ++---- 10 files changed, 30 insertions(+), 74 deletions(-) diff --git a/R-package/DESCRIPTION b/R-package/DESCRIPTION index 7c01d50c6..bbaf3e75d 100644 --- a/R-package/DESCRIPTION +++ b/R-package/DESCRIPTION @@ -65,6 +65,6 @@ Imports: data.table (>= 1.9.6), jsonlite (>= 1.0) Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.0 Encoding: UTF-8 SystemRequirements: GNU make, C++17 diff --git a/R-package/R/xgb.importance.R b/R-package/R/xgb.importance.R index 44f2eb9b3..547d9677b 100644 --- a/R-package/R/xgb.importance.R +++ b/R-package/R/xgb.importance.R @@ -113,19 +113,12 @@ #' xgb.importance(model = mbst) #' #' @export -xgb.importance <- function(feature_names = NULL, model = NULL, trees = NULL, +xgb.importance <- function(model = NULL, feature_names = getinfo(model, "feature_name"), trees = NULL, data = NULL, label = NULL, target = NULL) { if (!(is.null(data) && is.null(label) && is.null(target))) warning("xgb.importance: parameters 'data', 'label' and 'target' are deprecated") - if (is.null(feature_names)) { - model_feature_names <- xgb.feature_names(model) - if (NROW(model_feature_names)) { - feature_names <- model_feature_names - } - } - if (!(is.null(feature_names) || is.character(feature_names))) stop("feature_names: Has to be a character vector") diff --git a/R-package/R/xgb.model.dt.tree.R b/R-package/R/xgb.model.dt.tree.R index df0e672a9..ff416b73e 100644 --- a/R-package/R/xgb.model.dt.tree.R +++ b/R-package/R/xgb.model.dt.tree.R @@ -2,11 +2,8 @@ #' #' Parse a boosted tree model text dump into a `data.table` structure. #' -#' @param feature_names Character vector of feature names. If the model already -#' contains feature names, those will be used when \code{feature_names=NULL} (default value). -#' -#' Note that, if the model already contains feature names, it's \bold{not} possible to override them here. -#' @param model Object of class `xgb.Booster`. +#' @param model Object of class `xgb.Booster`. If it contains feature names (they can be set through +#' \link{setinfo}), they will be used in the output from this function. #' @param text Character vector previously generated by the function [xgb.dump()] #' (called with parameter `with_stats = TRUE`). `text` takes precedence over `model`. #' @param trees An integer vector of tree indices that should be used. @@ -58,7 +55,7 @@ #' #' # This bst model already has feature_names stored with it, so those would be used when #' # feature_names is not set: -#' (dt <- xgb.model.dt.tree(model = bst)) +#' dt <- xgb.model.dt.tree(bst) #' #' # How to match feature names of splits that are following a current 'Yes' branch: #' merge( @@ -69,7 +66,7 @@ #' ] #' #' @export -xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, +xgb.model.dt.tree <- function(model = NULL, text = NULL, trees = NULL, use_int_id = FALSE, ...) { check.deprecation(...) @@ -79,24 +76,15 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, " (or NULL if 'model' was provided).") } - model_feature_names <- NULL - if (inherits(model, "xgb.Booster")) { - model_feature_names <- xgb.feature_names(model) - if (NROW(model_feature_names) && !is.null(feature_names)) { - stop("'model' contains feature names. Cannot override them.") - } - } - if (is.null(feature_names) && !is.null(model) && !is.null(model_feature_names)) - feature_names <- model_feature_names - - if (!(is.null(feature_names) || is.character(feature_names))) { - stop("feature_names: must be a character vector") - } - if (!(is.null(trees) || is.numeric(trees))) { stop("trees: must be a vector of integers.") } + feature_names <- NULL + if (inherits(model, "xgb.Booster")) { + feature_names <- xgb.feature_names(model) + } + from_text <- TRUE if (is.null(text)) { text <- xgb.dump(model = model, with_stats = TRUE) @@ -134,7 +122,7 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, branch_rx_w_names <- paste0("\\d+:\\[(.+)<(", anynumber_regex, ")\\] yes=(\\d+),no=(\\d+),missing=(\\d+),", "gain=(", anynumber_regex, "),cover=(", anynumber_regex, ")") text_has_feature_names <- FALSE - if (NROW(model_feature_names)) { + if (NROW(feature_names)) { branch_rx <- branch_rx_w_names text_has_feature_names <- TRUE } else { @@ -148,9 +136,6 @@ xgb.model.dt.tree <- function(feature_names = NULL, model = NULL, text = NULL, } } } - if (text_has_feature_names && is.null(model) && !is.null(feature_names)) { - stop("'text' contains feature names. Cannot override them.") - } branch_cols <- c("Feature", "Split", "Yes", "No", "Missing", "Gain", "Cover") td[ isLeaf == FALSE, diff --git a/R-package/R/xgb.plot.multi.trees.R b/R-package/R/xgb.plot.multi.trees.R index 88616cfb7..e6d678ee7 100644 --- a/R-package/R/xgb.plot.multi.trees.R +++ b/R-package/R/xgb.plot.multi.trees.R @@ -62,13 +62,13 @@ #' } #' #' @export -xgb.plot.multi.trees <- function(model, feature_names = NULL, features_keep = 5, plot_width = NULL, plot_height = NULL, +xgb.plot.multi.trees <- function(model, features_keep = 5, plot_width = NULL, plot_height = NULL, render = TRUE, ...) { if (!requireNamespace("DiagrammeR", quietly = TRUE)) { stop("DiagrammeR is required for xgb.plot.multi.trees") } check.deprecation(...) - tree.matrix <- xgb.model.dt.tree(feature_names = feature_names, model = model) + tree.matrix <- xgb.model.dt.tree(model = model) # first number of the path represents the tree, then the following numbers are related to the path to follow # root init diff --git a/R-package/R/xgb.plot.tree.R b/R-package/R/xgb.plot.tree.R index c75a42e84..5ed1e70f6 100644 --- a/R-package/R/xgb.plot.tree.R +++ b/R-package/R/xgb.plot.tree.R @@ -2,9 +2,8 @@ #' #' Read a tree model text dump and plot the model. #' -#' @param feature_names Character vector used to overwrite the feature names -#' of the model. The default (`NULL`) uses the original feature names. -#' @param model Object of class `xgb.Booster`. +#' @param model Object of class `xgb.Booster`. If it contains feature names (they can be set through +#' \link{setinfo}), they will be used in the output from this function. #' @param trees An integer vector of tree indices that should be used. #' The default (`NULL`) uses all trees. #' Useful, e.g., in multiclass classification to get only @@ -103,7 +102,7 @@ #' } #' #' @export -xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot_width = NULL, plot_height = NULL, +xgb.plot.tree <- function(model = NULL, trees = NULL, plot_width = NULL, plot_height = NULL, render = TRUE, show_node_id = FALSE, style = c("R", "xgboost"), ...) { check.deprecation(...) if (!inherits(model, "xgb.Booster")) { @@ -120,17 +119,12 @@ xgb.plot.tree <- function(feature_names = NULL, model = NULL, trees = NULL, plot if (NROW(trees) != 1L || !render || show_node_id) { stop("style='xgboost' is only supported for single, rendered tree, without node IDs.") } - if (!is.null(feature_names)) { - stop( - "style='xgboost' cannot override 'feature_names'. Will automatically take them from the model." - ) - } txt <- xgb.dump(model, dump_format = "dot") return(DiagrammeR::grViz(txt[[trees + 1]], width = plot_width, height = plot_height)) } - dt <- xgb.model.dt.tree(feature_names = feature_names, model = model, trees = trees) + dt <- xgb.model.dt.tree(model = model, trees = trees) dt[, label := paste0(Feature, "\nCover: ", Cover, ifelse(Feature == "Leaf", "\nValue: ", "\nGain: "), Gain)] if (show_node_id) diff --git a/R-package/man/xgb.importance.Rd b/R-package/man/xgb.importance.Rd index fca1b70c4..73b91e8b4 100644 --- a/R-package/man/xgb.importance.Rd +++ b/R-package/man/xgb.importance.Rd @@ -5,8 +5,8 @@ \title{Feature importance} \usage{ xgb.importance( - feature_names = NULL, model = NULL, + feature_names = getinfo(model, "feature_name"), trees = NULL, data = NULL, label = NULL, @@ -14,11 +14,11 @@ xgb.importance( ) } \arguments{ +\item{model}{Object of class \code{xgb.Booster}.} + \item{feature_names}{Character vector used to overwrite the feature names of the model. The default is \code{NULL} (use original feature names).} -\item{model}{Object of class \code{xgb.Booster}.} - \item{trees}{An integer vector of tree indices that should be included into the importance calculation (only for the "gbtree" booster). The default (\code{NULL}) parses all trees. diff --git a/R-package/man/xgb.model.dt.tree.Rd b/R-package/man/xgb.model.dt.tree.Rd index e63bd4b10..75f1cd0f4 100644 --- a/R-package/man/xgb.model.dt.tree.Rd +++ b/R-package/man/xgb.model.dt.tree.Rd @@ -5,7 +5,6 @@ \title{Parse model text dump} \usage{ xgb.model.dt.tree( - feature_names = NULL, model = NULL, text = NULL, trees = NULL, @@ -14,13 +13,8 @@ xgb.model.dt.tree( ) } \arguments{ -\item{feature_names}{Character vector of feature names. If the model already -contains feature names, those will be used when \code{feature_names=NULL} (default value). - -\if{html}{\out{
}}\preformatted{ Note that, if the model already contains feature names, it's \\bold\{not\} possible to override them here. -}\if{html}{\out{
}}} - -\item{model}{Object of class \code{xgb.Booster}.} +\item{model}{Object of class \code{xgb.Booster}. If it contains feature names (they can be set through +\link{setinfo}), they will be used in the output from this function.} \item{text}{Character vector previously generated by the function \code{\link[=xgb.dump]{xgb.dump()}} (called with parameter \code{with_stats = TRUE}). \code{text} takes precedence over \code{model}.} @@ -81,7 +75,7 @@ bst <- xgboost( # This bst model already has feature_names stored with it, so those would be used when # feature_names is not set: -(dt <- xgb.model.dt.tree(model = bst)) +dt <- xgb.model.dt.tree(bst) # How to match feature names of splits that are following a current 'Yes' branch: merge( diff --git a/R-package/man/xgb.plot.multi.trees.Rd b/R-package/man/xgb.plot.multi.trees.Rd index d98a3482c..7fa75c85d 100644 --- a/R-package/man/xgb.plot.multi.trees.Rd +++ b/R-package/man/xgb.plot.multi.trees.Rd @@ -6,7 +6,6 @@ \usage{ xgb.plot.multi.trees( model, - feature_names = NULL, features_keep = 5, plot_width = NULL, plot_height = NULL, @@ -15,10 +14,8 @@ xgb.plot.multi.trees( ) } \arguments{ -\item{model}{Object of class \code{xgb.Booster}.} - -\item{feature_names}{Character vector used to overwrite the feature names -of the model. The default (\code{NULL}) uses the original feature names.} +\item{model}{Object of class \code{xgb.Booster}. If it contains feature names (they can be set through +\link{setinfo}), they will be used in the output from this function.} \item{features_keep}{Number of features to keep in each position of the multi trees, by default 5.} diff --git a/R-package/man/xgb.plot.tree.Rd b/R-package/man/xgb.plot.tree.Rd index a09bb7183..69d37301d 100644 --- a/R-package/man/xgb.plot.tree.Rd +++ b/R-package/man/xgb.plot.tree.Rd @@ -5,7 +5,6 @@ \title{Plot boosted trees} \usage{ xgb.plot.tree( - feature_names = NULL, model = NULL, trees = NULL, plot_width = NULL, @@ -17,10 +16,8 @@ xgb.plot.tree( ) } \arguments{ -\item{feature_names}{Character vector used to overwrite the feature names -of the model. The default (\code{NULL}) uses the original feature names.} - -\item{model}{Object of class \code{xgb.Booster}.} +\item{model}{Object of class \code{xgb.Booster}. If it contains feature names (they can be set through +\link{setinfo}), they will be used in the output from this function.} \item{trees}{An integer vector of tree indices that should be used. The default (\code{NULL}) uses all trees. diff --git a/R-package/tests/testthat/test_helpers.R b/R-package/tests/testthat/test_helpers.R index 372f2520c..badac0213 100644 --- a/R-package/tests/testthat/test_helpers.R +++ b/R-package/tests/testthat/test_helpers.R @@ -282,9 +282,6 @@ test_that("xgb.model.dt.tree works with and without feature names", { expect_equal(dim(dt.tree), c(188, 10)) expect_output(str(dt.tree), 'Feature.*\\"Age\\"') - dt.tree.0 <- xgb.model.dt.tree(model = bst.Tree) - expect_equal(dt.tree, dt.tree.0) - # when model contains no feature names: dt.tree.x <- xgb.model.dt.tree(model = bst.Tree.unnamed) expect_output(str(dt.tree.x), 'Feature.*\\"3\\"') @@ -304,7 +301,7 @@ test_that("xgb.model.dt.tree throws error for gblinear", { test_that("xgb.importance works with and without feature names", { .skip_if_vcd_not_available() - importance.Tree <- xgb.importance(feature_names = feature.names, model = bst.Tree) + importance.Tree <- xgb.importance(feature_names = feature.names, model = bst.Tree.unnamed) if (!flag_32bit) expect_equal(dim(importance.Tree), c(7, 4)) expect_equal(colnames(importance.Tree), c("Feature", "Gain", "Cover", "Frequency")) @@ -330,9 +327,8 @@ test_that("xgb.importance works with and without feature names", { importance <- xgb.importance(feature_names = feature.names, model = bst.Tree, trees = trees) importance_from_dump <- function() { - model_text_dump <- xgb.dump(model = bst.Tree.unnamed, with_stats = TRUE, trees = trees) + model_text_dump <- xgb.dump(model = bst.Tree, with_stats = TRUE, trees = trees) imp <- xgb.model.dt.tree( - feature_names = feature.names, text = model_text_dump, trees = trees )[ From 2de85d3241cd489d3eb8d3f5a593564725d7166e Mon Sep 17 00:00:00 2001 From: greydoubt <43443470+greydoubt@users.noreply.github.com> Date: Mon, 15 Jan 2024 03:09:20 -0800 Subject: [PATCH 097/109] [doc] slight cleanup (#9988) --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index 4c29f4905..7d54b78cb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,7 +4,7 @@ facilities. # Directories * ci_build: Test facilities for Jenkins CI and GitHub action. * cli: Basic test for command line executable `xgboost`. Most of the other command line - specific tests are in Python test `test_cli.py` + specific tests are in Python test `test_cli.py`. * cpp: Tests for C++ core, using Google test framework. * python: Tests for Python package, demonstrations and CLI. For how to setup the dependencies for tests, see conda files in `ci_build`. From 0798e36d733eb56313870b3fb301490f71c9ac78 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Mon, 15 Jan 2024 20:40:05 +0800 Subject: [PATCH 098/109] [breaking] Remove deprecated parameters in the skl interface. (#9986) --- demo/guide-python/continuation.py | 31 ++-- demo/guide-python/sklearn_evals_result.py | 35 ++-- demo/guide-python/sklearn_examples.py | 31 ++-- demo/guide-python/sklearn_parallel.py | 1 + doc/tutorials/custom_metric_obj.rst | 12 +- python-package/xgboost/dask/__init__.py | 61 ++----- python-package/xgboost/sklearn.py | 150 ++-------------- tests/ci_build/lint_python.py | 6 + tests/python/test_callback.py | 169 ++++++++++-------- tests/python/test_early_stopping.py | 26 +-- tests/python/test_eval_metrics.py | 165 +++++++++++------ tests/python/test_training_continuation.py | 7 +- tests/python/test_with_sklearn.py | 117 ++++++------ .../test_gpu_with_dask/test_gpu_with_dask.py | 2 +- .../test_with_dask/test_with_dask.py | 59 +++--- .../test_with_spark/test_spark_local.py | 8 +- 16 files changed, 418 insertions(+), 462 deletions(-) diff --git a/demo/guide-python/continuation.py b/demo/guide-python/continuation.py index 84afc3710..e32c48665 100644 --- a/demo/guide-python/continuation.py +++ b/demo/guide-python/continuation.py @@ -16,14 +16,14 @@ def training_continuation(tmpdir: str, use_pickle: bool) -> None: """Basic training continuation.""" # Train 128 iterations in 1 session X, y = load_breast_cancer(return_X_y=True) - clf = xgboost.XGBClassifier(n_estimators=128) - clf.fit(X, y, eval_set=[(X, y)], eval_metric="logloss") + clf = xgboost.XGBClassifier(n_estimators=128, eval_metric="logloss") + clf.fit(X, y, eval_set=[(X, y)]) print("Total boosted rounds:", clf.get_booster().num_boosted_rounds()) # Train 128 iterations in 2 sessions, with the first one runs for 32 iterations and # the second one runs for 96 iterations - clf = xgboost.XGBClassifier(n_estimators=32) - clf.fit(X, y, eval_set=[(X, y)], eval_metric="logloss") + clf = xgboost.XGBClassifier(n_estimators=32, eval_metric="logloss") + clf.fit(X, y, eval_set=[(X, y)]) assert clf.get_booster().num_boosted_rounds() == 32 # load back the model, this could be a checkpoint @@ -39,8 +39,8 @@ def training_continuation(tmpdir: str, use_pickle: bool) -> None: loaded = xgboost.XGBClassifier() loaded.load_model(path) - clf = xgboost.XGBClassifier(n_estimators=128 - 32) - clf.fit(X, y, eval_set=[(X, y)], eval_metric="logloss", xgb_model=loaded) + clf = xgboost.XGBClassifier(n_estimators=128 - 32, eval_metric="logloss") + clf.fit(X, y, eval_set=[(X, y)], xgb_model=loaded) print("Total boosted rounds:", clf.get_booster().num_boosted_rounds()) @@ -56,19 +56,24 @@ def training_continuation_early_stop(tmpdir: str, use_pickle: bool) -> None: n_estimators = 512 X, y = load_breast_cancer(return_X_y=True) - clf = xgboost.XGBClassifier(n_estimators=n_estimators) - clf.fit(X, y, eval_set=[(X, y)], eval_metric="logloss", callbacks=[early_stop]) + clf = xgboost.XGBClassifier( + n_estimators=n_estimators, eval_metric="logloss", callbacks=[early_stop] + ) + clf.fit(X, y, eval_set=[(X, y)]) print("Total boosted rounds:", clf.get_booster().num_boosted_rounds()) best = clf.best_iteration # Train 512 iterations in 2 sessions, with the first one runs for 128 iterations and # the second one runs until early stop. - clf = xgboost.XGBClassifier(n_estimators=128) + clf = xgboost.XGBClassifier( + n_estimators=128, eval_metric="logloss", callbacks=[early_stop] + ) # Reinitialize the early stop callback early_stop = xgboost.callback.EarlyStopping( rounds=early_stopping_rounds, save_best=True ) - clf.fit(X, y, eval_set=[(X, y)], eval_metric="logloss", callbacks=[early_stop]) + clf.set_params(callbacks=[early_stop]) + clf.fit(X, y, eval_set=[(X, y)]) assert clf.get_booster().num_boosted_rounds() == 128 # load back the model, this could be a checkpoint @@ -87,13 +92,13 @@ def training_continuation_early_stop(tmpdir: str, use_pickle: bool) -> None: early_stop = xgboost.callback.EarlyStopping( rounds=early_stopping_rounds, save_best=True ) - clf = xgboost.XGBClassifier(n_estimators=n_estimators - 128) + clf = xgboost.XGBClassifier( + n_estimators=n_estimators - 128, eval_metric="logloss", callbacks=[early_stop] + ) clf.fit( X, y, eval_set=[(X, y)], - eval_metric="logloss", - callbacks=[early_stop], xgb_model=loaded, ) diff --git a/demo/guide-python/sklearn_evals_result.py b/demo/guide-python/sklearn_evals_result.py index 9aed58500..781ab81af 100644 --- a/demo/guide-python/sklearn_evals_result.py +++ b/demo/guide-python/sklearn_evals_result.py @@ -16,30 +16,35 @@ labels, y = np.unique(y, return_inverse=True) X_train, X_test = X[:1600], X[1600:] y_train, y_test = y[:1600], y[1600:] -param_dist = {'objective':'binary:logistic', 'n_estimators':2} +param_dist = {"objective": "binary:logistic", "n_estimators": 2} -clf = xgb.XGBModel(**param_dist) +clf = xgb.XGBModel( + **param_dist, + eval_metric="logloss", +) # Or you can use: clf = xgb.XGBClassifier(**param_dist) -clf.fit(X_train, y_train, - eval_set=[(X_train, y_train), (X_test, y_test)], - eval_metric='logloss', - verbose=True) +clf.fit( + X_train, + y_train, + eval_set=[(X_train, y_train), (X_test, y_test)], + verbose=True, +) # Load evals result by calling the evals_result() function evals_result = clf.evals_result() -print('Access logloss metric directly from validation_0:') -print(evals_result['validation_0']['logloss']) +print("Access logloss metric directly from validation_0:") +print(evals_result["validation_0"]["logloss"]) -print('') -print('Access metrics through a loop:') +print("") +print("Access metrics through a loop:") for e_name, e_mtrs in evals_result.items(): - print('- {}'.format(e_name)) + print("- {}".format(e_name)) for e_mtr_name, e_mtr_vals in e_mtrs.items(): - print(' - {}'.format(e_mtr_name)) - print(' - {}'.format(e_mtr_vals)) + print(" - {}".format(e_mtr_name)) + print(" - {}".format(e_mtr_vals)) -print('') -print('Access complete dict:') +print("") +print("Access complete dict:") print(evals_result) diff --git a/demo/guide-python/sklearn_examples.py b/demo/guide-python/sklearn_examples.py index cf33e959a..0fe7a8e24 100644 --- a/demo/guide-python/sklearn_examples.py +++ b/demo/guide-python/sklearn_examples.py @@ -1,4 +1,4 @@ -''' +""" Collection of examples for using sklearn interface ================================================== @@ -8,7 +8,7 @@ For an introduction to XGBoost's scikit-learn estimator interface, see Created on 1 Apr 2015 @author: Jamie Hall -''' +""" import pickle import numpy as np @@ -22,8 +22,8 @@ rng = np.random.RandomState(31337) print("Zeros and Ones from the Digits dataset: binary classification") digits = load_digits(n_class=2) -y = digits['target'] -X = digits['data'] +y = digits["target"] +X = digits["data"] kf = KFold(n_splits=2, shuffle=True, random_state=rng) for train_index, test_index in kf.split(X): xgb_model = xgb.XGBClassifier(n_jobs=1).fit(X[train_index], y[train_index]) @@ -33,8 +33,8 @@ for train_index, test_index in kf.split(X): print("Iris: multiclass classification") iris = load_iris() -y = iris['target'] -X = iris['data'] +y = iris["target"] +X = iris["data"] kf = KFold(n_splits=2, shuffle=True, random_state=rng) for train_index, test_index in kf.split(X): xgb_model = xgb.XGBClassifier(n_jobs=1).fit(X[train_index], y[train_index]) @@ -53,9 +53,13 @@ for train_index, test_index in kf.split(X): print("Parameter optimization") xgb_model = xgb.XGBRegressor(n_jobs=1) -clf = GridSearchCV(xgb_model, - {'max_depth': [2, 4], - 'n_estimators': [50, 100]}, verbose=1, n_jobs=1, cv=3) +clf = GridSearchCV( + xgb_model, + {"max_depth": [2, 4], "n_estimators": [50, 100]}, + verbose=1, + n_jobs=1, + cv=3, +) clf.fit(X, y) print(clf.best_score_) print(clf.best_params_) @@ -69,9 +73,8 @@ print(np.allclose(clf.predict(X), clf2.predict(X))) # Early-stopping -X = digits['data'] -y = digits['target'] +X = digits["data"] +y = digits["target"] X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0) -clf = xgb.XGBClassifier(n_jobs=1) -clf.fit(X_train, y_train, early_stopping_rounds=10, eval_metric="auc", - eval_set=[(X_test, y_test)]) +clf = xgb.XGBClassifier(n_jobs=1, early_stopping_rounds=10, eval_metric="auc") +clf.fit(X_train, y_train, eval_set=[(X_test, y_test)]) diff --git a/demo/guide-python/sklearn_parallel.py b/demo/guide-python/sklearn_parallel.py index 2ebefffc7..55e0bff74 100644 --- a/demo/guide-python/sklearn_parallel.py +++ b/demo/guide-python/sklearn_parallel.py @@ -12,6 +12,7 @@ import xgboost as xgb if __name__ == "__main__": print("Parallel Parameter optimization") X, y = fetch_california_housing(return_X_y=True) + # Make sure the number of threads is balanced. xgb_model = xgb.XGBRegressor( n_jobs=multiprocessing.cpu_count() // 2, tree_method="hist" ) diff --git a/doc/tutorials/custom_metric_obj.rst b/doc/tutorials/custom_metric_obj.rst index 118a099c1..36bd0c8d6 100644 --- a/doc/tutorials/custom_metric_obj.rst +++ b/doc/tutorials/custom_metric_obj.rst @@ -123,11 +123,11 @@ monitor our model's performance. As mentioned above, the default metric for ``S elements = np.power(np.log1p(y) - np.log1p(predt), 2) return 'PyRMSLE', float(np.sqrt(np.sum(elements) / len(y))) -Since we are demonstrating in Python, the metric or objective need not be a function, -any callable object should suffice. Similar to the objective function, our metric also -accepts ``predt`` and ``dtrain`` as inputs, but returns the name of the metric itself and a -floating point value as the result. After passing it into XGBoost as argument of ``feval`` -parameter: +Since we are demonstrating in Python, the metric or objective need not be a function, any +callable object should suffice. Similar to the objective function, our metric also +accepts ``predt`` and ``dtrain`` as inputs, but returns the name of the metric itself and +a floating point value as the result. After passing it into XGBoost as argument of +``custom_metric`` parameter: .. code-block:: python @@ -136,7 +136,7 @@ parameter: dtrain=dtrain, num_boost_round=10, obj=squared_log, - feval=rmsle, + custom_metric=rmsle, evals=[(dtrain, 'dtrain'), (dtest, 'dtest')], evals_result=results) diff --git a/python-package/xgboost/dask/__init__.py b/python-package/xgboost/dask/__init__.py index 6b4ae5b07..a9b51f35d 100644 --- a/python-package/xgboost/dask/__init__.py +++ b/python-package/xgboost/dask/__init__.py @@ -61,7 +61,7 @@ from typing import ( import numpy from xgboost import collective, config -from xgboost._typing import _T, FeatureNames, FeatureTypes, ModelIn +from xgboost._typing import _T, FeatureNames, FeatureTypes from xgboost.callback import TrainingCallback from xgboost.compat import DataFrame, LazyLoader, concat, lazy_isinstance from xgboost.core import ( @@ -1774,14 +1774,11 @@ class DaskXGBRegressor(DaskScikitLearnBase, XGBRegressorBase): sample_weight: Optional[_DaskCollection], base_margin: Optional[_DaskCollection], eval_set: Optional[Sequence[Tuple[_DaskCollection, _DaskCollection]]], - eval_metric: Optional[Union[str, Sequence[str], Metric]], sample_weight_eval_set: Optional[Sequence[_DaskCollection]], base_margin_eval_set: Optional[Sequence[_DaskCollection]], - early_stopping_rounds: Optional[int], verbose: Union[int, bool], xgb_model: Optional[Union[Booster, XGBModel]], feature_weights: Optional[_DaskCollection], - callbacks: Optional[Sequence[TrainingCallback]], ) -> _DaskCollection: params = self.get_xgb_params() dtrain, evals = await _async_wrap_evaluation_matrices( @@ -1809,9 +1806,7 @@ class DaskXGBRegressor(DaskScikitLearnBase, XGBRegressorBase): obj: Optional[Callable] = _objective_decorator(self.objective) else: obj = None - model, metric, params, early_stopping_rounds, callbacks = self._configure_fit( - xgb_model, eval_metric, params, early_stopping_rounds, callbacks - ) + model, metric, params = self._configure_fit(xgb_model, params) results = await self.client.sync( _train_async, asynchronous=True, @@ -1826,8 +1821,8 @@ class DaskXGBRegressor(DaskScikitLearnBase, XGBRegressorBase): feval=None, custom_metric=metric, verbose_eval=verbose, - early_stopping_rounds=early_stopping_rounds, - callbacks=callbacks, + early_stopping_rounds=self.early_stopping_rounds, + callbacks=self.callbacks, xgb_model=model, ) self._Booster = results["booster"] @@ -1844,14 +1839,11 @@ class DaskXGBRegressor(DaskScikitLearnBase, XGBRegressorBase): sample_weight: Optional[_DaskCollection] = None, base_margin: Optional[_DaskCollection] = None, eval_set: Optional[Sequence[Tuple[_DaskCollection, _DaskCollection]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Callable]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Union[int, bool] = True, xgb_model: Optional[Union[Booster, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[_DaskCollection]] = None, base_margin_eval_set: Optional[Sequence[_DaskCollection]] = None, feature_weights: Optional[_DaskCollection] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "DaskXGBRegressor": _assert_dask_support() args = {k: v for k, v in locals().items() if k not in ("self", "__class__")} @@ -1871,14 +1863,11 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): sample_weight: Optional[_DaskCollection], base_margin: Optional[_DaskCollection], eval_set: Optional[Sequence[Tuple[_DaskCollection, _DaskCollection]]], - eval_metric: Optional[Union[str, Sequence[str], Metric]], sample_weight_eval_set: Optional[Sequence[_DaskCollection]], base_margin_eval_set: Optional[Sequence[_DaskCollection]], - early_stopping_rounds: Optional[int], verbose: Union[int, bool], xgb_model: Optional[Union[Booster, XGBModel]], feature_weights: Optional[_DaskCollection], - callbacks: Optional[Sequence[TrainingCallback]], ) -> "DaskXGBClassifier": params = self.get_xgb_params() dtrain, evals = await _async_wrap_evaluation_matrices( @@ -1924,9 +1913,7 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): obj: Optional[Callable] = _objective_decorator(self.objective) else: obj = None - model, metric, params, early_stopping_rounds, callbacks = self._configure_fit( - xgb_model, eval_metric, params, early_stopping_rounds, callbacks - ) + model, metric, params = self._configure_fit(xgb_model, params) results = await self.client.sync( _train_async, asynchronous=True, @@ -1941,8 +1928,8 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): feval=None, custom_metric=metric, verbose_eval=verbose, - early_stopping_rounds=early_stopping_rounds, - callbacks=callbacks, + early_stopping_rounds=self.early_stopping_rounds, + callbacks=self.callbacks, xgb_model=model, ) self._Booster = results["booster"] @@ -1960,14 +1947,11 @@ class DaskXGBClassifier(DaskScikitLearnBase, XGBClassifierBase): sample_weight: Optional[_DaskCollection] = None, base_margin: Optional[_DaskCollection] = None, eval_set: Optional[Sequence[Tuple[_DaskCollection, _DaskCollection]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Callable]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Union[int, bool] = True, xgb_model: Optional[Union[Booster, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[_DaskCollection]] = None, base_margin_eval_set: Optional[Sequence[_DaskCollection]] = None, feature_weights: Optional[_DaskCollection] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "DaskXGBClassifier": _assert_dask_support() args = {k: v for k, v in locals().items() if k not in ("self", "__class__")} @@ -2063,7 +2047,7 @@ class DaskXGBRanker(DaskScikitLearnBase, XGBRankerMixIn): def __init__(self, *, objective: str = "rank:pairwise", **kwargs: Any): if callable(objective): raise ValueError("Custom objective function not supported by XGBRanker.") - super().__init__(objective=objective, kwargs=kwargs) + super().__init__(objective=objective, **kwargs) async def _fit_async( self, @@ -2078,12 +2062,9 @@ class DaskXGBRanker(DaskScikitLearnBase, XGBRankerMixIn): base_margin_eval_set: Optional[Sequence[_DaskCollection]], eval_group: Optional[Sequence[_DaskCollection]], eval_qid: Optional[Sequence[_DaskCollection]], - eval_metric: Optional[Union[str, Sequence[str], Metric]], - early_stopping_rounds: Optional[int], verbose: Union[int, bool], xgb_model: Optional[Union[XGBModel, Booster]], feature_weights: Optional[_DaskCollection], - callbacks: Optional[Sequence[TrainingCallback]], ) -> "DaskXGBRanker": msg = "Use `qid` instead of `group` on dask interface." if not (group is None and eval_group is None): @@ -2111,14 +2092,7 @@ class DaskXGBRanker(DaskScikitLearnBase, XGBRankerMixIn): enable_categorical=self.enable_categorical, feature_types=self.feature_types, ) - if eval_metric is not None: - if callable(eval_metric): - raise ValueError( - "Custom evaluation metric is not yet supported for XGBRanker." - ) - model, metric, params, early_stopping_rounds, callbacks = self._configure_fit( - xgb_model, eval_metric, params, early_stopping_rounds, callbacks - ) + model, metric, params = self._configure_fit(xgb_model, params) results = await self.client.sync( _train_async, asynchronous=True, @@ -2133,8 +2107,8 @@ class DaskXGBRanker(DaskScikitLearnBase, XGBRankerMixIn): feval=None, custom_metric=metric, verbose_eval=verbose, - early_stopping_rounds=early_stopping_rounds, - callbacks=callbacks, + early_stopping_rounds=self.early_stopping_rounds, + callbacks=self.callbacks, xgb_model=model, ) self._Booster = results["booster"] @@ -2155,14 +2129,11 @@ class DaskXGBRanker(DaskScikitLearnBase, XGBRankerMixIn): eval_set: Optional[Sequence[Tuple[_DaskCollection, _DaskCollection]]] = None, eval_group: Optional[Sequence[_DaskCollection]] = None, eval_qid: Optional[Sequence[_DaskCollection]] = None, - eval_metric: Optional[Union[str, Sequence[str], Callable]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Union[int, bool] = False, xgb_model: Optional[Union[XGBModel, Booster]] = None, sample_weight_eval_set: Optional[Sequence[_DaskCollection]] = None, base_margin_eval_set: Optional[Sequence[_DaskCollection]] = None, feature_weights: Optional[_DaskCollection] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "DaskXGBRanker": _assert_dask_support() args = {k: v for k, v in locals().items() if k not in ("self", "__class__")} @@ -2221,18 +2192,15 @@ class DaskXGBRFRegressor(DaskXGBRegressor): sample_weight: Optional[_DaskCollection] = None, base_margin: Optional[_DaskCollection] = None, eval_set: Optional[Sequence[Tuple[_DaskCollection, _DaskCollection]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Callable]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Union[int, bool] = True, xgb_model: Optional[Union[Booster, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[_DaskCollection]] = None, base_margin_eval_set: Optional[Sequence[_DaskCollection]] = None, feature_weights: Optional[_DaskCollection] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "DaskXGBRFRegressor": _assert_dask_support() args = {k: v for k, v in locals().items() if k not in ("self", "__class__")} - _check_rf_callback(early_stopping_rounds, callbacks) + _check_rf_callback(self.early_stopping_rounds, self.callbacks) super().fit(**args) return self @@ -2285,17 +2253,14 @@ class DaskXGBRFClassifier(DaskXGBClassifier): sample_weight: Optional[_DaskCollection] = None, base_margin: Optional[_DaskCollection] = None, eval_set: Optional[Sequence[Tuple[_DaskCollection, _DaskCollection]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Callable]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Union[int, bool] = True, xgb_model: Optional[Union[Booster, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[_DaskCollection]] = None, base_margin_eval_set: Optional[Sequence[_DaskCollection]] = None, feature_weights: Optional[_DaskCollection] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "DaskXGBRFClassifier": _assert_dask_support() args = {k: v for k, v in locals().items() if k not in ("self", "__class__")} - _check_rf_callback(early_stopping_rounds, callbacks) + _check_rf_callback(self.early_stopping_rounds, self.callbacks) super().fit(**args) return self diff --git a/python-package/xgboost/sklearn.py b/python-package/xgboost/sklearn.py index 8c3a96784..a0fde2292 100644 --- a/python-package/xgboost/sklearn.py +++ b/python-package/xgboost/sklearn.py @@ -349,12 +349,6 @@ __model_doc = f""" See :doc:`/tutorials/custom_metric_obj` and :ref:`custom-obj-metric` for more information. - .. note:: - - This parameter replaces `eval_metric` in :py:meth:`fit` method. The old - one receives un-transformed prediction regardless of whether custom - objective is being used. - .. code-block:: python from sklearn.datasets import load_diabetes @@ -389,10 +383,6 @@ __model_doc = f""" early stopping. If there's more than one metric in **eval_metric**, the last metric will be used for early stopping. - .. note:: - - This parameter replaces `early_stopping_rounds` in :py:meth:`fit` method. - callbacks : Optional[List[TrainingCallback]] List of callback functions that are applied at end of each iteration. It is possible to use predefined callbacks by using @@ -872,16 +862,11 @@ class XGBModel(XGBModelBase): def _configure_fit( self, booster: Optional[Union[Booster, "XGBModel", str]], - eval_metric: Optional[Union[Callable, str, Sequence[str]]], params: Dict[str, Any], - early_stopping_rounds: Optional[int], - callbacks: Optional[Sequence[TrainingCallback]], ) -> Tuple[ Optional[Union[Booster, str, "XGBModel"]], Optional[Metric], Dict[str, Any], - Optional[int], - Optional[Sequence[TrainingCallback]], ]: """Configure parameters for :py:meth:`fit`.""" if isinstance(booster, XGBModel): @@ -903,49 +888,16 @@ class XGBModel(XGBModelBase): "or `set_params` instead." ) - # Configure evaluation metric. - if eval_metric is not None: - _deprecated("eval_metric") - if self.eval_metric is not None and eval_metric is not None: - _duplicated("eval_metric") - # - track where does the evaluation metric come from - if self.eval_metric is not None: - from_fit = False - eval_metric = self.eval_metric - else: - from_fit = True # - configure callable evaluation metric metric: Optional[Metric] = None - if eval_metric is not None: - if callable(eval_metric) and from_fit: - # No need to wrap the evaluation function for old parameter. - metric = eval_metric - elif callable(eval_metric): - # Parameter from constructor or set_params + if self.eval_metric is not None: + if callable(self.eval_metric): if self._get_type() == "ranker": - metric = ltr_metric_decorator(eval_metric, self.n_jobs) + metric = ltr_metric_decorator(self.eval_metric, self.n_jobs) else: - metric = _metric_decorator(eval_metric) + metric = _metric_decorator(self.eval_metric) else: - params.update({"eval_metric": eval_metric}) - - # Configure early_stopping_rounds - if early_stopping_rounds is not None: - _deprecated("early_stopping_rounds") - if early_stopping_rounds is not None and self.early_stopping_rounds is not None: - _duplicated("early_stopping_rounds") - early_stopping_rounds = ( - self.early_stopping_rounds - if self.early_stopping_rounds is not None - else early_stopping_rounds - ) - - # Configure callbacks - if callbacks is not None: - _deprecated("callbacks") - if callbacks is not None and self.callbacks is not None: - _duplicated("callbacks") - callbacks = self.callbacks if self.callbacks is not None else callbacks + params.update({"eval_metric": self.eval_metric}) tree_method = params.get("tree_method", None) if self.enable_categorical and tree_method == "exact": @@ -953,7 +905,7 @@ class XGBModel(XGBModelBase): "Experimental support for categorical data is not implemented for" " current tree method yet." ) - return model, metric, params, early_stopping_rounds, callbacks + return model, metric, params def _create_dmatrix(self, ref: Optional[DMatrix], **kwargs: Any) -> DMatrix: # Use `QuantileDMatrix` to save memory. @@ -979,14 +931,11 @@ class XGBModel(XGBModelBase): sample_weight: Optional[ArrayLike] = None, base_margin: Optional[ArrayLike] = None, eval_set: Optional[Sequence[Tuple[ArrayLike, ArrayLike]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Metric]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Optional[Union[bool, int]] = True, xgb_model: Optional[Union[Booster, str, "XGBModel"]] = None, sample_weight_eval_set: Optional[Sequence[ArrayLike]] = None, base_margin_eval_set: Optional[Sequence[ArrayLike]] = None, feature_weights: Optional[ArrayLike] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "XGBModel": # pylint: disable=invalid-name,attribute-defined-outside-init """Fit gradient boosting model. @@ -1017,18 +966,6 @@ class XGBModel(XGBModelBase): metrics will be computed. Validation metrics will help us track the performance of the model. - eval_metric : str, list of str, or callable, optional - - .. deprecated:: 1.6.0 - - Use `eval_metric` in :py:meth:`__init__` or :py:meth:`set_params` instead. - - early_stopping_rounds : int - - .. deprecated:: 1.6.0 - - Use `early_stopping_rounds` in :py:meth:`__init__` or :py:meth:`set_params` - instead. verbose : If `verbose` is True and an evaluation set is used, the evaluation metric measured on the validation set is printed to stdout at each boosting stage. @@ -1049,10 +986,6 @@ class XGBModel(XGBModelBase): selected when colsample is being used. All values must be greater than 0, otherwise a `ValueError` is thrown. - callbacks : - .. deprecated:: 1.6.0 - Use `callbacks` in :py:meth:`__init__` or :py:meth:`set_params` instead. - """ with config_context(verbosity=self.verbosity): evals_result: TrainingCallback.EvalsLog = {} @@ -1082,27 +1015,19 @@ class XGBModel(XGBModelBase): else: obj = None - ( - model, - metric, - params, - early_stopping_rounds, - callbacks, - ) = self._configure_fit( - xgb_model, eval_metric, params, early_stopping_rounds, callbacks - ) + model, metric, params = self._configure_fit(xgb_model, params) self._Booster = train( params, train_dmatrix, self.get_num_boosting_rounds(), evals=evals, - early_stopping_rounds=early_stopping_rounds, + early_stopping_rounds=self.early_stopping_rounds, evals_result=evals_result, obj=obj, custom_metric=metric, verbose_eval=verbose, xgb_model=model, - callbacks=callbacks, + callbacks=self.callbacks, ) self._set_evaluation_result(evals_result) @@ -1437,14 +1362,11 @@ class XGBClassifier(XGBModel, XGBClassifierBase): sample_weight: Optional[ArrayLike] = None, base_margin: Optional[ArrayLike] = None, eval_set: Optional[Sequence[Tuple[ArrayLike, ArrayLike]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Metric]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Optional[Union[bool, int]] = True, xgb_model: Optional[Union[Booster, str, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[ArrayLike]] = None, base_margin_eval_set: Optional[Sequence[ArrayLike]] = None, feature_weights: Optional[ArrayLike] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "XGBClassifier": # pylint: disable = attribute-defined-outside-init,too-many-statements with config_context(verbosity=self.verbosity): @@ -1492,15 +1414,7 @@ class XGBClassifier(XGBModel, XGBClassifierBase): params["objective"] = "multi:softprob" params["num_class"] = self.n_classes_ - ( - model, - metric, - params, - early_stopping_rounds, - callbacks, - ) = self._configure_fit( - xgb_model, eval_metric, params, early_stopping_rounds, callbacks - ) + model, metric, params = self._configure_fit(xgb_model, params) train_dmatrix, evals = _wrap_evaluation_matrices( missing=self.missing, X=X, @@ -1525,13 +1439,13 @@ class XGBClassifier(XGBModel, XGBClassifierBase): train_dmatrix, self.get_num_boosting_rounds(), evals=evals, - early_stopping_rounds=early_stopping_rounds, + early_stopping_rounds=self.early_stopping_rounds, evals_result=evals_result, obj=obj, custom_metric=metric, verbose_eval=verbose, xgb_model=model, - callbacks=callbacks, + callbacks=self.callbacks, ) if not callable(self.objective): @@ -1693,17 +1607,14 @@ class XGBRFClassifier(XGBClassifier): sample_weight: Optional[ArrayLike] = None, base_margin: Optional[ArrayLike] = None, eval_set: Optional[Sequence[Tuple[ArrayLike, ArrayLike]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Metric]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Optional[Union[bool, int]] = True, xgb_model: Optional[Union[Booster, str, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[ArrayLike]] = None, base_margin_eval_set: Optional[Sequence[ArrayLike]] = None, feature_weights: Optional[ArrayLike] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "XGBRFClassifier": args = {k: v for k, v in locals().items() if k not in ("self", "__class__")} - _check_rf_callback(early_stopping_rounds, callbacks) + _check_rf_callback(self.early_stopping_rounds, self.callbacks) super().fit(**args) return self @@ -1768,17 +1679,14 @@ class XGBRFRegressor(XGBRegressor): sample_weight: Optional[ArrayLike] = None, base_margin: Optional[ArrayLike] = None, eval_set: Optional[Sequence[Tuple[ArrayLike, ArrayLike]]] = None, - eval_metric: Optional[Union[str, Sequence[str], Metric]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Optional[Union[bool, int]] = True, xgb_model: Optional[Union[Booster, str, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[ArrayLike]] = None, base_margin_eval_set: Optional[Sequence[ArrayLike]] = None, feature_weights: Optional[ArrayLike] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "XGBRFRegressor": args = {k: v for k, v in locals().items() if k not in ("self", "__class__")} - _check_rf_callback(early_stopping_rounds, callbacks) + _check_rf_callback(self.early_stopping_rounds, self.callbacks) super().fit(**args) return self @@ -1883,14 +1791,11 @@ class XGBRanker(XGBModel, XGBRankerMixIn): eval_set: Optional[Sequence[Tuple[ArrayLike, ArrayLike]]] = None, eval_group: Optional[Sequence[ArrayLike]] = None, eval_qid: Optional[Sequence[ArrayLike]] = None, - eval_metric: Optional[Union[str, Sequence[str], Metric]] = None, - early_stopping_rounds: Optional[int] = None, verbose: Optional[Union[bool, int]] = False, xgb_model: Optional[Union[Booster, str, XGBModel]] = None, sample_weight_eval_set: Optional[Sequence[ArrayLike]] = None, base_margin_eval_set: Optional[Sequence[ArrayLike]] = None, feature_weights: Optional[ArrayLike] = None, - callbacks: Optional[Sequence[TrainingCallback]] = None, ) -> "XGBRanker": # pylint: disable = attribute-defined-outside-init,arguments-differ """Fit gradient boosting ranker @@ -1960,15 +1865,6 @@ class XGBRanker(XGBModel, XGBRankerMixIn): pair in **eval_set**. The special column convention in `X` applies to validation datasets as well. - eval_metric : str, list of str, optional - .. deprecated:: 1.6.0 - use `eval_metric` in :py:meth:`__init__` or :py:meth:`set_params` instead. - - early_stopping_rounds : int - .. deprecated:: 1.6.0 - use `early_stopping_rounds` in :py:meth:`__init__` or - :py:meth:`set_params` instead. - verbose : If `verbose` is True and an evaluation set is used, the evaluation metric measured on the validation set is printed to stdout at each boosting stage. @@ -1996,10 +1892,6 @@ class XGBRanker(XGBModel, XGBRankerMixIn): selected when colsample is being used. All values must be greater than 0, otherwise a `ValueError` is thrown. - callbacks : - .. deprecated:: 1.6.0 - Use `callbacks` in :py:meth:`__init__` or :py:meth:`set_params` instead. - """ with config_context(verbosity=self.verbosity): train_dmatrix, evals = _wrap_evaluation_matrices( @@ -2024,27 +1916,19 @@ class XGBRanker(XGBModel, XGBRankerMixIn): evals_result: TrainingCallback.EvalsLog = {} params = self.get_xgb_params() - ( - model, - metric, - params, - early_stopping_rounds, - callbacks, - ) = self._configure_fit( - xgb_model, eval_metric, params, early_stopping_rounds, callbacks - ) + model, metric, params = self._configure_fit(xgb_model, params) self._Booster = train( params, train_dmatrix, num_boost_round=self.get_num_boosting_rounds(), - early_stopping_rounds=early_stopping_rounds, + early_stopping_rounds=self.early_stopping_rounds, evals=evals, evals_result=evals_result, custom_metric=metric, verbose_eval=verbose, xgb_model=model, - callbacks=callbacks, + callbacks=self.callbacks, ) self.objective = params["objective"] diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index 87d76607f..bd9b9bedb 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -18,10 +18,12 @@ class LintersPaths: "python-package/", # tests "tests/python/test_config.py", + "tests/python/test_callback.py", "tests/python/test_data_iterator.py", "tests/python/test_dmatrix.py", "tests/python/test_dt.py", "tests/python/test_demos.py", + "tests/python/test_eval_metrics.py", "tests/python/test_multi_target.py", "tests/python/test_predict.py", "tests/python/test_quantile_dmatrix.py", @@ -39,12 +41,15 @@ class LintersPaths: "demo/dask/", "demo/rmm_plugin", "demo/json-model/json_parser.py", + "demo/guide-python/continuation.py", "demo/guide-python/cat_in_the_dat.py", "demo/guide-python/callbacks.py", "demo/guide-python/categorical.py", "demo/guide-python/cat_pipeline.py", "demo/guide-python/feature_weights.py", "demo/guide-python/sklearn_parallel.py", + "demo/guide-python/sklearn_examples.py", + "demo/guide-python/sklearn_evals_result.py", "demo/guide-python/spark_estimator_examples.py", "demo/guide-python/external_memory.py", "demo/guide-python/individual_trees.py", @@ -93,6 +98,7 @@ class LintersPaths: # demo "demo/json-model/json_parser.py", "demo/guide-python/external_memory.py", + "demo/guide-python/continuation.py", "demo/guide-python/callbacks.py", "demo/guide-python/cat_in_the_dat.py", "demo/guide-python/categorical.py", diff --git a/tests/python/test_callback.py b/tests/python/test_callback.py index 3a7501e48..d2e7cb5c4 100644 --- a/tests/python/test_callback.py +++ b/tests/python/test_callback.py @@ -16,13 +16,14 @@ class TestCallbacks: @classmethod def setup_class(cls): from sklearn.datasets import load_breast_cancer + X, y = load_breast_cancer(return_X_y=True) cls.X = X cls.y = y - split = int(X.shape[0]*0.8) - cls.X_train = X[: split, ...] - cls.y_train = y[: split, ...] + split = int(X.shape[0] * 0.8) + cls.X_train = X[:split, ...] + cls.y_train = y[:split, ...] cls.X_valid = X[split:, ...] cls.y_valid = y[split:, ...] @@ -31,31 +32,32 @@ class TestCallbacks: D_train: xgb.DMatrix, D_valid: xgb.DMatrix, rounds: int, - verbose_eval: Union[bool, int] + verbose_eval: Union[bool, int], ): def check_output(output: str) -> None: if int(verbose_eval) == 1: # Should print each iteration info - assert len(output.split('\n')) == rounds + assert len(output.split("\n")) == rounds elif int(verbose_eval) > rounds: # Should print first and latest iteration info - assert len(output.split('\n')) == 2 + assert len(output.split("\n")) == 2 else: # Should print info by each period additionaly to first and latest # iteration num_periods = rounds // int(verbose_eval) # Extra information is required for latest iteration is_extra_info_required = num_periods * int(verbose_eval) < (rounds - 1) - assert len(output.split('\n')) == ( + assert len(output.split("\n")) == ( 1 + num_periods + int(is_extra_info_required) ) evals_result: xgb.callback.TrainingCallback.EvalsLog = {} - params = {'objective': 'binary:logistic', 'eval_metric': 'error'} + params = {"objective": "binary:logistic", "eval_metric": "error"} with tm.captured_output() as (out, err): xgb.train( - params, D_train, - evals=[(D_train, 'Train'), (D_valid, 'Valid')], + params, + D_train, + evals=[(D_train, "Train"), (D_valid, "Valid")], num_boost_round=rounds, evals_result=evals_result, verbose_eval=verbose_eval, @@ -73,14 +75,16 @@ class TestCallbacks: D_valid = xgb.DMatrix(self.X_valid, self.y_valid) evals_result = {} rounds = 10 - xgb.train({'objective': 'binary:logistic', - 'eval_metric': 'error'}, D_train, - evals=[(D_train, 'Train'), (D_valid, 'Valid')], - num_boost_round=rounds, - evals_result=evals_result, - verbose_eval=True) - assert len(evals_result['Train']['error']) == rounds - assert len(evals_result['Valid']['error']) == rounds + xgb.train( + {"objective": "binary:logistic", "eval_metric": "error"}, + D_train, + evals=[(D_train, "Train"), (D_valid, "Valid")], + num_boost_round=rounds, + evals_result=evals_result, + verbose_eval=True, + ) + assert len(evals_result["Train"]["error"]) == rounds + assert len(evals_result["Valid"]["error"]) == rounds self.run_evaluation_monitor(D_train, D_valid, rounds, True) self.run_evaluation_monitor(D_train, D_valid, rounds, 2) @@ -93,72 +97,83 @@ class TestCallbacks: evals_result = {} rounds = 30 early_stopping_rounds = 5 - booster = xgb.train({'objective': 'binary:logistic', - 'eval_metric': 'error'}, D_train, - evals=[(D_train, 'Train'), (D_valid, 'Valid')], - num_boost_round=rounds, - evals_result=evals_result, - verbose_eval=True, - early_stopping_rounds=early_stopping_rounds) - dump = booster.get_dump(dump_format='json') + booster = xgb.train( + {"objective": "binary:logistic", "eval_metric": "error"}, + D_train, + evals=[(D_train, "Train"), (D_valid, "Valid")], + num_boost_round=rounds, + evals_result=evals_result, + verbose_eval=True, + early_stopping_rounds=early_stopping_rounds, + ) + dump = booster.get_dump(dump_format="json") assert len(dump) - booster.best_iteration == early_stopping_rounds + 1 def test_early_stopping_custom_eval(self): D_train = xgb.DMatrix(self.X_train, self.y_train) D_valid = xgb.DMatrix(self.X_valid, self.y_valid) early_stopping_rounds = 5 - booster = xgb.train({'objective': 'binary:logistic', - 'eval_metric': 'error', - 'tree_method': 'hist'}, D_train, - evals=[(D_train, 'Train'), (D_valid, 'Valid')], - feval=tm.eval_error_metric, - num_boost_round=1000, - early_stopping_rounds=early_stopping_rounds, - verbose_eval=False) - dump = booster.get_dump(dump_format='json') + booster = xgb.train( + { + "objective": "binary:logistic", + "eval_metric": "error", + "tree_method": "hist", + }, + D_train, + evals=[(D_train, "Train"), (D_valid, "Valid")], + feval=tm.eval_error_metric, + num_boost_round=1000, + early_stopping_rounds=early_stopping_rounds, + verbose_eval=False, + ) + dump = booster.get_dump(dump_format="json") assert len(dump) - booster.best_iteration == early_stopping_rounds + 1 def test_early_stopping_customize(self): D_train = xgb.DMatrix(self.X_train, self.y_train) D_valid = xgb.DMatrix(self.X_valid, self.y_valid) early_stopping_rounds = 5 - early_stop = xgb.callback.EarlyStopping(rounds=early_stopping_rounds, - metric_name='CustomErr', - data_name='Train') + early_stop = xgb.callback.EarlyStopping( + rounds=early_stopping_rounds, metric_name="CustomErr", data_name="Train" + ) # Specify which dataset and which metric should be used for early stopping. booster = xgb.train( - {'objective': 'binary:logistic', - 'eval_metric': ['error', 'rmse'], - 'tree_method': 'hist'}, D_train, - evals=[(D_train, 'Train'), (D_valid, 'Valid')], + { + "objective": "binary:logistic", + "eval_metric": ["error", "rmse"], + "tree_method": "hist", + }, + D_train, + evals=[(D_train, "Train"), (D_valid, "Valid")], feval=tm.eval_error_metric, num_boost_round=1000, callbacks=[early_stop], - verbose_eval=False) - dump = booster.get_dump(dump_format='json') + verbose_eval=False, + ) + dump = booster.get_dump(dump_format="json") assert len(dump) - booster.best_iteration == early_stopping_rounds + 1 - assert len(early_stop.stopping_history['Train']['CustomErr']) == len(dump) + assert len(early_stop.stopping_history["Train"]["CustomErr"]) == len(dump) rounds = 100 early_stop = xgb.callback.EarlyStopping( rounds=early_stopping_rounds, - metric_name='CustomErr', - data_name='Train', + metric_name="CustomErr", + data_name="Train", min_delta=100, save_best=True, ) booster = xgb.train( { - 'objective': 'binary:logistic', - 'eval_metric': ['error', 'rmse'], - 'tree_method': 'hist' + "objective": "binary:logistic", + "eval_metric": ["error", "rmse"], + "tree_method": "hist", }, D_train, - evals=[(D_train, 'Train'), (D_valid, 'Valid')], + evals=[(D_train, "Train"), (D_valid, "Valid")], feval=tm.eval_error_metric, num_boost_round=rounds, callbacks=[early_stop], - verbose_eval=False + verbose_eval=False, ) # No iteration can be made with min_delta == 100 assert booster.best_iteration == 0 @@ -166,18 +181,20 @@ class TestCallbacks: def test_early_stopping_skl(self): from sklearn.datasets import load_breast_cancer + X, y = load_breast_cancer(return_X_y=True) early_stopping_rounds = 5 cls = xgb.XGBClassifier( - early_stopping_rounds=early_stopping_rounds, eval_metric='error' + early_stopping_rounds=early_stopping_rounds, eval_metric="error" ) cls.fit(X, y, eval_set=[(X, y)]) booster = cls.get_booster() - dump = booster.get_dump(dump_format='json') + dump = booster.get_dump(dump_format="json") assert len(dump) - booster.best_iteration == early_stopping_rounds + 1 def test_early_stopping_custom_eval_skl(self): from sklearn.datasets import load_breast_cancer + X, y = load_breast_cancer(return_X_y=True) early_stopping_rounds = 5 early_stop = xgb.callback.EarlyStopping(rounds=early_stopping_rounds) @@ -186,11 +203,12 @@ class TestCallbacks: ) cls.fit(X, y, eval_set=[(X, y)]) booster = cls.get_booster() - dump = booster.get_dump(dump_format='json') + dump = booster.get_dump(dump_format="json") assert len(dump) - booster.best_iteration == early_stopping_rounds + 1 def test_early_stopping_save_best_model(self): from sklearn.datasets import load_breast_cancer + X, y = load_breast_cancer(return_X_y=True) n_estimators = 100 early_stopping_rounds = 5 @@ -200,11 +218,11 @@ class TestCallbacks: cls = xgb.XGBClassifier( n_estimators=n_estimators, eval_metric=tm.eval_error_metric_skl, - callbacks=[early_stop] + callbacks=[early_stop], ) cls.fit(X, y, eval_set=[(X, y)]) booster = cls.get_booster() - dump = booster.get_dump(dump_format='json') + dump = booster.get_dump(dump_format="json") assert len(dump) == booster.best_iteration + 1 early_stop = xgb.callback.EarlyStopping( @@ -220,8 +238,9 @@ class TestCallbacks: cls.fit(X, y, eval_set=[(X, y)]) # No error - early_stop = xgb.callback.EarlyStopping(rounds=early_stopping_rounds, - save_best=False) + early_stop = xgb.callback.EarlyStopping( + rounds=early_stopping_rounds, save_best=False + ) xgb.XGBClassifier( booster="gblinear", n_estimators=10, @@ -231,14 +250,17 @@ class TestCallbacks: def test_early_stopping_continuation(self): from sklearn.datasets import load_breast_cancer + X, y = load_breast_cancer(return_X_y=True) - cls = xgb.XGBClassifier(eval_metric=tm.eval_error_metric_skl) + early_stopping_rounds = 5 early_stop = xgb.callback.EarlyStopping( rounds=early_stopping_rounds, save_best=True ) - with pytest.warns(UserWarning): - cls.fit(X, y, eval_set=[(X, y)], callbacks=[early_stop]) + cls = xgb.XGBClassifier( + eval_metric=tm.eval_error_metric_skl, callbacks=[early_stop] + ) + cls.fit(X, y, eval_set=[(X, y)]) booster = cls.get_booster() assert booster.num_boosted_rounds() == booster.best_iteration + 1 @@ -256,21 +278,10 @@ class TestCallbacks: ) cls.fit(X, y, eval_set=[(X, y)]) booster = cls.get_booster() - assert booster.num_boosted_rounds() == \ - booster.best_iteration + early_stopping_rounds + 1 - - def test_deprecated(self): - from sklearn.datasets import load_breast_cancer - X, y = load_breast_cancer(return_X_y=True) - early_stopping_rounds = 5 - early_stop = xgb.callback.EarlyStopping( - rounds=early_stopping_rounds, save_best=True - ) - clf = xgb.XGBClassifier( - eval_metric=tm.eval_error_metric_skl, callbacks=[early_stop] - ) - with pytest.raises(ValueError, match=r".*set_params.*"): - clf.fit(X, y, eval_set=[(X, y)], callbacks=[early_stop]) + assert ( + booster.num_boosted_rounds() + == booster.best_iteration + early_stopping_rounds + 1 + ) def run_eta_decay(self, tree_method): """Test learning rate scheduler, used by both CPU and GPU tests.""" @@ -343,7 +354,7 @@ class TestCallbacks: callbacks=[scheduler([0, 0, 0, 0])], evals_result=evals_result, ) - eval_errors_2 = list(map(float, evals_result['eval']['error'])) + eval_errors_2 = list(map(float, evals_result["eval"]["error"])) assert isinstance(bst, xgb.core.Booster) # validation error should not decrease, if eta/learning_rate = 0 assert eval_errors_2[0] == eval_errors_2[-1] @@ -361,7 +372,7 @@ class TestCallbacks: callbacks=[scheduler(eta_decay)], evals_result=evals_result, ) - eval_errors_3 = list(map(float, evals_result['eval']['error'])) + eval_errors_3 = list(map(float, evals_result["eval"]["error"])) assert isinstance(bst, xgb.core.Booster) diff --git a/tests/python/test_early_stopping.py b/tests/python/test_early_stopping.py index 7695c6861..a275a8077 100644 --- a/tests/python/test_early_stopping.py +++ b/tests/python/test_early_stopping.py @@ -15,23 +15,23 @@ class TestEarlyStopping: from sklearn.model_selection import train_test_split digits = load_digits(n_class=2) - X = digits['data'] - y = digits['target'] + X = digits["data"] + y = digits["target"] X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0) - clf1 = xgb.XGBClassifier(learning_rate=0.1) - clf1.fit(X_train, y_train, early_stopping_rounds=5, eval_metric="auc", - eval_set=[(X_test, y_test)]) - clf2 = xgb.XGBClassifier(learning_rate=0.1) - clf2.fit(X_train, y_train, early_stopping_rounds=4, eval_metric="auc", - eval_set=[(X_test, y_test)]) + clf1 = xgb.XGBClassifier( + learning_rate=0.1, early_stopping_rounds=5, eval_metric="auc" + ) + clf1.fit(X_train, y_train, eval_set=[(X_test, y_test)]) + clf2 = xgb.XGBClassifier( + learning_rate=0.1, early_stopping_rounds=4, eval_metric="auc" + ) + clf2.fit(X_train, y_train, eval_set=[(X_test, y_test)]) # should be the same assert clf1.best_score == clf2.best_score assert clf1.best_score != 1 # check overfit clf3 = xgb.XGBClassifier( - learning_rate=0.1, - eval_metric="auc", - early_stopping_rounds=10 + learning_rate=0.1, eval_metric="auc", early_stopping_rounds=10 ) clf3.fit(X_train, y_train, eval_set=[(X_test, y_test)]) base_score = get_basescore(clf3) @@ -39,9 +39,9 @@ class TestEarlyStopping: clf3 = xgb.XGBClassifier( learning_rate=0.1, - base_score=.5, + base_score=0.5, eval_metric="auc", - early_stopping_rounds=10 + early_stopping_rounds=10, ) clf3.fit(X_train, y_train, eval_set=[(X_test, y_test)]) diff --git a/tests/python/test_eval_metrics.py b/tests/python/test_eval_metrics.py index 92726014b..cbb3dc88d 100644 --- a/tests/python/test_eval_metrics.py +++ b/tests/python/test_eval_metrics.py @@ -9,37 +9,41 @@ rng = np.random.RandomState(1337) class TestEvalMetrics: - xgb_params_01 = {'nthread': 1, 'eval_metric': 'error'} + xgb_params_01 = {"nthread": 1, "eval_metric": "error"} - xgb_params_02 = {'nthread': 1, 'eval_metric': ['error']} + xgb_params_02 = {"nthread": 1, "eval_metric": ["error"]} - xgb_params_03 = {'nthread': 1, 'eval_metric': ['rmse', 'error']} + xgb_params_03 = {"nthread": 1, "eval_metric": ["rmse", "error"]} - xgb_params_04 = {'nthread': 1, 'eval_metric': ['error', 'rmse']} + xgb_params_04 = {"nthread": 1, "eval_metric": ["error", "rmse"]} def evalerror_01(self, preds, dtrain): labels = dtrain.get_label() - return 'error', float(sum(labels != (preds > 0.0))) / len(labels) + return "error", float(sum(labels != (preds > 0.0))) / len(labels) def evalerror_02(self, preds, dtrain): labels = dtrain.get_label() - return [('error', float(sum(labels != (preds > 0.0))) / len(labels))] + return [("error", float(sum(labels != (preds > 0.0))) / len(labels))] @pytest.mark.skipif(**tm.no_sklearn()) def evalerror_03(self, preds, dtrain): from sklearn.metrics import mean_squared_error labels = dtrain.get_label() - return [('rmse', mean_squared_error(labels, preds)), - ('error', float(sum(labels != (preds > 0.0))) / len(labels))] + return [ + ("rmse", mean_squared_error(labels, preds)), + ("error", float(sum(labels != (preds > 0.0))) / len(labels)), + ] @pytest.mark.skipif(**tm.no_sklearn()) def evalerror_04(self, preds, dtrain): from sklearn.metrics import mean_squared_error labels = dtrain.get_label() - return [('error', float(sum(labels != (preds > 0.0))) / len(labels)), - ('rmse', mean_squared_error(labels, preds))] + return [ + ("error", float(sum(labels != (preds > 0.0))) / len(labels)), + ("rmse", mean_squared_error(labels, preds)), + ] @pytest.mark.skipif(**tm.no_sklearn()) def test_eval_metrics(self): @@ -50,15 +54,15 @@ class TestEvalMetrics: from sklearn.datasets import load_digits digits = load_digits(n_class=2) - X = digits['data'] - y = digits['target'] + X = digits["data"] + y = digits["target"] Xt, Xv, yt, yv = train_test_split(X, y, test_size=0.2, random_state=0) dtrain = xgb.DMatrix(Xt, label=yt) dvalid = xgb.DMatrix(Xv, label=yv) - watchlist = [(dtrain, 'train'), (dvalid, 'val')] + watchlist = [(dtrain, "train"), (dvalid, "val")] gbdt_01 = xgb.train(self.xgb_params_01, dtrain, num_boost_round=10) gbdt_02 = xgb.train(self.xgb_params_02, dtrain, num_boost_round=10) @@ -66,26 +70,54 @@ class TestEvalMetrics: assert gbdt_01.predict(dvalid)[0] == gbdt_02.predict(dvalid)[0] assert gbdt_01.predict(dvalid)[0] == gbdt_03.predict(dvalid)[0] - gbdt_01 = xgb.train(self.xgb_params_01, dtrain, 10, watchlist, - early_stopping_rounds=2) - gbdt_02 = xgb.train(self.xgb_params_02, dtrain, 10, watchlist, - early_stopping_rounds=2) - gbdt_03 = xgb.train(self.xgb_params_03, dtrain, 10, watchlist, - early_stopping_rounds=2) - gbdt_04 = xgb.train(self.xgb_params_04, dtrain, 10, watchlist, - early_stopping_rounds=2) + gbdt_01 = xgb.train( + self.xgb_params_01, dtrain, 10, watchlist, early_stopping_rounds=2 + ) + gbdt_02 = xgb.train( + self.xgb_params_02, dtrain, 10, watchlist, early_stopping_rounds=2 + ) + gbdt_03 = xgb.train( + self.xgb_params_03, dtrain, 10, watchlist, early_stopping_rounds=2 + ) + gbdt_04 = xgb.train( + self.xgb_params_04, dtrain, 10, watchlist, early_stopping_rounds=2 + ) assert gbdt_01.predict(dvalid)[0] == gbdt_02.predict(dvalid)[0] assert gbdt_01.predict(dvalid)[0] == gbdt_03.predict(dvalid)[0] assert gbdt_03.predict(dvalid)[0] != gbdt_04.predict(dvalid)[0] - gbdt_01 = xgb.train(self.xgb_params_01, dtrain, 10, watchlist, - early_stopping_rounds=2, feval=self.evalerror_01) - gbdt_02 = xgb.train(self.xgb_params_02, dtrain, 10, watchlist, - early_stopping_rounds=2, feval=self.evalerror_02) - gbdt_03 = xgb.train(self.xgb_params_03, dtrain, 10, watchlist, - early_stopping_rounds=2, feval=self.evalerror_03) - gbdt_04 = xgb.train(self.xgb_params_04, dtrain, 10, watchlist, - early_stopping_rounds=2, feval=self.evalerror_04) + gbdt_01 = xgb.train( + self.xgb_params_01, + dtrain, + 10, + watchlist, + early_stopping_rounds=2, + feval=self.evalerror_01, + ) + gbdt_02 = xgb.train( + self.xgb_params_02, + dtrain, + 10, + watchlist, + early_stopping_rounds=2, + feval=self.evalerror_02, + ) + gbdt_03 = xgb.train( + self.xgb_params_03, + dtrain, + 10, + watchlist, + early_stopping_rounds=2, + feval=self.evalerror_03, + ) + gbdt_04 = xgb.train( + self.xgb_params_04, + dtrain, + 10, + watchlist, + early_stopping_rounds=2, + feval=self.evalerror_04, + ) assert gbdt_01.predict(dvalid)[0] == gbdt_02.predict(dvalid)[0] assert gbdt_01.predict(dvalid)[0] == gbdt_03.predict(dvalid)[0] assert gbdt_03.predict(dvalid)[0] != gbdt_04.predict(dvalid)[0] @@ -93,6 +125,7 @@ class TestEvalMetrics: @pytest.mark.skipif(**tm.no_sklearn()) def test_gamma_deviance(self): from sklearn.metrics import mean_gamma_deviance + rng = np.random.RandomState(1994) n_samples = 100 n_features = 30 @@ -101,8 +134,13 @@ class TestEvalMetrics: y = rng.randn(n_samples) y = y - y.min() * 100 - reg = xgb.XGBRegressor(tree_method="hist", objective="reg:gamma", n_estimators=10) - reg.fit(X, y, eval_metric="gamma-deviance") + reg = xgb.XGBRegressor( + tree_method="hist", + objective="reg:gamma", + n_estimators=10, + eval_metric="gamma-deviance", + ) + reg.fit(X, y) booster = reg.get_booster() score = reg.predict(X) @@ -113,16 +151,26 @@ class TestEvalMetrics: @pytest.mark.skipif(**tm.no_sklearn()) def test_gamma_lik(self) -> None: import scipy.stats as stats + rng = np.random.default_rng(1994) n_samples = 32 n_features = 10 - X = rng.normal(0, 1, size=n_samples * n_features).reshape((n_samples, n_features)) + X = rng.normal(0, 1, size=n_samples * n_features).reshape( + (n_samples, n_features) + ) alpha, loc, beta = 5.0, 11.1, 22 - y = stats.gamma.rvs(alpha, loc=loc, scale=beta, size=n_samples, random_state=rng) - reg = xgb.XGBRegressor(tree_method="hist", objective="reg:gamma", n_estimators=64) - reg.fit(X, y, eval_metric="gamma-nloglik", eval_set=[(X, y)]) + y = stats.gamma.rvs( + alpha, loc=loc, scale=beta, size=n_samples, random_state=rng + ) + reg = xgb.XGBRegressor( + tree_method="hist", + objective="reg:gamma", + n_estimators=64, + eval_metric="gamma-nloglik", + ) + reg.fit(X, y, eval_set=[(X, y)]) score = reg.predict(X) @@ -134,7 +182,7 @@ class TestEvalMetrics: # XGBoost uses the canonical link function of gamma in evaluation function. # so \theta = - (1.0 / y) # dispersion is hardcoded as 1.0, so shape (a in scipy parameter) is also 1.0 - beta = - (1.0 / (- (1.0 / y))) # == y + beta = -(1.0 / (-(1.0 / y))) # == y nloglik_stats = -stats.gamma.logpdf(score, a=1.0, scale=beta) np.testing.assert_allclose(nloglik, np.mean(nloglik_stats), rtol=1e-3) @@ -153,7 +201,7 @@ class TestEvalMetrics: n_features, n_informative=n_features, n_redundant=0, - random_state=rng + random_state=rng, ) Xy = xgb.DMatrix(X, y) booster = xgb.train( @@ -197,7 +245,7 @@ class TestEvalMetrics: n_informative=n_features, n_redundant=0, n_classes=n_classes, - random_state=rng + random_state=rng, ) if weighted: weights = rng.randn(n_samples) @@ -242,20 +290,25 @@ class TestEvalMetrics: def run_pr_auc_binary(self, tree_method): from sklearn.datasets import make_classification from sklearn.metrics import auc, precision_recall_curve + X, y = make_classification(128, 4, n_classes=2, random_state=1994) - clf = xgb.XGBClassifier(tree_method=tree_method, n_estimators=1) - clf.fit(X, y, eval_metric="aucpr", eval_set=[(X, y)]) + clf = xgb.XGBClassifier( + tree_method=tree_method, n_estimators=1, eval_metric="aucpr" + ) + clf.fit(X, y, eval_set=[(X, y)]) evals_result = clf.evals_result()["validation_0"]["aucpr"][-1] y_score = clf.predict_proba(X)[:, 1] # get the positive column precision, recall, _ = precision_recall_curve(y, y_score) prauc = auc(recall, precision) - # Interpolation results are slightly different from sklearn, but overall should be - # similar. + # Interpolation results are slightly different from sklearn, but overall should + # be similar. np.testing.assert_allclose(prauc, evals_result, rtol=1e-2) - clf = xgb.XGBClassifier(tree_method=tree_method, n_estimators=10) - clf.fit(X, y, eval_metric="aucpr", eval_set=[(X, y)]) + clf = xgb.XGBClassifier( + tree_method=tree_method, n_estimators=10, eval_metric="aucpr" + ) + clf.fit(X, y, eval_set=[(X, y)]) evals_result = clf.evals_result()["validation_0"]["aucpr"][-1] np.testing.assert_allclose(0.99, evals_result, rtol=1e-2) @@ -264,16 +317,21 @@ class TestEvalMetrics: def run_pr_auc_multi(self, tree_method): from sklearn.datasets import make_classification + X, y = make_classification( 64, 16, n_informative=8, n_classes=3, random_state=1994 ) - clf = xgb.XGBClassifier(tree_method=tree_method, n_estimators=1) - clf.fit(X, y, eval_metric="aucpr", eval_set=[(X, y)]) + clf = xgb.XGBClassifier( + tree_method=tree_method, n_estimators=1, eval_metric="aucpr" + ) + clf.fit(X, y, eval_set=[(X, y)]) evals_result = clf.evals_result()["validation_0"]["aucpr"][-1] - # No available implementation for comparison, just check that XGBoost converges to - # 1.0 - clf = xgb.XGBClassifier(tree_method=tree_method, n_estimators=10) - clf.fit(X, y, eval_metric="aucpr", eval_set=[(X, y)]) + # No available implementation for comparison, just check that XGBoost converges + # to 1.0 + clf = xgb.XGBClassifier( + tree_method=tree_method, n_estimators=10, eval_metric="aucpr" + ) + clf.fit(X, y, eval_set=[(X, y)]) evals_result = clf.evals_result()["validation_0"]["aucpr"][-1] np.testing.assert_allclose(1.0, evals_result, rtol=1e-2) @@ -282,9 +340,13 @@ class TestEvalMetrics: def run_pr_auc_ltr(self, tree_method): from sklearn.datasets import make_classification + X, y = make_classification(128, 4, n_classes=2, random_state=1994) ltr = xgb.XGBRanker( - tree_method=tree_method, n_estimators=16, objective="rank:pairwise" + tree_method=tree_method, + n_estimators=16, + objective="rank:pairwise", + eval_metric="aucpr", ) groups = np.array([32, 32, 64]) ltr.fit( @@ -293,7 +355,6 @@ class TestEvalMetrics: group=groups, eval_set=[(X, y)], eval_group=[groups], - eval_metric="aucpr", ) results = ltr.evals_result()["validation_0"]["aucpr"] assert results[-1] >= 0.99 diff --git a/tests/python/test_training_continuation.py b/tests/python/test_training_continuation.py index 6b2f96301..1798a4d93 100644 --- a/tests/python/test_training_continuation.py +++ b/tests/python/test_training_continuation.py @@ -149,8 +149,8 @@ class TestTrainingContinuation: from sklearn.datasets import load_breast_cancer X, y = load_breast_cancer(return_X_y=True) - clf = xgb.XGBClassifier(n_estimators=2) - clf.fit(X, y, eval_set=[(X, y)], eval_metric="logloss") + clf = xgb.XGBClassifier(n_estimators=2, eval_metric="logloss") + clf.fit(X, y, eval_set=[(X, y)]) assert tm.non_increasing(clf.evals_result()["validation_0"]["logloss"]) with tempfile.TemporaryDirectory() as tmpdir: @@ -160,5 +160,6 @@ class TestTrainingContinuation: clf = xgb.XGBClassifier(n_estimators=2) # change metric to error - clf.fit(X, y, eval_set=[(X, y)], eval_metric="error") + clf.set_params(eval_metric="error") + clf.fit(X, y, eval_set=[(X, y)], xgb_model=loaded) assert tm.non_increasing(clf.evals_result()["validation_0"]["error"]) diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 47f1778d6..9047cee6e 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -30,8 +30,8 @@ def test_binary_classification(): kf = KFold(n_splits=2, shuffle=True, random_state=rng) for cls in (xgb.XGBClassifier, xgb.XGBRFClassifier): for train_index, test_index in kf.split(X, y): - clf = cls(random_state=42) - xgb_model = clf.fit(X[train_index], y[train_index], eval_metric=['auc', 'logloss']) + clf = cls(random_state=42, eval_metric=['auc', 'logloss']) + xgb_model = clf.fit(X[train_index], y[train_index]) preds = xgb_model.predict(X[test_index]) labels = y[test_index] err = sum(1 for i in range(len(preds)) @@ -101,10 +101,11 @@ def test_best_iteration(): def train(booster: str, forest: Optional[int]) -> None: rounds = 4 cls = xgb.XGBClassifier( - n_estimators=rounds, num_parallel_tree=forest, booster=booster - ).fit( - X, y, eval_set=[(X, y)], early_stopping_rounds=3 - ) + n_estimators=rounds, + num_parallel_tree=forest, + booster=booster, + early_stopping_rounds=3, + ).fit(X, y, eval_set=[(X, y)]) assert cls.best_iteration == rounds - 1 # best_iteration is used by default, assert that under gblinear it's @@ -112,9 +113,9 @@ def test_best_iteration(): cls.predict(X) num_parallel_tree = 4 - train('gbtree', num_parallel_tree) - train('dart', num_parallel_tree) - train('gblinear', None) + train("gbtree", num_parallel_tree) + train("dart", num_parallel_tree) + train("gblinear", None) def test_ranking(): @@ -258,6 +259,7 @@ def test_stacking_classification(): X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) clf.fit(X_train, y_train).score(X_test, y_test) + @pytest.mark.skipif(**tm.no_pandas()) def test_feature_importances_weight(): from sklearn.datasets import load_digits @@ -474,7 +476,8 @@ def run_housing_rf_regression(tree_method): rfreg = xgb.XGBRFRegressor() with pytest.raises(NotImplementedError): - rfreg.fit(X, y, early_stopping_rounds=10) + rfreg.set_params(early_stopping_rounds=10) + rfreg.fit(X, y) def test_rf_regression(): @@ -574,7 +577,7 @@ def test_classification_with_custom_objective(): return logregobj(y, p) cls.set_params(objective=wrapped) - cls.predict(X) # no throw + cls.predict(X) # no throw cls.fit(X, y) assert is_called[0] @@ -844,51 +847,65 @@ def run_validation_weights(model): y_train, y_test = y[:1600], y[1600:] # instantiate model - param_dist = {'objective': 'binary:logistic', 'n_estimators': 2, - 'random_state': 123} + param_dist = { + "objective": "binary:logistic", + "n_estimators": 2, + "random_state": 123, + } clf = model(**param_dist) # train it using instance weights only in the training set weights_train = np.random.choice([1, 2], len(X_train)) - clf.fit(X_train, y_train, - sample_weight=weights_train, - eval_set=[(X_test, y_test)], - eval_metric='logloss', - verbose=False) - + clf.set_params(eval_metric="logloss") + clf.fit( + X_train, + y_train, + sample_weight=weights_train, + eval_set=[(X_test, y_test)], + verbose=False, + ) # evaluate logloss metric on test set *without* using weights evals_result_without_weights = clf.evals_result() - logloss_without_weights = evals_result_without_weights[ - "validation_0"]["logloss"] + logloss_without_weights = evals_result_without_weights["validation_0"]["logloss"] # now use weights for the test set np.random.seed(0) weights_test = np.random.choice([1, 2], len(X_test)) - clf.fit(X_train, y_train, - sample_weight=weights_train, - eval_set=[(X_test, y_test)], - sample_weight_eval_set=[weights_test], - eval_metric='logloss', - verbose=False) + clf.set_params(eval_metric="logloss") + clf.fit( + X_train, + y_train, + sample_weight=weights_train, + eval_set=[(X_test, y_test)], + sample_weight_eval_set=[weights_test], + verbose=False, + ) evals_result_with_weights = clf.evals_result() logloss_with_weights = evals_result_with_weights["validation_0"]["logloss"] # check that the logloss in the test set is actually different when using # weights than when not using them - assert all((logloss_with_weights[i] != logloss_without_weights[i] - for i in [0, 1])) + assert all((logloss_with_weights[i] != logloss_without_weights[i] for i in [0, 1])) with pytest.raises(ValueError): # length of eval set and sample weight doesn't match. - clf.fit(X_train, y_train, sample_weight=weights_train, - eval_set=[(X_train, y_train), (X_test, y_test)], - sample_weight_eval_set=[weights_train]) + clf.fit( + X_train, + y_train, + sample_weight=weights_train, + eval_set=[(X_train, y_train), (X_test, y_test)], + sample_weight_eval_set=[weights_train], + ) with pytest.raises(ValueError): cls = xgb.XGBClassifier() - cls.fit(X_train, y_train, sample_weight=weights_train, - eval_set=[(X_train, y_train), (X_test, y_test)], - sample_weight_eval_set=[weights_train]) + cls.fit( + X_train, + y_train, + sample_weight=weights_train, + eval_set=[(X_train, y_train), (X_test, y_test)], + sample_weight_eval_set=[weights_train], + ) def test_validation_weights(): @@ -960,8 +977,7 @@ def test_XGBClassifier_resume(): # file name of stored xgb model model1.save_model(model1_path) - model2 = xgb.XGBClassifier( - learning_rate=0.3, random_state=0, n_estimators=8) + model2 = xgb.XGBClassifier(learning_rate=0.3, random_state=0, n_estimators=8) model2.fit(X, Y, xgb_model=model1_path) pred2 = model2.predict(X) @@ -972,8 +988,7 @@ def test_XGBClassifier_resume(): # file name of 'Booster' instance Xgb model model1.get_booster().save_model(model1_booster_path) - model2 = xgb.XGBClassifier( - learning_rate=0.3, random_state=0, n_estimators=8) + model2 = xgb.XGBClassifier(learning_rate=0.3, random_state=0, n_estimators=8) model2.fit(X, Y, xgb_model=model1_booster_path) pred2 = model2.predict(X) @@ -1279,12 +1294,16 @@ def test_estimator_reg(estimator, check): ): estimator.fit(X, y) return - if os.environ["PYTEST_CURRENT_TEST"].find("check_estimators_overwrite_params") != -1: + if ( + os.environ["PYTEST_CURRENT_TEST"].find("check_estimators_overwrite_params") + != -1 + ): # A hack to pass the scikit-learn parameter mutation tests. XGBoost regressor - # returns actual internal default values for parameters in `get_params`, but those - # are set as `None` in sklearn interface to avoid duplication. So we fit a dummy - # model and obtain the default parameters here for the mutation tests. + # returns actual internal default values for parameters in `get_params`, but + # those are set as `None` in sklearn interface to avoid duplication. So we fit + # a dummy model and obtain the default parameters here for the mutation tests. from sklearn.datasets import make_regression + X, y = make_regression(n_samples=2, n_features=1) estimator.set_params(**xgb.XGBRegressor().fit(X, y).get_params()) @@ -1325,6 +1344,7 @@ def test_categorical(): def test_evaluation_metric(): from sklearn.datasets import load_diabetes, load_digits from sklearn.metrics import mean_absolute_error + X, y = load_diabetes(return_X_y=True) n_estimators = 16 @@ -1341,17 +1361,6 @@ def test_evaluation_metric(): for line in lines: assert line.find("mean_absolute_error") != -1 - def metric(predt: np.ndarray, Xy: xgb.DMatrix): - y = Xy.get_label() - return "m", np.abs(predt - y).sum() - - with pytest.warns(UserWarning): - reg = xgb.XGBRegressor( - tree_method="hist", - n_estimators=1, - ) - reg.fit(X, y, eval_set=[(X, y)], eval_metric=metric) - def merror(y_true: np.ndarray, predt: np.ndarray): n_samples = y_true.shape[0] assert n_samples == predt.size diff --git a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py index a15e1903d..865623671 100644 --- a/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py +++ b/tests/test_distributed/test_gpu_with_dask/test_gpu_with_dask.py @@ -363,12 +363,12 @@ class TestDistributedGPU: device="cuda", eval_metric="error", n_estimators=100, + early_stopping_rounds=early_stopping_rounds, ) cls.client = local_cuda_client cls.fit( X, y, - early_stopping_rounds=early_stopping_rounds, eval_set=[(valid_X, valid_y)], ) booster = cls.get_booster() diff --git a/tests/test_distributed/test_with_dask/test_with_dask.py b/tests/test_distributed/test_with_dask/test_with_dask.py index 79df025fe..150e698d3 100644 --- a/tests/test_distributed/test_with_dask/test_with_dask.py +++ b/tests/test_distributed/test_with_dask/test_with_dask.py @@ -937,8 +937,10 @@ def run_empty_dmatrix_auc(client: "Client", device: str, n_workers: int) -> None valid_X = dd.from_array(valid_X_, chunksize=n_samples) valid_y = dd.from_array(valid_y_, chunksize=n_samples) - cls = xgb.dask.DaskXGBClassifier(device=device, n_estimators=2) - cls.fit(X, y, eval_metric=["auc", "aucpr"], eval_set=[(valid_X, valid_y)]) + cls = xgb.dask.DaskXGBClassifier( + device=device, n_estimators=2, eval_metric=["auc", "aucpr"] + ) + cls.fit(X, y, eval_set=[(valid_X, valid_y)]) # multiclass X_, y_ = make_classification( @@ -966,8 +968,10 @@ def run_empty_dmatrix_auc(client: "Client", device: str, n_workers: int) -> None valid_X = dd.from_array(valid_X_, chunksize=n_samples) valid_y = dd.from_array(valid_y_, chunksize=n_samples) - cls = xgb.dask.DaskXGBClassifier(device=device, n_estimators=2) - cls.fit(X, y, eval_metric=["auc", "aucpr"], eval_set=[(valid_X, valid_y)]) + cls = xgb.dask.DaskXGBClassifier( + device=device, n_estimators=2, eval_metric=["auc", "aucpr"] + ) + cls.fit(X, y, eval_set=[(valid_X, valid_y)]) def test_empty_dmatrix_auc() -> None: @@ -994,11 +998,11 @@ def run_auc(client: "Client", device: str) -> None: valid_X = dd.from_array(valid_X_, chunksize=10) valid_y = dd.from_array(valid_y_, chunksize=10) - cls = xgb.XGBClassifier(device=device, n_estimators=2) - cls.fit(X_, y_, eval_metric="auc", eval_set=[(valid_X_, valid_y_)]) + cls = xgb.XGBClassifier(device=device, n_estimators=2, eval_metric="auc") + cls.fit(X_, y_, eval_set=[(valid_X_, valid_y_)]) - dcls = xgb.dask.DaskXGBClassifier(device=device, n_estimators=2) - dcls.fit(X, y, eval_metric="auc", eval_set=[(valid_X, valid_y)]) + dcls = xgb.dask.DaskXGBClassifier(device=device, n_estimators=2, eval_metric="auc") + dcls.fit(X, y, eval_set=[(valid_X, valid_y)]) approx = dcls.evals_result()["validation_0"]["auc"] exact = cls.evals_result()["validation_0"]["auc"] @@ -1267,16 +1271,16 @@ def test_dask_ranking(client: "Client") -> None: qid_valid = qid_valid.astype(np.uint32) qid_test = qid_test.astype(np.uint32) - rank = xgb.dask.DaskXGBRanker(n_estimators=2500) + rank = xgb.dask.DaskXGBRanker( + n_estimators=2500, eval_metric=["ndcg"], early_stopping_rounds=10 + ) rank.fit( x_train, y_train, qid=qid_train, eval_set=[(x_test, y_test), (x_train, y_train)], eval_qid=[qid_test, qid_train], - eval_metric=["ndcg"], verbose=True, - early_stopping_rounds=10, ) assert rank.n_features_in_ == 46 assert rank.best_score > 0.98 @@ -2150,13 +2154,15 @@ class TestDaskCallbacks: valid_X, valid_y = load_breast_cancer(return_X_y=True) valid_X, valid_y = da.from_array(valid_X), da.from_array(valid_y) cls = xgb.dask.DaskXGBClassifier( - objective="binary:logistic", tree_method="hist", n_estimators=1000 + objective="binary:logistic", + tree_method="hist", + n_estimators=1000, + early_stopping_rounds=early_stopping_rounds, ) cls.client = client cls.fit( X, y, - early_stopping_rounds=early_stopping_rounds, eval_set=[(valid_X, valid_y)], ) booster = cls.get_booster() @@ -2165,15 +2171,17 @@ class TestDaskCallbacks: # Specify the metric cls = xgb.dask.DaskXGBClassifier( - objective="binary:logistic", tree_method="hist", n_estimators=1000 + objective="binary:logistic", + tree_method="hist", + n_estimators=1000, + early_stopping_rounds=early_stopping_rounds, + eval_metric="error", ) cls.client = client cls.fit( X, y, - early_stopping_rounds=early_stopping_rounds, eval_set=[(valid_X, valid_y)], - eval_metric="error", ) assert tm.non_increasing(cls.evals_result()["validation_0"]["error"]) booster = cls.get_booster() @@ -2215,12 +2223,12 @@ class TestDaskCallbacks: tree_method="hist", n_estimators=1000, eval_metric=tm.eval_error_metric_skl, + early_stopping_rounds=early_stopping_rounds, ) cls.client = client cls.fit( X, y, - early_stopping_rounds=early_stopping_rounds, eval_set=[(valid_X, valid_y)], ) booster = cls.get_booster() @@ -2234,21 +2242,22 @@ class TestDaskCallbacks: X, y = load_breast_cancer(return_X_y=True) X, y = da.from_array(X), da.from_array(y) - cls = xgb.dask.DaskXGBClassifier( - objective="binary:logistic", tree_method="hist", n_estimators=10 - ) - cls.client = client - with tempfile.TemporaryDirectory() as tmpdir: - cls.fit( - X, - y, + cls = xgb.dask.DaskXGBClassifier( + objective="binary:logistic", + tree_method="hist", + n_estimators=10, callbacks=[ xgb.callback.TrainingCheckPoint( directory=Path(tmpdir), interval=1, name="model" ) ], ) + cls.client = client + cls.fit( + X, + y, + ) for i in range(1, 10): assert os.path.exists( os.path.join( diff --git a/tests/test_distributed/test_with_spark/test_spark_local.py b/tests/test_distributed/test_with_spark/test_spark_local.py index 406174542..b8c16ef1c 100644 --- a/tests/test_distributed/test_with_spark/test_spark_local.py +++ b/tests/test_distributed/test_with_spark/test_spark_local.py @@ -311,24 +311,20 @@ def clf_with_weight( y_val = np.array([0, 1]) w_train = np.array([1.0, 2.0]) w_val = np.array([1.0, 2.0]) - cls2 = XGBClassifier() + cls2 = XGBClassifier(eval_metric="logloss", early_stopping_rounds=1) cls2.fit( X_train, y_train, eval_set=[(X_val, y_val)], - early_stopping_rounds=1, - eval_metric="logloss", ) - cls3 = XGBClassifier() + cls3 = XGBClassifier(eval_metric="logloss", early_stopping_rounds=1) cls3.fit( X_train, y_train, sample_weight=w_train, eval_set=[(X_val, y_val)], sample_weight_eval_set=[w_val], - early_stopping_rounds=1, - eval_metric="logloss", ) cls_df_train_with_eval_weight = spark.createDataFrame( From 85d09245f6af50c0a7c69d1408b166461f09b8d6 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 17 Jan 2024 05:35:35 +0800 Subject: [PATCH 099/109] Fix error handling in the event loop. (#9990) --- src/collective/loop.cc | 170 ++++++++++++++++++++++++----------------- src/collective/loop.h | 30 +++++--- 2 files changed, 120 insertions(+), 80 deletions(-) diff --git a/src/collective/loop.cc b/src/collective/loop.cc index 5cfb0034d..b51749fcd 100644 --- a/src/collective/loop.cc +++ b/src/collective/loop.cc @@ -1,11 +1,19 @@ /** - * Copyright 2023, XGBoost Contributors + * Copyright 2023-2024, XGBoost Contributors */ #include "loop.h" -#include // for queue +#include // for size_t +#include // for int32_t +#include // for exception, current_exception, rethrow_exception +#include // for lock_guard, unique_lock +#include // for queue +#include // for string +#include // for thread +#include // for move #include "rabit/internal/socket.h" // for PollHelper +#include "xgboost/collective/result.h" // for Fail, Success #include "xgboost/collective/socket.h" // for FailWithCode #include "xgboost/logging.h" // for CHECK @@ -109,62 +117,94 @@ Result Loop::EmptyQueue(std::queue* p_queue) const { } void Loop::Process() { - // consumer - while (true) { - std::unique_lock lock{mu_}; - cv_.wait(lock, [this] { return !this->queue_.empty() || stop_; }); - if (stop_) { - break; - } + auto set_rc = [this](Result&& rc) { + std::lock_guard lock{rc_lock_}; + rc_ = std::forward(rc); + }; + + // This loop cannot exit unless `stop_` is set to true. There must always be a thread to + // answer the blocking call even if there are errors, otherwise the blocking will wait + // forever. + while (true) { + try { + std::unique_lock lock{mu_}; + cv_.wait(lock, [this] { return !this->queue_.empty() || stop_; }); + if (stop_) { + break; // only point where this loop can exit. + } + + // Move the global queue into a local variable to unblock it. + std::queue qcopy; + + bool is_blocking = false; + while (!queue_.empty()) { + auto op = queue_.front(); + queue_.pop(); + if (op.code == Op::kBlock) { + is_blocking = true; + // Block must be the last op in the current batch since no further submit can be + // issued until the blocking call is finished. + CHECK(queue_.empty()); + } else { + qcopy.push(op); + } + } - auto unlock_notify = [&](bool is_blocking, bool stop) { if (!is_blocking) { - std::lock_guard guard{mu_}; - stop_ = stop; - } else { - stop_ = stop; + // Unblock, we can write to the global queue again. lock.unlock(); } - cv_.notify_one(); - }; - // move the queue - std::queue qcopy; - bool is_blocking = false; - while (!queue_.empty()) { - auto op = queue_.front(); - queue_.pop(); - if (op.code == Op::kBlock) { - is_blocking = true; - } else { - qcopy.push(op); + // Clear the local queue, this is blocking the current worker thread (but not the + // client thread), wait until all operations are finished. + auto rc = this->EmptyQueue(&qcopy); + + if (is_blocking) { + // The unlock is delayed if this is a blocking call + lock.unlock(); } - } - // unblock the queue - if (!is_blocking) { - lock.unlock(); - } - // clear the queue - auto rc = this->EmptyQueue(&qcopy); - // Handle error - if (!rc.OK()) { - unlock_notify(is_blocking, true); - std::lock_guard guard{rc_lock_}; - this->rc_ = std::move(rc); - return; - } - CHECK(qcopy.empty()); - unlock_notify(is_blocking, false); + // Notify the client thread who called block after all error conditions are set. + auto notify_if_block = [&] { + if (is_blocking) { + std::unique_lock lock{mu_}; + block_done_ = true; + lock.unlock(); + block_cv_.notify_one(); + } + }; + + // Handle error + if (!rc.OK()) { + set_rc(std::move(rc)); + } else { + CHECK(qcopy.empty()); + } + + notify_if_block(); + } catch (std::exception const& e) { + curr_exce_ = std::current_exception(); + set_rc(Fail("Exception inside the event loop:" + std::string{e.what()})); + } catch (...) { + curr_exce_ = std::current_exception(); + set_rc(Fail("Unknown exception inside the event loop.")); + } } } Result Loop::Stop() { + // Finish all remaining tasks + CHECK_EQ(this->Block().OK(), this->rc_.OK()); + + // Notify the loop to stop std::unique_lock lock{mu_}; stop_ = true; lock.unlock(); + this->cv_.notify_one(); - CHECK_EQ(this->Block().OK(), this->rc_.OK()); + if (this->worker_.joinable()) { + this->worker_.join(); + } if (curr_exce_) { std::rethrow_exception(curr_exce_); @@ -175,17 +215,29 @@ Result Loop::Stop() { [[nodiscard]] Result Loop::Block() { { + // Check whether the last op was successful, stop if not. std::lock_guard guard{rc_lock_}; if (!rc_.OK()) { - return std::move(rc_); + stop_ = true; } } - this->Submit(Op{Op::kBlock}); - { - std::unique_lock lock{mu_}; - cv_.wait(lock, [this] { return (this->queue_.empty()) || stop_; }); + + if (!this->worker_.joinable()) { + std::lock_guard guard{rc_lock_}; + return Fail("Worker has stopped.", std::move(rc_)); } + + this->Submit(Op{Op::kBlock}); + { + // Wait for the block call to finish. + std::unique_lock lock{mu_}; + block_cv_.wait(lock, [this] { return block_done_ || stop_; }); + block_done_ = false; + } + + { + // Transfer the rc. std::lock_guard lock{rc_lock_}; return std::move(rc_); } @@ -193,26 +245,6 @@ Result Loop::Stop() { Loop::Loop(std::chrono::seconds timeout) : timeout_{timeout} { timer_.Init(__func__); - worker_ = std::thread{[this] { - try { - this->Process(); - } catch (std::exception const& e) { - std::lock_guard guard{mu_}; - if (!curr_exce_) { - curr_exce_ = std::current_exception(); - rc_ = Fail("Exception was thrown"); - } - stop_ = true; - cv_.notify_all(); - } catch (...) { - std::lock_guard guard{mu_}; - if (!curr_exce_) { - curr_exce_ = std::current_exception(); - rc_ = Fail("Exception was thrown"); - } - stop_ = true; - cv_.notify_all(); - } - }}; + worker_ = std::thread{[this] { this->Process(); }}; } } // namespace xgboost::collective diff --git a/src/collective/loop.h b/src/collective/loop.h index 0c1fdcbfe..4839abfd3 100644 --- a/src/collective/loop.h +++ b/src/collective/loop.h @@ -1,5 +1,5 @@ /** - * Copyright 2023, XGBoost Contributors + * Copyright 2023-2024, XGBoost Contributors */ #pragma once #include // for seconds @@ -10,7 +10,6 @@ #include // for unique_lock, mutex #include // for queue #include // for thread -#include // for move #include "../common/timer.h" // for Monitor #include "xgboost/collective/result.h" // for Result @@ -37,10 +36,15 @@ class Loop { }; private: - std::thread worker_; - std::condition_variable cv_; - std::mutex mu_; - std::queue queue_; + std::thread worker_; // thread worker to execute the tasks + + std::condition_variable cv_; // CV used to notify a new submit call + std::condition_variable block_cv_; // CV used to notify the blocking call + bool block_done_{false}; // Flag to indicate whether the blocking call has finished. + + std::queue queue_; // event queue + std::mutex mu_; // mutex to protect the queue, cv, and block_done + std::chrono::seconds timeout_; Result rc_; @@ -51,29 +55,33 @@ class Loop { common::Monitor mutable timer_; Result EmptyQueue(std::queue* p_queue) const; + // The cunsumer function that runs inside a worker thread. void Process(); public: + /** + * @brief Stop the worker thread. + */ Result Stop(); void Submit(Op op) { - // producer std::unique_lock lock{mu_}; queue_.push(op); lock.unlock(); cv_.notify_one(); } + /** + * @brief Block the event loop until all ops are finished. In the case of failure, this + * loop should be not be used for new operations. + */ [[nodiscard]] Result Block(); explicit Loop(std::chrono::seconds timeout); ~Loop() noexcept(false) { + // The worker will be joined in the stop function. this->Stop(); - - if (worker_.joinable()) { - worker_.join(); - } } }; } // namespace xgboost::collective From cacb4b1fdd0ac71ee164dc2c4f103cea8d515b72 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 17 Jan 2024 13:18:44 +0800 Subject: [PATCH 100/109] Fix gain calculation in multi-target tree. (#9978) --- include/xgboost/tree_model.h | 4 +- src/tree/hist/evaluate_splits.h | 3 + src/tree/multi_target_tree_model.cc | 3 +- src/tree/updater_quantile_hist.cc | 3 + tests/cpp/tree/test_quantile_hist.cc | 3 +- tests/cpp/tree/test_tree_stat.cc | 181 ++++++++++++++++++--------- 6 files changed, 132 insertions(+), 65 deletions(-) diff --git a/include/xgboost/tree_model.h b/include/xgboost/tree_model.h index 393dda59c..4c475da2e 100644 --- a/include/xgboost/tree_model.h +++ b/include/xgboost/tree_model.h @@ -398,8 +398,8 @@ class RegTree : public Model { if (!func(nidx)) { return; } - auto left = self[nidx].LeftChild(); - auto right = self[nidx].RightChild(); + auto left = self.LeftChild(nidx); + auto right = self.RightChild(nidx); if (left != RegTree::kInvalidNodeId) { nodes.push(left); } diff --git a/src/tree/hist/evaluate_splits.h b/src/tree/hist/evaluate_splits.h index 680c50398..bc534d351 100644 --- a/src/tree/hist/evaluate_splits.h +++ b/src/tree/hist/evaluate_splits.h @@ -730,6 +730,9 @@ class HistMultiEvaluator { std::size_t n_nodes = p_tree->Size(); gain_.resize(n_nodes); + // Re-calculate weight without learning rate. + CalcWeight(*param_, left_sum, left_weight); + CalcWeight(*param_, right_sum, right_weight); gain_[left_child] = CalcGainGivenWeight(*param_, left_sum, left_weight); gain_[right_child] = CalcGainGivenWeight(*param_, right_sum, right_weight); diff --git a/src/tree/multi_target_tree_model.cc b/src/tree/multi_target_tree_model.cc index bccc1967e..11ee1f6dd 100644 --- a/src/tree/multi_target_tree_model.cc +++ b/src/tree/multi_target_tree_model.cc @@ -195,8 +195,9 @@ void MultiTargetTree::Expand(bst_node_t nidx, bst_feature_t split_idx, float spl split_index_.resize(n); split_index_[nidx] = split_idx; - split_conds_.resize(n); + split_conds_.resize(n, std::numeric_limits::quiet_NaN()); split_conds_[nidx] = split_cond; + default_left_.resize(n); default_left_[nidx] = static_cast(default_left); diff --git a/src/tree/updater_quantile_hist.cc b/src/tree/updater_quantile_hist.cc index 7731f505e..c2aaedafa 100644 --- a/src/tree/updater_quantile_hist.cc +++ b/src/tree/updater_quantile_hist.cc @@ -149,6 +149,9 @@ class MultiTargetHistBuilder { } void InitData(DMatrix *p_fmat, RegTree const *p_tree) { + if (collective::IsDistributed()) { + LOG(FATAL) << "Distributed training for vector-leaf is not yet supported."; + } monitor_->Start(__func__); p_last_fmat_ = p_fmat; diff --git a/tests/cpp/tree/test_quantile_hist.cc b/tests/cpp/tree/test_quantile_hist.cc index 6327703ed..cf806536a 100644 --- a/tests/cpp/tree/test_quantile_hist.cc +++ b/tests/cpp/tree/test_quantile_hist.cc @@ -253,6 +253,5 @@ void TestColumnSplit(bst_target_t n_targets) { TEST(QuantileHist, ColumnSplit) { TestColumnSplit(1); } -TEST(QuantileHist, ColumnSplitMultiTarget) { TestColumnSplit(3); } - +TEST(QuantileHist, DISABLED_ColumnSplitMultiTarget) { TestColumnSplit(3); } } // namespace xgboost::tree diff --git a/tests/cpp/tree/test_tree_stat.cc b/tests/cpp/tree/test_tree_stat.cc index d112efa9d..5f0646f22 100644 --- a/tests/cpp/tree/test_tree_stat.cc +++ b/tests/cpp/tree/test_tree_stat.cc @@ -1,18 +1,21 @@ /** - * Copyright 2020-2023 by XGBoost Contributors + * Copyright 2020-2024, XGBoost Contributors */ #include -#include // for Context -#include // for ObjInfo -#include -#include +#include // for Context +#include // for ObjInfo +#include // for RegTree +#include // for TreeUpdater -#include // for unique_ptr +#include // for unique_ptr #include "../../../src/tree/param.h" // for TrainParam #include "../helpers.h" namespace xgboost { +/** + * @brief Test the tree statistic (like sum Hessian) is correct. + */ class UpdaterTreeStatTest : public ::testing::Test { protected: std::shared_ptr p_dmat_; @@ -28,13 +31,12 @@ class UpdaterTreeStatTest : public ::testing::Test { gpairs_.Data()->Copy(g); } - void RunTest(std::string updater) { + void RunTest(Context const* ctx, std::string updater) { tree::TrainParam param; ObjInfo task{ObjInfo::kRegression}; param.Init(Args{}); - Context ctx(updater == "grow_gpu_hist" ? MakeCUDACtx(0) : MakeCUDACtx(DeviceOrd::CPUOrdinal())); - auto up = std::unique_ptr{TreeUpdater::Create(updater, &ctx, &task)}; + auto up = std::unique_ptr{TreeUpdater::Create(updater, ctx, &task)}; up->Configure(Args{}); RegTree tree{1u, kCols}; std::vector> position(1); @@ -51,76 +53,135 @@ class UpdaterTreeStatTest : public ::testing::Test { }; #if defined(XGBOOST_USE_CUDA) -TEST_F(UpdaterTreeStatTest, GpuHist) { this->RunTest("grow_gpu_hist"); } +TEST_F(UpdaterTreeStatTest, GpuHist) { + auto ctx = MakeCUDACtx(0); + this->RunTest(&ctx, "grow_gpu_hist"); +} + +TEST_F(UpdaterTreeStatTest, GpuApprox) { + auto ctx = MakeCUDACtx(0); + this->RunTest(&ctx, "grow_gpu_approx"); +} #endif // defined(XGBOOST_USE_CUDA) -TEST_F(UpdaterTreeStatTest, Hist) { this->RunTest("grow_quantile_histmaker"); } +TEST_F(UpdaterTreeStatTest, Hist) { + Context ctx; + this->RunTest(&ctx, "grow_quantile_histmaker"); +} -TEST_F(UpdaterTreeStatTest, Exact) { this->RunTest("grow_colmaker"); } +TEST_F(UpdaterTreeStatTest, Exact) { + Context ctx; + this->RunTest(&ctx, "grow_colmaker"); +} -TEST_F(UpdaterTreeStatTest, Approx) { this->RunTest("grow_histmaker"); } +TEST_F(UpdaterTreeStatTest, Approx) { + Context ctx; + this->RunTest(&ctx, "grow_histmaker"); +} -class UpdaterEtaTest : public ::testing::Test { +/** + * @brief Test changing learning rate doesn't change internal splits. + */ +class TestSplitWithEta : public ::testing::Test { protected: - std::shared_ptr p_dmat_; - linalg::Matrix gpairs_; - size_t constexpr static kRows = 10; - size_t constexpr static kCols = 10; - size_t constexpr static kClasses = 10; + void Run(Context const* ctx, bst_target_t n_targets, std::string name) { + auto Xy = RandomDataGenerator{512, 64, 0.2}.Targets(n_targets).GenerateDMatrix(true); - void SetUp() override { - p_dmat_ = RandomDataGenerator(kRows, kCols, .5f).GenerateDMatrix(true, false, kClasses); - auto g = GenerateRandomGradients(kRows); - gpairs_.Reshape(kRows, 1); - gpairs_.Data()->Copy(g); - } + auto gen_tree = [&](float eta) { + auto tree = + std::make_unique(n_targets, static_cast(Xy->Info().num_col_)); + std::vector trees{tree.get()}; + ObjInfo task{ObjInfo::kRegression}; + std::unique_ptr updater{TreeUpdater::Create(name, ctx, &task)}; + updater->Configure({}); - void RunTest(std::string updater) { - ObjInfo task{ObjInfo::kClassification}; + auto grad = GenerateRandomGradients(ctx, Xy->Info().num_row_, n_targets); + CHECK_EQ(grad.Shape(1), n_targets); + tree::TrainParam param; + param.Init(Args{{"learning_rate", std::to_string(eta)}}); + HostDeviceVector position; - Context ctx(updater == "grow_gpu_hist" ? MakeCUDACtx(0) : MakeCUDACtx(DeviceOrd::CPUOrdinal())); - - float eta = 0.4; - auto up_0 = std::unique_ptr{TreeUpdater::Create(updater, &ctx, &task)}; - up_0->Configure(Args{}); - tree::TrainParam param0; - param0.Init(Args{{"eta", std::to_string(eta)}}); - - auto up_1 = std::unique_ptr{TreeUpdater::Create(updater, &ctx, &task)}; - up_1->Configure(Args{{"eta", "1.0"}}); - tree::TrainParam param1; - param1.Init(Args{{"eta", "1.0"}}); - - for (size_t iter = 0; iter < 4; ++iter) { - RegTree tree_0{1u, kCols}; - { - std::vector> position(1); - up_0->Update(¶m0, &gpairs_, p_dmat_.get(), position, {&tree_0}); + updater->Update(¶m, &grad, Xy.get(), common::Span{&position, 1}, trees); + CHECK_EQ(tree->NumTargets(), n_targets); + if (n_targets > 1) { + CHECK(tree->IsMultiTarget()); } + return tree; + }; - RegTree tree_1{1u, kCols}; - { - std::vector> position(1); - up_1->Update(¶m1, &gpairs_, p_dmat_.get(), position, {&tree_1}); - } - tree_0.WalkTree([&](bst_node_t nidx) { - if (tree_0[nidx].IsLeaf()) { - EXPECT_NEAR(tree_1[nidx].LeafValue() * eta, tree_0[nidx].LeafValue(), kRtEps); + auto eta_ratio = 8.0f; + auto p_tree0 = gen_tree(0.1f); + auto p_tree1 = gen_tree(0.1f * eta_ratio); + // Just to make sure we are not testing a stump. + CHECK_GE(p_tree0->NumExtraNodes(), 32); + + bst_node_t n_nodes{0}; + p_tree0->WalkTree([&](bst_node_t nidx) { + if (p_tree0->IsLeaf(nidx)) { + CHECK(p_tree1->IsLeaf(nidx)); + if (p_tree0->IsMultiTarget()) { + CHECK(p_tree1->IsMultiTarget()); + auto leaf_0 = p_tree0->GetMultiTargetTree()->LeafValue(nidx); + auto leaf_1 = p_tree1->GetMultiTargetTree()->LeafValue(nidx); + CHECK_EQ(leaf_0.Size(), leaf_1.Size()); + for (std::size_t i = 0; i < leaf_0.Size(); ++i) { + CHECK_EQ(leaf_0(i) * eta_ratio, leaf_1(i)); + } + CHECK(std::isnan(p_tree0->SplitCond(nidx))); + CHECK(std::isnan(p_tree1->SplitCond(nidx))); + } else { + // NON-mt tree reuses split cond for leaf value. + auto leaf_0 = p_tree0->SplitCond(nidx); + auto leaf_1 = p_tree1->SplitCond(nidx); + CHECK_EQ(leaf_0 * eta_ratio, leaf_1); } - return true; - }); - } + } else { + CHECK(!p_tree1->IsLeaf(nidx)); + CHECK_EQ(p_tree0->SplitCond(nidx), p_tree1->SplitCond(nidx)); + } + n_nodes++; + return true; + }); + ASSERT_EQ(n_nodes, p_tree0->NumExtraNodes() + 1); } }; -TEST_F(UpdaterEtaTest, Hist) { this->RunTest("grow_quantile_histmaker"); } +TEST_F(TestSplitWithEta, HistMulti) { + Context ctx; + bst_target_t n_targets{3}; + this->Run(&ctx, n_targets, "grow_quantile_histmaker"); +} -TEST_F(UpdaterEtaTest, Exact) { this->RunTest("grow_colmaker"); } +TEST_F(TestSplitWithEta, Hist) { + Context ctx; + bst_target_t n_targets{1}; + this->Run(&ctx, n_targets, "grow_quantile_histmaker"); +} -TEST_F(UpdaterEtaTest, Approx) { this->RunTest("grow_histmaker"); } +TEST_F(TestSplitWithEta, Approx) { + Context ctx; + bst_target_t n_targets{1}; + this->Run(&ctx, n_targets, "grow_histmaker"); +} + +TEST_F(TestSplitWithEta, Exact) { + Context ctx; + bst_target_t n_targets{1}; + this->Run(&ctx, n_targets, "grow_colmaker"); +} #if defined(XGBOOST_USE_CUDA) -TEST_F(UpdaterEtaTest, GpuHist) { this->RunTest("grow_gpu_hist"); } +TEST_F(TestSplitWithEta, GpuHist) { + auto ctx = MakeCUDACtx(0); + bst_target_t n_targets{1}; + this->Run(&ctx, n_targets, "grow_gpu_hist"); +} + +TEST_F(TestSplitWithEta, GpuApprox) { + auto ctx = MakeCUDACtx(0); + bst_target_t n_targets{1}; + this->Run(&ctx, n_targets, "grow_gpu_approx"); +} #endif // defined(XGBOOST_USE_CUDA) class TestMinSplitLoss : public ::testing::Test { From d07e8b503e46b6ab6872f2c03119d68ff0753925 Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 17 Jan 2024 13:19:08 +0800 Subject: [PATCH 101/109] Fix quantile regression demo. (#9991) --- demo/guide-python/quantile_regression.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/guide-python/quantile_regression.py b/demo/guide-python/quantile_regression.py index 5d186714c..a9e4532ba 100644 --- a/demo/guide-python/quantile_regression.py +++ b/demo/guide-python/quantile_regression.py @@ -46,10 +46,11 @@ def quantile_loss(args: argparse.Namespace) -> None: X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=rng) # We will be using the `hist` tree method, quantile DMatrix can be used to preserve - # memory. + # memory (which has nothing to do with quantile regression itself, see its document + # for details). # Do not use the `exact` tree method for quantile regression, otherwise the # performance might drop. - Xy = xgb.QuantileDMatrix(X, y) + Xy = xgb.QuantileDMatrix(X_train, y_train) # use Xy as a reference Xy_test = xgb.QuantileDMatrix(X_test, y_test, ref=Xy) From bde20dd897aa20683b7fdc83836d6973af1b23bf Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 17 Jan 2024 13:19:34 +0800 Subject: [PATCH 102/109] Remove benchmark scripts. (#9992) --- tests/README.md | 4 +- tests/benchmark/benchmark_linear.py | 69 ----------------------- tests/benchmark/benchmark_tree.py | 86 ---------------------------- tests/benchmark/generate_libsvm.py | 87 ----------------------------- 4 files changed, 1 insertion(+), 245 deletions(-) delete mode 100644 tests/benchmark/benchmark_linear.py delete mode 100644 tests/benchmark/benchmark_tree.py delete mode 100644 tests/benchmark/generate_libsvm.py diff --git a/tests/README.md b/tests/README.md index 7d54b78cb..a118e7918 100644 --- a/tests/README.md +++ b/tests/README.md @@ -10,9 +10,7 @@ facilities. dependencies for tests, see conda files in `ci_build`. * python-gpu: Similar to python tests, but for GPU. * travis: CI facilities for Travis. - * distributed: Test for distributed system. - * benchmark: Legacy benchmark code. There are a number of benchmark projects for - XGBoost with much better configurations. + * test_distributed: Test for distributed systems including spark and dask. # Others * pytest.ini: Describes the `pytest` marker for python tests, some markers are generated diff --git a/tests/benchmark/benchmark_linear.py b/tests/benchmark/benchmark_linear.py deleted file mode 100644 index cb5141714..000000000 --- a/tests/benchmark/benchmark_linear.py +++ /dev/null @@ -1,69 +0,0 @@ -#pylint: skip-file -import argparse -import xgboost as xgb -import numpy as np -from sklearn.datasets import make_classification -from sklearn.model_selection import train_test_split -import time -import ast - -rng = np.random.RandomState(1994) - - -def run_benchmark(args): - - try: - dtest = xgb.DMatrix('dtest.dm') - dtrain = xgb.DMatrix('dtrain.dm') - - if not (dtest.num_col() == args.columns \ - and dtrain.num_col() == args.columns): - raise ValueError("Wrong cols") - if not (dtest.num_row() == args.rows * args.test_size \ - and dtrain.num_row() == args.rows * (1-args.test_size)): - raise ValueError("Wrong rows") - except: - - print("Generating dataset: {} rows * {} columns".format(args.rows, args.columns)) - print("{}/{} test/train split".format(args.test_size, 1.0 - args.test_size)) - tmp = time.time() - X, y = make_classification(args.rows, n_features=args.columns, n_redundant=0, n_informative=args.columns, n_repeated=0, random_state=7) - if args.sparsity < 1.0: - X = np.array([[np.nan if rng.uniform(0, 1) < args.sparsity else x for x in x_row] for x_row in X]) - - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=args.test_size, random_state=7) - print ("Generate Time: %s seconds" % (str(time.time() - tmp))) - tmp = time.time() - print ("DMatrix Start") - dtrain = xgb.DMatrix(X_train, y_train) - dtest = xgb.DMatrix(X_test, y_test, nthread=-1) - print ("DMatrix Time: %s seconds" % (str(time.time() - tmp))) - - dtest.save_binary('dtest.dm') - dtrain.save_binary('dtrain.dm') - - param = {'objective': 'binary:logistic','booster':'gblinear'} - if args.params != '': - param.update(ast.literal_eval(args.params)) - - param['updater'] = args.updater - print("Training with '%s'" % param['updater']) - tmp = time.time() - xgb.train(param, dtrain, args.iterations, evals=[(dtrain,"train")], early_stopping_rounds = args.columns) - print ("Train Time: %s seconds" % (str(time.time() - tmp))) - -parser = argparse.ArgumentParser() -parser.add_argument('--updater', default='coord_descent') -parser.add_argument('--sparsity', type=float, default=0.0) -parser.add_argument('--lambda', type=float, default=1.0) -parser.add_argument('--tol', type=float, default=1e-5) -parser.add_argument('--alpha', type=float, default=1.0) -parser.add_argument('--rows', type=int, default=1000000) -parser.add_argument('--iterations', type=int, default=10000) -parser.add_argument('--columns', type=int, default=50) -parser.add_argument('--test_size', type=float, default=0.25) -parser.add_argument('--standardise', type=bool, default=False) -parser.add_argument('--params', default='', help='Provide additional parameters as a Python dict string, e.g. --params \"{\'max_depth\':2}\"') -args = parser.parse_args() - -run_benchmark(args) diff --git a/tests/benchmark/benchmark_tree.py b/tests/benchmark/benchmark_tree.py deleted file mode 100644 index 380e03463..000000000 --- a/tests/benchmark/benchmark_tree.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Run benchmark on the tree booster.""" - -import argparse -import ast -import time - -import numpy as np -import xgboost as xgb - -RNG = np.random.RandomState(1994) - - -def run_benchmark(args): - """Runs the benchmark.""" - try: - dtest = xgb.DMatrix('dtest.dm') - dtrain = xgb.DMatrix('dtrain.dm') - - if not (dtest.num_col() == args.columns - and dtrain.num_col() == args.columns): - raise ValueError("Wrong cols") - if not (dtest.num_row() == args.rows * args.test_size - and dtrain.num_row() == args.rows * (1 - args.test_size)): - raise ValueError("Wrong rows") - except: - print("Generating dataset: {} rows * {} columns".format(args.rows, args.columns)) - print("{}/{} test/train split".format(args.test_size, 1.0 - args.test_size)) - tmp = time.time() - X = RNG.rand(args.rows, args.columns) - y = RNG.randint(0, 2, args.rows) - if 0.0 < args.sparsity < 1.0: - X = np.array([[np.nan if RNG.uniform(0, 1) < args.sparsity else x for x in x_row] - for x_row in X]) - - train_rows = int(args.rows * (1.0 - args.test_size)) - test_rows = int(args.rows * args.test_size) - X_train = X[:train_rows, :] - X_test = X[-test_rows:, :] - y_train = y[:train_rows] - y_test = y[-test_rows:] - print("Generate Time: %s seconds" % (str(time.time() - tmp))) - del X, y - - tmp = time.time() - print("DMatrix Start") - dtrain = xgb.DMatrix(X_train, y_train, nthread=-1) - dtest = xgb.DMatrix(X_test, y_test, nthread=-1) - print("DMatrix Time: %s seconds" % (str(time.time() - tmp))) - del X_train, y_train, X_test, y_test - - dtest.save_binary('dtest.dm') - dtrain.save_binary('dtrain.dm') - - param = {'objective': 'binary:logistic'} - if args.params != '': - param.update(ast.literal_eval(args.params)) - - param['tree_method'] = args.tree_method - print("Training with '%s'" % param['tree_method']) - tmp = time.time() - xgb.train(param, dtrain, args.iterations, evals=[(dtest, "test")]) - print("Train Time: %s seconds" % (str(time.time() - tmp))) - - -def main(): - """The main function. - - Defines and parses command line arguments and calls the benchmark. - """ - parser = argparse.ArgumentParser() - parser.add_argument('--tree_method', default='gpu_hist') - parser.add_argument('--sparsity', type=float, default=0.0) - parser.add_argument('--rows', type=int, default=1000000) - parser.add_argument('--columns', type=int, default=50) - parser.add_argument('--iterations', type=int, default=500) - parser.add_argument('--test_size', type=float, default=0.25) - parser.add_argument('--params', default='', - help='Provide additional parameters as a Python dict string, e.g. --params ' - '\"{\'max_depth\':2}\"') - args = parser.parse_args() - - run_benchmark(args) - - -if __name__ == '__main__': - main() diff --git a/tests/benchmark/generate_libsvm.py b/tests/benchmark/generate_libsvm.py deleted file mode 100644 index be152df39..000000000 --- a/tests/benchmark/generate_libsvm.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Generate synthetic data in LIBSVM format.""" - -import argparse -import io -import time - -import numpy as np -from sklearn.datasets import make_classification -from sklearn.model_selection import train_test_split - -RNG = np.random.RandomState(2019) - - -def generate_data(args): - """Generates the data.""" - print("Generating dataset: {} rows * {} columns".format(args.rows, args.columns)) - print("Sparsity {}".format(args.sparsity)) - print("{}/{} train/test split".format(1.0 - args.test_size, args.test_size)) - - tmp = time.time() - n_informative = args.columns * 7 // 10 - n_redundant = args.columns // 10 - n_repeated = args.columns // 10 - print("n_informative: {}, n_redundant: {}, n_repeated: {}".format(n_informative, n_redundant, - n_repeated)) - x, y = make_classification(n_samples=args.rows, n_features=args.columns, - n_informative=n_informative, n_redundant=n_redundant, - n_repeated=n_repeated, shuffle=False, random_state=RNG) - print("Generate Time: {} seconds".format(time.time() - tmp)) - - tmp = time.time() - x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=args.test_size, - random_state=RNG, shuffle=False) - print("Train/Test Split Time: {} seconds".format(time.time() - tmp)) - - tmp = time.time() - write_file('train.libsvm', x_train, y_train, args.sparsity) - print("Write Train Time: {} seconds".format(time.time() - tmp)) - - tmp = time.time() - write_file('test.libsvm', x_test, y_test, args.sparsity) - print("Write Test Time: {} seconds".format(time.time() - tmp)) - - -def write_file(filename, x_data, y_data, sparsity): - with open(filename, 'w') as f: - for x, y in zip(x_data, y_data): - write_line(f, x, y, sparsity) - - -def write_line(f, x, y, sparsity): - with io.StringIO() as line: - line.write(str(y)) - for i, col in enumerate(x): - if 0.0 < sparsity < 1.0: - if RNG.uniform(0, 1) > sparsity: - write_feature(line, i, col) - else: - write_feature(line, i, col) - line.write('\n') - f.write(line.getvalue()) - - -def write_feature(line, index, feature): - line.write(' ') - line.write(str(index)) - line.write(':') - line.write(str(feature)) - - -def main(): - """The main function. - - Defines and parses command line arguments and calls the generator. - """ - parser = argparse.ArgumentParser() - parser.add_argument('--rows', type=int, default=1000000) - parser.add_argument('--columns', type=int, default=50) - parser.add_argument('--sparsity', type=float, default=0.0) - parser.add_argument('--test_size', type=float, default=0.01) - args = parser.parse_args() - - generate_data(args) - - -if __name__ == '__main__': - main() From 2c8fa8b8b96c4c8b62a715f1334577b10512df71 Mon Sep 17 00:00:00 2001 From: Philip Hyunsu Cho Date: Thu, 18 Jan 2024 08:09:53 -0800 Subject: [PATCH 103/109] [CI] Skip MSVC when building R package (#9995) * [CI] Skip MSVC when building R package * [CI] Stop building binary tarball for Windows * Remove unused script --- .github/workflows/r_tests.yml | 1 - dev/release-artifacts.py | 2 +- tests/buildkite/build-rpkg-win64-gpu.ps1 | 21 ----------- tests/buildkite/pipeline-win64.yml | 5 --- tests/ci_build/build_r_pkg_with_cuda_win64.sh | 36 ------------------- 5 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 tests/buildkite/build-rpkg-win64-gpu.ps1 delete mode 100644 tests/ci_build/build_r_pkg_with_cuda_win64.sh diff --git a/.github/workflows/r_tests.yml b/.github/workflows/r_tests.yml index 917245ec6..d004ab15c 100644 --- a/.github/workflows/r_tests.yml +++ b/.github/workflows/r_tests.yml @@ -54,7 +54,6 @@ jobs: matrix: config: - {os: windows-latest, r: 'release', compiler: 'mingw', build: 'autotools'} - - {os: windows-latest, r: '4.3.0', compiler: 'msvc', build: 'cmake'} env: R_REMOTES_NO_ERRORS_FROM_WARNINGS: true RSPM: ${{ matrix.config.rspm }} diff --git a/dev/release-artifacts.py b/dev/release-artifacts.py index 429fac078..d5f28f6fc 100644 --- a/dev/release-artifacts.py +++ b/dev/release-artifacts.py @@ -153,7 +153,7 @@ Following steps should be done manually: def download_r_packages( release: str, branch: str, rc: str, commit: str, outdir: str ) -> Tuple[Dict[str, str], List[str]]: - platforms = ["win64", "linux"] + platforms = ["linux"] dirname = os.path.join(outdir, "r-packages") if not os.path.exists(dirname): os.mkdir(dirname) diff --git a/tests/buildkite/build-rpkg-win64-gpu.ps1 b/tests/buildkite/build-rpkg-win64-gpu.ps1 deleted file mode 100644 index a6947c270..000000000 --- a/tests/buildkite/build-rpkg-win64-gpu.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -$ErrorActionPreference = "Stop" - -. tests/buildkite/conftest.ps1 - -Write-Host "--- Build XGBoost R package with CUDA" - -nvcc --version -$arch_flag = "-DGPU_COMPUTE_VER=75" - -bash tests/ci_build/build_r_pkg_with_cuda_win64.sh $Env:BUILDKITE_COMMIT -if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - -if ( $is_release_branch -eq 1 ) { - Write-Host "--- Upload R tarball" - Get-ChildItem . -Filter xgboost_r_gpu_win64_*.tar.gz | - Foreach-Object { - & aws s3 cp $_ s3://xgboost-nightly-builds/$Env:BUILDKITE_BRANCH/ ` - --acl public-read --no-progress - if ($LASTEXITCODE -ne 0) { throw "Last command failed" } - } -} diff --git a/tests/buildkite/pipeline-win64.yml b/tests/buildkite/pipeline-win64.yml index d4491148e..83a61981e 100644 --- a/tests/buildkite/pipeline-win64.yml +++ b/tests/buildkite/pipeline-win64.yml @@ -13,11 +13,6 @@ steps: key: build-win64-gpu agents: queue: windows-cpu - - label: ":windows: Build XGBoost R package for Windows with CUDA" - command: "tests/buildkite/build-rpkg-win64-gpu.ps1" - key: build-rpkg-win64-gpu - agents: - queue: windows-cpu - wait diff --git a/tests/ci_build/build_r_pkg_with_cuda_win64.sh b/tests/ci_build/build_r_pkg_with_cuda_win64.sh deleted file mode 100644 index 580358883..000000000 --- a/tests/ci_build/build_r_pkg_with_cuda_win64.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash -set -e -set -x - -if [ "$#" -ne 1 ] -then - echo "Build the R package tarball with CUDA code. Usage: $0 [commit hash]" - exit 1 -fi - -commit_hash="$1" -# Clear all positional args -set -- - -source activate -python tests/ci_build/test_r_package.py --task=pack -mv xgboost/ xgboost_rpack/ - -mkdir build -cd build -cmake .. -G"Visual Studio 17 2022" -A x64 -DUSE_CUDA=ON -DR_LIB=ON -DLIBR_HOME="c:\\Program Files\\R\\R-4.3.2" -DCMAKE_PREFIX_PATH="C:\\rtools43\\x86_64-w64-mingw32.static.posix\\bin" -cmake --build . --config Release --parallel -cd .. - -# This super wacky hack is found in cmake/RPackageInstall.cmake.in and -# cmake/RPackageInstallTargetSetup.cmake. This hack lets us bypass the normal build process of R -# and have R use xgboost.dll that we've already built. -rm -v xgboost_rpack/configure -rm -rfv xgboost_rpack/src -mkdir -p xgboost_rpack/src -cp -v lib/xgboost.dll xgboost_rpack/src/ -echo 'all:' > xgboost_rpack/src/Makefile -echo 'all:' > xgboost_rpack/src/Makefile.win -mv xgboost_rpack/ xgboost/ -/c/Rtools43/usr/bin/tar -cvf xgboost_r_gpu_win64_${commit_hash}.tar xgboost/ -/c/Rtools43/usr/bin/gzip -9c xgboost_r_gpu_win64_${commit_hash}.tar > xgboost_r_gpu_win64_${commit_hash}.tar.gz From 60b9d2eeb9f88f8d028a1f2fba37abbb3d0f47cb Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 20 Jan 2024 17:53:18 +0100 Subject: [PATCH 104/109] [R] Avoid memory copies in `predict` (#9902) --- R-package/R/xgb.Booster.R | 48 ++++++++++++++++++++++---------------- R-package/src/init.c | 4 ++++ R-package/src/xgboost_R.cc | 10 ++++++++ R-package/src/xgboost_R.h | 16 +++++++++++++ 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index cee7e9fc5..5562c22f3 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -343,24 +343,24 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA ) names(predts) <- c("shape", "results") shape <- predts$shape - ret <- predts$results + arr <- predts$results - n_ret <- length(ret) + n_ret <- length(arr) n_row <- nrow(newdata) if (n_row != shape[1]) { stop("Incorrect predict shape.") } - arr <- array(data = ret, dim = rev(shape)) + .Call(XGSetArrayDimInplace_R, arr, rev(shape)) cnames <- if (!is.null(colnames(newdata))) c(colnames(newdata), "BIAS") else NULL n_groups <- shape[2] ## Needed regardless of whether strict shape is being used. if (predcontrib) { - dimnames(arr) <- list(cnames, NULL, NULL) + .Call(XGSetArrayDimNamesInplace_R, arr, list(cnames, NULL, NULL)) } else if (predinteraction) { - dimnames(arr) <- list(cnames, cnames, NULL, NULL) + .Call(XGSetArrayDimNamesInplace_R, arr, list(cnames, cnames, NULL, NULL)) } if (strict_shape) { return(arr) # strict shape is calculated by libxgboost uniformly. @@ -368,43 +368,51 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA if (predleaf) { ## Predict leaf - arr <- if (n_ret == n_row) { - matrix(arr, ncol = 1) + if (n_ret == n_row) { + .Call(XGSetArrayDimInplace_R, arr, c(n_row, 1L)) } else { - matrix(arr, nrow = n_row, byrow = TRUE) + arr <- matrix(arr, nrow = n_row, byrow = TRUE) } } else if (predcontrib) { ## Predict contribution arr <- aperm(a = arr, perm = c(2, 3, 1)) # [group, row, col] - arr <- if (n_ret == n_row) { - matrix(arr, ncol = 1, dimnames = list(NULL, cnames)) + if (n_ret == n_row) { + .Call(XGSetArrayDimInplace_R, arr, c(n_row, 1L)) + .Call(XGSetArrayDimNamesInplace_R, arr, list(NULL, cnames)) } else if (n_groups != 1) { ## turns array into list of matrices - lapply(seq_len(n_groups), function(g) arr[g, , ]) + arr <- lapply(seq_len(n_groups), function(g) arr[g, , ]) } else { ## remove the first axis (group) - dn <- dimnames(arr) - matrix(arr[1, , ], nrow = dim(arr)[2], ncol = dim(arr)[3], dimnames = c(dn[2], dn[3])) + newdim <- dim(arr)[2:3] + newdn <- dimnames(arr)[2:3] + arr <- arr[1, , ] + .Call(XGSetArrayDimInplace_R, arr, newdim) + .Call(XGSetArrayDimNamesInplace_R, arr, newdn) } } else if (predinteraction) { ## Predict interaction arr <- aperm(a = arr, perm = c(3, 4, 1, 2)) # [group, row, col, col] - arr <- if (n_ret == n_row) { - matrix(arr, ncol = 1, dimnames = list(NULL, cnames)) + if (n_ret == n_row) { + .Call(XGSetArrayDimInplace_R, arr, c(n_row, 1L)) + .Call(XGSetArrayDimNamesInplace_R, arr, list(NULL, cnames)) } else if (n_groups != 1) { ## turns array into list of matrices - lapply(seq_len(n_groups), function(g) arr[g, , , ]) + arr <- lapply(seq_len(n_groups), function(g) arr[g, , , ]) } else { ## remove the first axis (group) arr <- arr[1, , , , drop = FALSE] - array(arr, dim = dim(arr)[2:4], dimnames(arr)[2:4]) + newdim <- dim(arr)[2:4] + newdn <- dimnames(arr)[2:4] + .Call(XGSetArrayDimInplace_R, arr, newdim) + .Call(XGSetArrayDimNamesInplace_R, arr, newdn) } } else { ## Normal prediction - arr <- if (reshape && n_groups != 1) { - matrix(arr, ncol = n_groups, byrow = TRUE) + if (reshape && n_groups != 1) { + arr <- matrix(arr, ncol = n_groups, byrow = TRUE) } else { - as.vector(ret) + .Call(XGSetArrayDimInplace_R, arr, NULL) } } return(arr) diff --git a/R-package/src/init.c b/R-package/src/init.c index 81c28c401..dd3a1aa2f 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -42,6 +42,8 @@ extern SEXP XGBoosterSetAttr_R(SEXP, SEXP, SEXP); extern SEXP XGBoosterSetParam_R(SEXP, SEXP, SEXP); extern SEXP XGBoosterUpdateOneIter_R(SEXP, SEXP, SEXP); extern SEXP XGCheckNullPtr_R(SEXP); +extern SEXP XGSetArrayDimInplace_R(SEXP, SEXP); +extern SEXP XGSetArrayDimNamesInplace_R(SEXP, SEXP); extern SEXP XGDMatrixCreateFromCSC_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP XGDMatrixCreateFromCSR_R(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP XGDMatrixCreateFromFile_R(SEXP, SEXP); @@ -90,6 +92,8 @@ static const R_CallMethodDef CallEntries[] = { {"XGBoosterSetParam_R", (DL_FUNC) &XGBoosterSetParam_R, 3}, {"XGBoosterUpdateOneIter_R", (DL_FUNC) &XGBoosterUpdateOneIter_R, 3}, {"XGCheckNullPtr_R", (DL_FUNC) &XGCheckNullPtr_R, 1}, + {"XGSetArrayDimInplace_R", (DL_FUNC) &XGSetArrayDimInplace_R, 2}, + {"XGSetArrayDimNamesInplace_R", (DL_FUNC) &XGSetArrayDimNamesInplace_R, 2}, {"XGDMatrixCreateFromCSC_R", (DL_FUNC) &XGDMatrixCreateFromCSC_R, 6}, {"XGDMatrixCreateFromCSR_R", (DL_FUNC) &XGDMatrixCreateFromCSR_R, 6}, {"XGDMatrixCreateFromFile_R", (DL_FUNC) &XGDMatrixCreateFromFile_R, 2}, diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 63f36ad6a..4a8710124 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -263,6 +263,16 @@ XGB_DLL SEXP XGCheckNullPtr_R(SEXP handle) { return Rf_ScalarLogical(R_ExternalPtrAddr(handle) == nullptr); } +XGB_DLL SEXP XGSetArrayDimInplace_R(SEXP arr, SEXP dims) { + Rf_setAttrib(arr, R_DimSymbol, dims); + return R_NilValue; +} + +XGB_DLL SEXP XGSetArrayDimNamesInplace_R(SEXP arr, SEXP dim_names) { + Rf_setAttrib(arr, R_DimNamesSymbol, dim_names); + return R_NilValue; +} + namespace { void _DMatrixFinalizer(SEXP ext) { R_API_BEGIN(); diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index 79d441792..e2688bf34 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -23,6 +23,22 @@ */ XGB_DLL SEXP XGCheckNullPtr_R(SEXP handle); +/*! + * \brief set the dimensions of an array in-place + * \param arr + * \param dims dimensions to set to the array + * \return NULL value + */ +XGB_DLL SEXP XGSetArrayDimInplace_R(SEXP arr, SEXP dims); + +/*! + * \brief set the names of the dimensions of an array in-place + * \param arr + * \param dim_names names for the dimensions to set + * \return NULL value + */ +XGB_DLL SEXP XGSetArrayDimNamesInplace_R(SEXP arr, SEXP dim_names); + /*! * \brief Set global configuration * \param json_str a JSON string representing the list of key-value pairs From c5d0608057f3c0b335fd31594c65a3a3ca4afca3 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 20 Jan 2024 17:56:57 +0100 Subject: [PATCH 105/109] [R] Remove parameters and attributes related to `ntree` and rebase `iterationrange` (#9935) --- R-package/R/callbacks.R | 31 ++++++------- R-package/R/utils.R | 2 +- R-package/R/xgb.Booster.R | 52 ++++++++++------------ R-package/R/xgb.cv.R | 4 +- R-package/R/xgb.train.R | 1 - R-package/demo/predict_first_ntree.R | 2 +- R-package/man/cb.cv.predict.Rd | 2 - R-package/man/cb.early.stop.Rd | 1 - R-package/man/predict.xgb.Booster.Rd | 24 +++++----- R-package/man/xgb.cv.Rd | 1 - R-package/tests/testthat/test_basic.R | 31 +++++-------- R-package/tests/testthat/test_callbacks.R | 53 +++++++++++++++++++---- R-package/tests/testthat/test_glm.R | 4 +- R-package/tests/testthat/test_ranking.R | 2 +- 14 files changed, 112 insertions(+), 98 deletions(-) diff --git a/R-package/R/callbacks.R b/R-package/R/callbacks.R index b3d6bdb1a..02e0a7cd4 100644 --- a/R-package/R/callbacks.R +++ b/R-package/R/callbacks.R @@ -280,7 +280,6 @@ cb.reset.parameters <- function(new_params) { #' \code{iteration}, #' \code{begin_iteration}, #' \code{end_iteration}, -#' \code{num_parallel_tree}. #' #' @seealso #' \code{\link{callbacks}}, @@ -291,7 +290,6 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, metric_name = NULL, verbose = TRUE) { # state variables best_iteration <- -1 - best_ntreelimit <- -1 best_score <- Inf best_msg <- NULL metric_idx <- 1 @@ -358,12 +356,10 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, # If the difference is due to floating-point truncation, update best_score best_score <- attr_best_score } - xgb.attr(env$bst, "best_iteration") <- best_iteration - xgb.attr(env$bst, "best_ntreelimit") <- best_ntreelimit + xgb.attr(env$bst, "best_iteration") <- best_iteration - 1 xgb.attr(env$bst, "best_score") <- best_score } else { env$basket$best_iteration <- best_iteration - env$basket$best_ntreelimit <- best_ntreelimit } } @@ -385,14 +381,13 @@ cb.early.stop <- function(stopping_rounds, maximize = FALSE, ) best_score <<- score best_iteration <<- i - best_ntreelimit <<- best_iteration * env$num_parallel_tree # save the property to attributes, so they will occur in checkpoint if (!is.null(env$bst)) { xgb.attributes(env$bst) <- list( best_iteration = best_iteration - 1, # convert to 0-based index best_score = best_score, - best_msg = best_msg, - best_ntreelimit = best_ntreelimit) + best_msg = best_msg + ) } } else if (i - best_iteration >= stopping_rounds) { env$stop_condition <- TRUE @@ -475,8 +470,6 @@ cb.save.model <- function(save_period = 0, save_name = "xgboost.ubj") { #' \code{data}, #' \code{end_iteration}, #' \code{params}, -#' \code{num_parallel_tree}, -#' \code{num_class}. #' #' @return #' Predictions are returned inside of the \code{pred} element, which is either a vector or a matrix, @@ -499,19 +492,21 @@ cb.cv.predict <- function(save_models = FALSE) { stop("'cb.cv.predict' callback requires 'basket' and 'bst_folds' lists in its calling frame") N <- nrow(env$data) - pred <- - if (env$num_class > 1) { - matrix(NA_real_, N, env$num_class) - } else { - rep(NA_real_, N) - } + pred <- NULL - iterationrange <- c(1, NVL(env$basket$best_iteration, env$end_iteration) + 1) + iterationrange <- c(1, NVL(env$basket$best_iteration, env$end_iteration)) if (NVL(env$params[['booster']], '') == 'gblinear') { - iterationrange <- c(1, 1) # must be 0 for gblinear + iterationrange <- "all" } for (fd in env$bst_folds) { pr <- predict(fd$bst, fd$watchlist[[2]], iterationrange = iterationrange, reshape = TRUE) + if (is.null(pred)) { + if (NCOL(pr) > 1L) { + pred <- matrix(NA_real_, N, ncol(pr)) + } else { + pred <- matrix(NA_real_, N) + } + } if (is.matrix(pred)) { pred[fd$index, ] <- pr } else { diff --git a/R-package/R/utils.R b/R-package/R/utils.R index 945d86132..e8ae787fc 100644 --- a/R-package/R/utils.R +++ b/R-package/R/utils.R @@ -208,7 +208,7 @@ xgb.iter.eval <- function(bst, watchlist, iter, feval) { res <- sapply(seq_along(watchlist), function(j) { w <- watchlist[[j]] ## predict using all trees - preds <- predict(bst, w, outputmargin = TRUE, iterationrange = c(1, 1)) + preds <- predict(bst, w, outputmargin = TRUE, iterationrange = "all") eval_res <- feval(preds, w) out <- eval_res$value names(out) <- paste0(evnames[j], "-", eval_res$metric) diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 5562c22f3..5d8346abc 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -89,7 +89,6 @@ xgb.get.handle <- function(object) { #' @param outputmargin Whether the prediction should be returned in the form of original untransformed #' sum of predictions from boosting iterations' results. E.g., setting `outputmargin=TRUE` for #' logistic regression would return log-odds instead of probabilities. -#' @param ntreelimit Deprecated, use `iterationrange` instead. #' @param predleaf Whether to predict pre-tree leaf indices. #' @param predcontrib Whether to return feature contributions to individual predictions (see Details). #' @param approxcontrib Whether to use a fast approximation for feature contributions (see Details). @@ -99,11 +98,17 @@ xgb.get.handle <- function(object) { #' or `predinteraction` is `TRUE`. #' @param training Whether the predictions are used for training. For dart booster, #' training predicting will perform dropout. -#' @param iterationrange Specifies which trees are used in prediction. For -#' example, take a random forest with 100 rounds. -#' With `iterationrange=c(1, 21)`, only the trees built during `[1, 21)` (half open set) -#' rounds are used in this prediction. The index is 1-based just like an R vector. When set -#' to `c(1, 1)`, XGBoost will use all trees. +#' @param iterationrange Sequence of rounds/iterations from the model to use for prediction, specified by passing +#' a two-dimensional vector with the start and end numbers in the sequence (same format as R's `seq` - i.e. +#' base-1 indexing, and inclusive of both ends). +#' +#' For example, passing `c(1,20)` will predict using the first twenty iterations, while passing `c(1,1)` will +#' predict using only the first one. +#' +#' If passing `NULL`, will either stop at the best iteration if the model used early stopping, or use all +#' of the iterations (rounds) otherwise. +#' +#' If passing "all", will use all of the rounds regardless of whether the model had early stopping or not. #' @param strict_shape Default is `FALSE`. When set to `TRUE`, the output #' type and shape of predictions are invariant to the model type. #' @param ... Not used. @@ -189,7 +194,7 @@ xgb.get.handle <- function(object) { #' # use all trees by default #' pred <- predict(bst, test$data) #' # use only the 1st tree -#' pred1 <- predict(bst, test$data, iterationrange = c(1, 2)) +#' pred1 <- predict(bst, test$data, iterationrange = c(1, 1)) #' #' # Predicting tree leafs: #' # the result is an nsamples X ntrees matrix @@ -260,11 +265,11 @@ xgb.get.handle <- function(object) { #' all.equal(pred, pred_labels) #' # prediction from using only 5 iterations should result #' # in the same error as seen in iteration 5: -#' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) +#' pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 5)) #' sum(pred5 != lb) / length(lb) #' #' @export -predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FALSE, ntreelimit = NULL, +predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FALSE, predleaf = FALSE, predcontrib = FALSE, approxcontrib = FALSE, predinteraction = FALSE, reshape = FALSE, training = FALSE, iterationrange = NULL, strict_shape = FALSE, ...) { if (!inherits(newdata, "xgb.DMatrix")) { @@ -275,25 +280,21 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA ) } - if (NVL(xgb.booster_type(object), '') == 'gblinear' || is.null(ntreelimit)) - ntreelimit <- 0 - if (ntreelimit != 0 && is.null(iterationrange)) { - ## only ntreelimit, initialize iteration range - iterationrange <- c(0, 0) - } else if (ntreelimit == 0 && !is.null(iterationrange)) { - ## only iteration range, handle 1-based indexing - iterationrange <- c(iterationrange[1] - 1, iterationrange[2] - 1) - } else if (ntreelimit != 0 && !is.null(iterationrange)) { - ## both are specified, let libgxgboost throw an error + if (!is.null(iterationrange)) { + if (is.character(iterationrange)) { + stopifnot(iterationrange == "all") + iterationrange <- c(0, 0) + } else { + iterationrange[1] <- iterationrange[1] - 1 # base-0 indexing + } } else { ## no limit is supplied, use best best_iteration <- xgb.best_iteration(object) if (is.null(best_iteration)) { iterationrange <- c(0, 0) } else { - ## We don't need to + 1 as R is 1-based index. - iterationrange <- c(0, as.integer(best_iteration)) + iterationrange <- c(0, as.integer(best_iteration) + 1L) } } ## Handle the 0 length values. @@ -312,7 +313,6 @@ predict.xgb.Booster <- function(object, newdata, missing = NA, outputmargin = FA strict_shape = box(TRUE), iteration_begin = box(as.integer(iterationrange[1])), iteration_end = box(as.integer(iterationrange[2])), - ntree_limit = box(as.integer(ntreelimit)), type = box(as.integer(0)) ) @@ -500,7 +500,7 @@ xgb.attr <- function(object, name) { return(NULL) } if (!is.null(out)) { - if (name %in% c("best_iteration", "best_ntreelimit", "best_score")) { + if (name %in% c("best_iteration", "best_score")) { out <- as.numeric(out) } } @@ -718,12 +718,6 @@ variable.names.xgb.Booster <- function(object, ...) { return(getinfo(object, "feature_name")) } -xgb.ntree <- function(bst) { - config <- xgb.config(bst) - out <- strtoi(config$learner$gradient_booster$gbtree_model_param$num_trees) - return(out) -} - xgb.nthread <- function(bst) { config <- xgb.config(bst) out <- strtoi(config$learner$generic_param$nthread) diff --git a/R-package/R/xgb.cv.R b/R-package/R/xgb.cv.R index a960957ca..eb0495631 100644 --- a/R-package/R/xgb.cv.R +++ b/R-package/R/xgb.cv.R @@ -103,7 +103,6 @@ #' parameter or randomly generated. #' \item \code{best_iteration} iteration number with the best evaluation metric value #' (only available with early stopping). -#' \item \code{best_ntreelimit} and the \code{ntreelimit} Deprecated attributes, use \code{best_iteration} instead. #' \item \code{pred} CV prediction values available when \code{prediction} is set. #' It is either vector or matrix (see \code{\link{cb.cv.predict}}). #' \item \code{models} a list of the CV folds' models. It is only available with the explicit @@ -218,7 +217,6 @@ xgb.cv <- function(params = list(), data, nrounds, nfold, label = NULL, missing # extract parameters that can affect the relationship b/w #trees and #iterations num_class <- max(as.numeric(NVL(params[['num_class']], 1)), 1) # nolint - num_parallel_tree <- max(as.numeric(NVL(params[['num_parallel_tree']], 1)), 1) # nolint # those are fixed for CV (no training continuation) begin_iteration <- 1 @@ -318,7 +316,7 @@ print.xgb.cv.synchronous <- function(x, verbose = FALSE, ...) { }) } - for (n in c('niter', 'best_iteration', 'best_ntreelimit')) { + for (n in c('niter', 'best_iteration')) { if (is.null(x[[n]])) next cat(n, ': ', x[[n]], '\n', sep = '') diff --git a/R-package/R/xgb.train.R b/R-package/R/xgb.train.R index a313ed32f..f0f2332b5 100644 --- a/R-package/R/xgb.train.R +++ b/R-package/R/xgb.train.R @@ -393,7 +393,6 @@ xgb.train <- function(params = list(), data, nrounds, watchlist = list(), # Note: it might look like these aren't used, but they need to be defined in this # environment for the callbacks for work correctly. num_class <- max(as.numeric(NVL(params[['num_class']], 1)), 1) # nolint - num_parallel_tree <- max(as.numeric(NVL(params[['num_parallel_tree']], 1)), 1) # nolint if (is_update && nrounds > niter_init) stop("nrounds cannot be larger than ", niter_init, " (nrounds of xgb_model)") diff --git a/R-package/demo/predict_first_ntree.R b/R-package/demo/predict_first_ntree.R index 02c168b77..179c18c70 100644 --- a/R-package/demo/predict_first_ntree.R +++ b/R-package/demo/predict_first_ntree.R @@ -15,7 +15,7 @@ cat('start testing prediction from first n trees\n') labels <- getinfo(dtest, 'label') ### predict using first 1 tree -ypred1 <- predict(bst, dtest, ntreelimit = 1) +ypred1 <- predict(bst, dtest, iterationrange = c(1, 1)) # by default, we predict using all the trees ypred2 <- predict(bst, dtest) diff --git a/R-package/man/cb.cv.predict.Rd b/R-package/man/cb.cv.predict.Rd index ded899e8a..4cabac1c9 100644 --- a/R-package/man/cb.cv.predict.Rd +++ b/R-package/man/cb.cv.predict.Rd @@ -35,8 +35,6 @@ Callback function expects the following values to be set in its calling frame: \code{data}, \code{end_iteration}, \code{params}, -\code{num_parallel_tree}, -\code{num_class}. } \seealso{ \code{\link{callbacks}} diff --git a/R-package/man/cb.early.stop.Rd b/R-package/man/cb.early.stop.Rd index 7b6efa842..7cd51a3ce 100644 --- a/R-package/man/cb.early.stop.Rd +++ b/R-package/man/cb.early.stop.Rd @@ -55,7 +55,6 @@ Callback function expects the following values to be set in its calling frame: \code{iteration}, \code{begin_iteration}, \code{end_iteration}, -\code{num_parallel_tree}. } \seealso{ \code{\link{callbacks}}, diff --git a/R-package/man/predict.xgb.Booster.Rd b/R-package/man/predict.xgb.Booster.Rd index 66194c64f..7a6dd6c13 100644 --- a/R-package/man/predict.xgb.Booster.Rd +++ b/R-package/man/predict.xgb.Booster.Rd @@ -9,7 +9,6 @@ newdata, missing = NA, outputmargin = FALSE, - ntreelimit = NULL, predleaf = FALSE, predcontrib = FALSE, approxcontrib = FALSE, @@ -36,8 +35,6 @@ missing values in data (e.g., 0 or some other extreme value).} sum of predictions from boosting iterations' results. E.g., setting \code{outputmargin=TRUE} for logistic regression would return log-odds instead of probabilities.} -\item{ntreelimit}{Deprecated, use \code{iterationrange} instead.} - \item{predleaf}{Whether to predict pre-tree leaf indices.} \item{predcontrib}{Whether to return feature contributions to individual predictions (see Details).} @@ -53,11 +50,18 @@ or \code{predinteraction} is \code{TRUE}.} \item{training}{Whether the predictions are used for training. For dart booster, training predicting will perform dropout.} -\item{iterationrange}{Specifies which trees are used in prediction. For -example, take a random forest with 100 rounds. -With \code{iterationrange=c(1, 21)}, only the trees built during \verb{[1, 21)} (half open set) -rounds are used in this prediction. The index is 1-based just like an R vector. When set -to \code{c(1, 1)}, XGBoost will use all trees.} +\item{iterationrange}{Sequence of rounds/iterations from the model to use for prediction, specified by passing +a two-dimensional vector with the start and end numbers in the sequence (same format as R's \code{seq} - i.e. +base-1 indexing, and inclusive of both ends). + +\if{html}{\out{
}}\preformatted{ For example, passing `c(1,20)` will predict using the first twenty iterations, while passing `c(1,1)` will + predict using only the first one. + + If passing `NULL`, will either stop at the best iteration if the model used early stopping, or use all + of the iterations (rounds) otherwise. + + If passing "all", will use all of the rounds regardless of whether the model had early stopping or not. +}\if{html}{\out{
}}} \item{strict_shape}{Default is \code{FALSE}. When set to \code{TRUE}, the output type and shape of predictions are invariant to the model type.} @@ -145,7 +149,7 @@ bst <- xgb.train( # use all trees by default pred <- predict(bst, test$data) # use only the 1st tree -pred1 <- predict(bst, test$data, iterationrange = c(1, 2)) +pred1 <- predict(bst, test$data, iterationrange = c(1, 1)) # Predicting tree leafs: # the result is an nsamples X ntrees matrix @@ -216,7 +220,7 @@ str(pred) all.equal(pred, pred_labels) # prediction from using only 5 iterations should result # in the same error as seen in iteration 5: -pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 6)) +pred5 <- predict(bst, as.matrix(iris[, -5]), iterationrange = c(1, 5)) sum(pred5 != lb) / length(lb) } diff --git a/R-package/man/xgb.cv.Rd b/R-package/man/xgb.cv.Rd index 2d8508c4d..9f6103a52 100644 --- a/R-package/man/xgb.cv.Rd +++ b/R-package/man/xgb.cv.Rd @@ -135,7 +135,6 @@ It is created by the \code{\link{cb.evaluation.log}} callback. parameter or randomly generated. \item \code{best_iteration} iteration number with the best evaluation metric value (only available with early stopping). -\item \code{best_ntreelimit} and the \code{ntreelimit} Deprecated attributes, use \code{best_iteration} instead. \item \code{pred} CV prediction values available when \code{prediction} is set. It is either vector or matrix (see \code{\link{cb.cv.predict}}). \item \code{models} a list of the CV folds' models. It is only available with the explicit diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 8dd934765..03a8ddbe1 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -33,15 +33,11 @@ test_that("train and predict binary classification", { pred <- predict(bst, test$data) expect_length(pred, 1611) - pred1 <- predict(bst, train$data, ntreelimit = 1) + pred1 <- predict(bst, train$data, iterationrange = c(1, 1)) expect_length(pred1, 6513) err_pred1 <- sum((pred1 > 0.5) != train$label) / length(train$label) err_log <- attributes(bst)$evaluation_log[1, train_error] expect_lt(abs(err_pred1 - err_log), 10e-6) - - pred2 <- predict(bst, train$data, iterationrange = c(1, 2)) - expect_length(pred1, 6513) - expect_equal(pred1, pred2) }) test_that("parameter validation works", { @@ -117,8 +113,8 @@ test_that("dart prediction works", { nrounds = nrounds, objective = "reg:squarederror" ) - pred_by_xgboost_0 <- predict(booster_by_xgboost, newdata = d, ntreelimit = 0) - pred_by_xgboost_1 <- predict(booster_by_xgboost, newdata = d, ntreelimit = nrounds) + pred_by_xgboost_0 <- predict(booster_by_xgboost, newdata = d, iterationrange = NULL) + pred_by_xgboost_1 <- predict(booster_by_xgboost, newdata = d, iterationrange = c(1, nrounds)) expect_true(all(matrix(pred_by_xgboost_0, byrow = TRUE) == matrix(pred_by_xgboost_1, byrow = TRUE))) pred_by_xgboost_2 <- predict(booster_by_xgboost, newdata = d, training = TRUE) @@ -139,8 +135,8 @@ test_that("dart prediction works", { data = dtrain, nrounds = nrounds ) - pred_by_train_0 <- predict(booster_by_train, newdata = dtrain, ntreelimit = 0) - pred_by_train_1 <- predict(booster_by_train, newdata = dtrain, ntreelimit = nrounds) + pred_by_train_0 <- predict(booster_by_train, newdata = dtrain, iterationrange = NULL) + pred_by_train_1 <- predict(booster_by_train, newdata = dtrain, iterationrange = c(1, nrounds)) pred_by_train_2 <- predict(booster_by_train, newdata = dtrain, training = TRUE) expect_true(all(matrix(pred_by_train_0, byrow = TRUE) == matrix(pred_by_xgboost_0, byrow = TRUE))) @@ -162,7 +158,7 @@ test_that("train and predict softprob", { ) expect_false(is.null(attributes(bst)$evaluation_log)) expect_lt(attributes(bst)$evaluation_log[, min(train_merror)], 0.025) - expect_equal(xgb.get.num.boosted.rounds(bst) * 3, xgb.ntree(bst)) + expect_equal(xgb.get.num.boosted.rounds(bst), 5) pred <- predict(bst, as.matrix(iris[, -5])) expect_length(pred, nrow(iris) * 3) # row sums add up to total probability of 1: @@ -174,12 +170,12 @@ test_that("train and predict softprob", { err <- sum(pred_labels != lb) / length(lb) expect_equal(attributes(bst)$evaluation_log[5, train_merror], err, tolerance = 5e-6) # manually calculate error at the 1st iteration: - mpred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, ntreelimit = 1) + mpred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, iterationrange = c(1, 1)) pred_labels <- max.col(mpred) - 1 err <- sum(pred_labels != lb) / length(lb) expect_equal(attributes(bst)$evaluation_log[1, train_merror], err, tolerance = 5e-6) - mpred1 <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, iterationrange = c(1, 2)) + mpred1 <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, iterationrange = c(1, 1)) expect_equal(mpred, mpred1) d <- cbind( @@ -213,7 +209,7 @@ test_that("train and predict softmax", { ) expect_false(is.null(attributes(bst)$evaluation_log)) expect_lt(attributes(bst)$evaluation_log[, min(train_merror)], 0.025) - expect_equal(xgb.get.num.boosted.rounds(bst) * 3, xgb.ntree(bst)) + expect_equal(xgb.get.num.boosted.rounds(bst), 5) pred <- predict(bst, as.matrix(iris[, -5])) expect_length(pred, nrow(iris)) @@ -233,19 +229,15 @@ test_that("train and predict RF", { watchlist = list(train = xgb.DMatrix(train$data, label = lb)) ) expect_equal(xgb.get.num.boosted.rounds(bst), 1) - expect_equal(xgb.ntree(bst), 20) pred <- predict(bst, train$data) pred_err <- sum((pred > 0.5) != lb) / length(lb) expect_lt(abs(attributes(bst)$evaluation_log[1, train_error] - pred_err), 10e-6) # expect_lt(pred_err, 0.03) - pred <- predict(bst, train$data, ntreelimit = 20) + pred <- predict(bst, train$data, iterationrange = c(1, 1)) pred_err_20 <- sum((pred > 0.5) != lb) / length(lb) expect_equal(pred_err_20, pred_err) - - pred1 <- predict(bst, train$data, iterationrange = c(1, 2)) - expect_equal(pred, pred1) }) test_that("train and predict RF with softprob", { @@ -261,7 +253,6 @@ test_that("train and predict RF with softprob", { watchlist = list(train = xgb.DMatrix(as.matrix(iris[, -5]), label = lb)) ) expect_equal(xgb.get.num.boosted.rounds(bst), 15) - expect_equal(xgb.ntree(bst), 15 * 3 * 4) # predict for all iterations: pred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE) expect_equal(dim(pred), c(nrow(iris), 3)) @@ -269,7 +260,7 @@ test_that("train and predict RF with softprob", { err <- sum(pred_labels != lb) / length(lb) expect_equal(attributes(bst)$evaluation_log[nrounds, train_merror], err, tolerance = 5e-6) # predict for 7 iterations and adjust for 4 parallel trees per iteration - pred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, ntreelimit = 7 * 4) + pred <- predict(bst, as.matrix(iris[, -5]), reshape = TRUE, iterationrange = c(1, 7)) err <- sum((max.col(pred) - 1) != lb) / length(lb) expect_equal(attributes(bst)$evaluation_log[7, train_merror], err, tolerance = 5e-6) }) diff --git a/R-package/tests/testthat/test_callbacks.R b/R-package/tests/testthat/test_callbacks.R index afa270c0b..c60d0c246 100644 --- a/R-package/tests/testthat/test_callbacks.R +++ b/R-package/tests/testthat/test_callbacks.R @@ -211,12 +211,11 @@ test_that("early stopping xgb.train works", { , "Stopping. Best iteration") expect_false(is.null(xgb.attr(bst, "best_iteration"))) expect_lt(xgb.attr(bst, "best_iteration"), 19) - expect_equal(xgb.attr(bst, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) pred <- predict(bst, dtest) expect_equal(length(pred), 1611) err_pred <- err(ltest, pred) - err_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration"), test_error] + err_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration") + 1, test_error] expect_equal(err_log, err_pred, tolerance = 5e-6) set.seed(11) @@ -231,8 +230,7 @@ test_that("early stopping xgb.train works", { loaded <- xgb.load(fname) expect_false(is.null(xgb.attr(loaded, "best_iteration"))) - expect_equal(xgb.attr(loaded, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) - expect_equal(xgb.attr(loaded, "best_ntreelimit"), xgb.attr(bst, "best_ntreelimit")) + expect_equal(xgb.attr(loaded, "best_iteration"), xgb.attr(bst, "best_iteration")) }) test_that("early stopping using a specific metric works", { @@ -245,12 +243,11 @@ test_that("early stopping using a specific metric works", { , "Stopping. Best iteration") expect_false(is.null(xgb.attr(bst, "best_iteration"))) expect_lt(xgb.attr(bst, "best_iteration"), 19) - expect_equal(xgb.attr(bst, "best_iteration"), xgb.attr(bst, "best_ntreelimit")) - pred <- predict(bst, dtest, ntreelimit = xgb.attr(bst, "best_ntreelimit")) + pred <- predict(bst, dtest, iterationrange = c(1, xgb.attr(bst, "best_iteration") + 1)) expect_equal(length(pred), 1611) logloss_pred <- sum(-ltest * log(pred) - (1 - ltest) * log(1 - pred)) / length(ltest) - logloss_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration"), test_logloss] + logloss_log <- attributes(bst)$evaluation_log[xgb.attr(bst, "best_iteration") + 1, test_logloss] expect_equal(logloss_log, logloss_pred, tolerance = 1e-5) }) @@ -286,7 +283,6 @@ test_that("early stopping xgb.cv works", { , "Stopping. Best iteration") expect_false(is.null(cv$best_iteration)) expect_lt(cv$best_iteration, 19) - expect_equal(cv$best_iteration, cv$best_ntreelimit) # the best error is min error: expect_true(cv$evaluation_log[, test_error_mean[cv$best_iteration] == min(test_error_mean)]) }) @@ -354,3 +350,44 @@ test_that("prediction in xgb.cv for softprob works", { expect_equal(dim(cv$pred), c(nrow(iris), 3)) expect_lt(diff(range(rowSums(cv$pred))), 1e-6) }) + +test_that("prediction in xgb.cv works for multi-quantile", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = y, nthread = 1) + cv <- xgb.cv( + data = dm, + params = list( + objective = "reg:quantileerror", + quantile_alpha = c(0.1, 0.2, 0.5, 0.8, 0.9), + nthread = 1 + ), + nrounds = 5, + nfold = 3, + prediction = TRUE, + verbose = 0 + ) + expect_equal(dim(cv$pred), c(nrow(x), 5)) +}) + +test_that("prediction in xgb.cv works for multi-output", { + data(mtcars) + y <- mtcars$mpg + x <- as.matrix(mtcars[, -1]) + dm <- xgb.DMatrix(x, label = cbind(y, -y), nthread = 1) + cv <- xgb.cv( + data = dm, + params = list( + tree_method = "hist", + multi_strategy = "multi_output_tree", + objective = "reg:squarederror", + nthread = n_threads + ), + nrounds = 5, + nfold = 3, + prediction = TRUE, + verbose = 0 + ) + expect_equal(dim(cv$pred), c(nrow(x), 2)) +}) diff --git a/R-package/tests/testthat/test_glm.R b/R-package/tests/testthat/test_glm.R index ae698d98f..349bcce8d 100644 --- a/R-package/tests/testthat/test_glm.R +++ b/R-package/tests/testthat/test_glm.R @@ -72,10 +72,10 @@ test_that("gblinear early stopping works", { booster <- xgb.train( param, dtrain, n, list(eval = dtest, train = dtrain), early_stopping_rounds = es_round ) - expect_equal(xgb.attr(booster, "best_iteration"), 5) + expect_equal(xgb.attr(booster, "best_iteration"), 4) predt_es <- predict(booster, dtrain) - n <- xgb.attr(booster, "best_iteration") + es_round + n <- xgb.attr(booster, "best_iteration") + es_round + 1 booster <- xgb.train( param, dtrain, n, list(eval = dtest, train = dtrain), early_stopping_rounds = es_round ) diff --git a/R-package/tests/testthat/test_ranking.R b/R-package/tests/testthat/test_ranking.R index 277c8f288..e49a32025 100644 --- a/R-package/tests/testthat/test_ranking.R +++ b/R-package/tests/testthat/test_ranking.R @@ -44,7 +44,7 @@ test_that('Test ranking with weighted data', { expect_true(all(diff(attributes(bst)$evaluation_log$train_auc) >= 0)) expect_true(all(diff(attributes(bst)$evaluation_log$train_aucpr) >= 0)) for (i in 1:10) { - pred <- predict(bst, newdata = dtrain, ntreelimit = i) + pred <- predict(bst, newdata = dtrain, iterationrange = c(1, i)) # is_sorted[i]: is i-th group correctly sorted by the ranking predictor? is_sorted <- lapply(seq(1, 20, by = 5), function(k) { From 5062a3ab469582d084daaf3d240833bdb6ac0d31 Mon Sep 17 00:00:00 2001 From: david-cortes Date: Sat, 20 Jan 2024 22:11:26 +0100 Subject: [PATCH 106/109] [R] Support booster slicing. (#9948) --- R-package/NAMESPACE | 3 + R-package/R/xgb.Booster.R | 80 ++++++++++++++++++- R-package/man/xgb.get.num.boosted.rounds.Rd | 5 +- R-package/man/xgb.slice.Booster.Rd | 57 +++++++++++++ R-package/src/init.c | 2 + R-package/src/xgboost_R.cc | 15 ++++ R-package/src/xgboost_R.h | 10 +++ .../tests/testthat/test_booster_slicing.R | 67 ++++++++++++++++ 8 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 R-package/man/xgb.slice.Booster.Rd create mode 100644 R-package/tests/testthat/test_booster_slicing.R diff --git a/R-package/NAMESPACE b/R-package/NAMESPACE index a29c9b1e0..398b0da5a 100644 --- a/R-package/NAMESPACE +++ b/R-package/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +S3method("[",xgb.Booster) S3method("[",xgb.DMatrix) S3method("dimnames<-",xgb.DMatrix) S3method(coef,xgb.Booster) @@ -7,6 +8,7 @@ S3method(dim,xgb.DMatrix) S3method(dimnames,xgb.DMatrix) S3method(getinfo,xgb.Booster) S3method(getinfo,xgb.DMatrix) +S3method(length,xgb.Booster) S3method(predict,xgb.Booster) S3method(print,xgb.Booster) S3method(print,xgb.DMatrix) @@ -62,6 +64,7 @@ export(xgb.plot.tree) export(xgb.save) export(xgb.save.raw) export(xgb.set.config) +export(xgb.slice.Booster) export(xgb.train) export(xgboost) import(methods) diff --git a/R-package/R/xgb.Booster.R b/R-package/R/xgb.Booster.R index 5d8346abc..7613c9152 100644 --- a/R-package/R/xgb.Booster.R +++ b/R-package/R/xgb.Booster.R @@ -693,16 +693,94 @@ setinfo.xgb.Booster <- function(object, name, info) { } #' @title Get number of boosting in a fitted booster -#' @param model A fitted `xgb.Booster` model. +#' @param model,x A fitted `xgb.Booster` model. #' @return The number of rounds saved in the model, as an integer. #' @details Note that setting booster parameters related to training #' continuation / updates through \link{xgb.parameters<-} will reset the #' number of rounds to zero. #' @export +#' @rdname xgb.get.num.boosted.rounds xgb.get.num.boosted.rounds <- function(model) { return(.Call(XGBoosterBoostedRounds_R, xgb.get.handle(model))) } +#' @rdname xgb.get.num.boosted.rounds +#' @export +length.xgb.Booster <- function(x) { + return(xgb.get.num.boosted.rounds(x)) +} + +#' @title Slice Booster by Rounds +#' @description Creates a new booster including only a selected range of rounds / iterations +#' from an existing booster, as given by the sequence `seq(start, end, step)`. +#' @details Note that any R attributes that the booster might have, will not be copied into +#' the resulting object. +#' @param model,x A fitted `xgb.Booster` object, which is to be sliced by taking only a subset +#' of its rounds / iterations. +#' @param start Start of the slice (base-1 and inclusive, like R's \link{seq}). +#' @param end End of the slice (base-1 and inclusive, like R's \link{seq}). +#' +#' Passing a value of zero here is equivalent to passing the full number of rounds in the +#' booster object. +#' @param step Step size of the slice. Passing '1' will take every round in the sequence defined by +#' `(start, end)`, while passing '2' will take every second value, and so on. +#' @return A sliced booster object containing only the requested rounds. +#' @examples +#' data(mtcars) +#' y <- mtcars$mpg +#' x <- as.matrix(mtcars[, -1]) +#' dm <- xgb.DMatrix(x, label = y, nthread = 1) +#' model <- xgb.train(data = dm, params = list(nthread = 1), nrounds = 5) +#' model_slice <- xgb.slice.Booster(model, 1, 3) +#' # Prediction for first three rounds +#' predict(model, x, predleaf = TRUE)[, 1:3] +#' +#' # The new model has only those rounds, so +#' # a full prediction from it is equivalent +#' predict(model_slice, x, predleaf = TRUE) +#' @export +#' @rdname xgb.slice.Booster +xgb.slice.Booster <- function(model, start, end = xgb.get.num.boosted.rounds(model), step = 1L) { + # This makes the slice mimic the behavior of R's 'seq', + # which truncates on the end of the slice when the step + # doesn't reach it. + if (end > start && step > 1) { + d <- (end - start + 1) / step + if (d != floor(d)) { + end <- start + step * ceiling(d) - 1 + } + } + return( + .Call( + XGBoosterSlice_R, + xgb.get.handle(model), + start - 1, + end, + step + ) + ) +} + +#' @export +#' @rdname xgb.slice.Booster +#' @param i The indices - must be an increasing sequence as generated by e.g. `seq(...)`. +`[.xgb.Booster` <- function(x, i) { + if (missing(i)) { + return(xgb.slice.Booster(x, 1, 0)) + } + if (length(i) == 1) { + return(xgb.slice.Booster(x, i, i)) + } + steps <- diff(i) + if (any(steps < 0)) { + stop("Can only slice booster with ascending sequences.") + } + if (length(unique(steps)) > 1) { + stop("Can only slice booster with fixed-step sequences.") + } + return(xgb.slice.Booster(x, i[1L], i[length(i)], steps[1L])) +} + #' @title Get Features Names from Booster #' @description Returns the feature / variable / column names from a fitted #' booster object, which are set automatically during the call to \link{xgb.train} diff --git a/R-package/man/xgb.get.num.boosted.rounds.Rd b/R-package/man/xgb.get.num.boosted.rounds.Rd index 74c94d95b..551dc4a83 100644 --- a/R-package/man/xgb.get.num.boosted.rounds.Rd +++ b/R-package/man/xgb.get.num.boosted.rounds.Rd @@ -2,12 +2,15 @@ % Please edit documentation in R/xgb.Booster.R \name{xgb.get.num.boosted.rounds} \alias{xgb.get.num.boosted.rounds} +\alias{length.xgb.Booster} \title{Get number of boosting in a fitted booster} \usage{ xgb.get.num.boosted.rounds(model) + +\method{length}{xgb.Booster}(x) } \arguments{ -\item{model}{A fitted \code{xgb.Booster} model.} +\item{model, x}{A fitted \code{xgb.Booster} model.} } \value{ The number of rounds saved in the model, as an integer. diff --git a/R-package/man/xgb.slice.Booster.Rd b/R-package/man/xgb.slice.Booster.Rd new file mode 100644 index 000000000..759139901 --- /dev/null +++ b/R-package/man/xgb.slice.Booster.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/xgb.Booster.R +\name{xgb.slice.Booster} +\alias{xgb.slice.Booster} +\alias{[.xgb.Booster} +\title{Slice Booster by Rounds} +\usage{ +xgb.slice.Booster( + model, + start, + end = xgb.get.num.boosted.rounds(model), + step = 1L +) + +\method{[}{xgb.Booster}(x, i) +} +\arguments{ +\item{model, x}{A fitted \code{xgb.Booster} object, which is to be sliced by taking only a subset +of its rounds / iterations.} + +\item{start}{Start of the slice (base-1 and inclusive, like R's \link{seq}).} + +\item{end}{End of the slice (base-1 and inclusive, like R's \link{seq}). + +Passing a value of zero here is equivalent to passing the full number of rounds in the +booster object.} + +\item{step}{Step size of the slice. Passing '1' will take every round in the sequence defined by +\verb{(start, end)}, while passing '2' will take every second value, and so on.} + +\item{i}{The indices - must be an increasing sequence as generated by e.g. \code{seq(...)}.} +} +\value{ +A sliced booster object containing only the requested rounds. +} +\description{ +Creates a new booster including only a selected range of rounds / iterations +from an existing booster, as given by the sequence \code{seq(start, end, step)}. +} +\details{ +Note that any R attributes that the booster might have, will not be copied into +the resulting object. +} +\examples{ +data(mtcars) +y <- mtcars$mpg +x <- as.matrix(mtcars[, -1]) +dm <- xgb.DMatrix(x, label = y, nthread = 1) +model <- xgb.train(data = dm, params = list(nthread = 1), nrounds = 5) +model_slice <- xgb.slice.Booster(model, 1, 3) +# Prediction for first three rounds +predict(model, x, predleaf = TRUE)[, 1:3] + +# The new model has only those rounds, so +# a full prediction from it is equivalent +predict(model_slice, x, predleaf = TRUE) +} diff --git a/R-package/src/init.c b/R-package/src/init.c index dd3a1aa2f..fff5d9f90 100644 --- a/R-package/src/init.c +++ b/R-package/src/init.c @@ -64,6 +64,7 @@ extern SEXP XGDMatrixSliceDMatrix_R(SEXP, SEXP); extern SEXP XGBSetGlobalConfig_R(SEXP); extern SEXP XGBGetGlobalConfig_R(void); extern SEXP XGBoosterFeatureScore_R(SEXP, SEXP); +extern SEXP XGBoosterSlice_R(SEXP, SEXP, SEXP, SEXP); static const R_CallMethodDef CallEntries[] = { {"XGDuplicate_R", (DL_FUNC) &XGDuplicate_R, 1}, @@ -114,6 +115,7 @@ static const R_CallMethodDef CallEntries[] = { {"XGBSetGlobalConfig_R", (DL_FUNC) &XGBSetGlobalConfig_R, 1}, {"XGBGetGlobalConfig_R", (DL_FUNC) &XGBGetGlobalConfig_R, 0}, {"XGBoosterFeatureScore_R", (DL_FUNC) &XGBoosterFeatureScore_R, 2}, + {"XGBoosterSlice_R", (DL_FUNC) &XGBoosterSlice_R, 4}, {NULL, NULL, 0} }; diff --git a/R-package/src/xgboost_R.cc b/R-package/src/xgboost_R.cc index 4a8710124..1d01b9aae 100644 --- a/R-package/src/xgboost_R.cc +++ b/R-package/src/xgboost_R.cc @@ -1289,3 +1289,18 @@ XGB_DLL SEXP XGBoosterFeatureScore_R(SEXP handle, SEXP json_config) { return r_out; } + +XGB_DLL SEXP XGBoosterSlice_R(SEXP handle, SEXP begin_layer, SEXP end_layer, SEXP step) { + SEXP out = Rf_protect(XGBMakeEmptyAltrep()); + R_API_BEGIN(); + BoosterHandle handle_out = nullptr; + CHECK_CALL(XGBoosterSlice(R_ExternalPtrAddr(handle), + Rf_asInteger(begin_layer), + Rf_asInteger(end_layer), + Rf_asInteger(step), + &handle_out)); + XGBAltrepSetPointer(out, handle_out); + R_API_END(); + Rf_unprotect(1); + return out; +} diff --git a/R-package/src/xgboost_R.h b/R-package/src/xgboost_R.h index e2688bf34..ec30dbada 100644 --- a/R-package/src/xgboost_R.h +++ b/R-package/src/xgboost_R.h @@ -402,4 +402,14 @@ XGB_DLL SEXP XGBoosterGetAttrNames_R(SEXP handle); */ XGB_DLL SEXP XGBoosterFeatureScore_R(SEXP handle, SEXP json_config); +/*! + * \brief Slice a fitted booster model (by rounds) + * \param handle handle to the fitted booster + * \param begin_layer start of the slice + * \param end_later end of the slice; end_layer=0 is equivalent to end_layer=num_boost_round + * \param step step size of the slice + * \return The sliced booster with the requested rounds only + */ +XGB_DLL SEXP XGBoosterSlice_R(SEXP handle, SEXP begin_layer, SEXP end_layer, SEXP step); + #endif // XGBOOST_WRAPPER_R_H_ // NOLINT(*) diff --git a/R-package/tests/testthat/test_booster_slicing.R b/R-package/tests/testthat/test_booster_slicing.R new file mode 100644 index 000000000..711ccd8b6 --- /dev/null +++ b/R-package/tests/testthat/test_booster_slicing.R @@ -0,0 +1,67 @@ +context("testing xgb.Booster slicing") + +data(agaricus.train, package = "xgboost") +dm <- xgb.DMatrix(agaricus.train$data, label = agaricus.train$label, nthread = 1) +# Note: here need large step sizes in order for the predictions +# to have substantially different leaf assignments on each tree +model <- xgb.train( + params = list(objective = "binary:logistic", nthread = 1, max_depth = 4, eta = 0.5), + data = dm, + nrounds = 20 +) +pred <- predict(model, dm, predleaf = TRUE, reshape = TRUE) + +test_that("Slicing full model", { + new_model <- xgb.slice.Booster(model, 1, 0) + expect_equal(xgb.save.raw(new_model), xgb.save.raw(model)) + + new_model <- model[] + expect_equal(xgb.save.raw(new_model), xgb.save.raw(model)) + + new_model <- model[1:length(model)] # nolint + expect_equal(xgb.save.raw(new_model), xgb.save.raw(model)) +}) + +test_that("Slicing sequence from start", { + new_model <- xgb.slice.Booster(model, 1, 10) + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(1, 10)]) + + new_model <- model[1:10] + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(1, 10)]) +}) + +test_that("Slicing sequence from middle", { + new_model <- xgb.slice.Booster(model, 5, 10) + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(5, 10)]) + + new_model <- model[5:10] + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(5, 10)]) +}) + +test_that("Slicing with non-unit step", { + for (s in 2:5) { + new_model <- xgb.slice.Booster(model, 1, 17, s) + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(1, 17, s)]) + + new_model <- model[seq(1, 17, s)] + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(1, 17, s)]) + } +}) + +test_that("Slicing with non-unit step from middle", { + for (s in 2:5) { + new_model <- xgb.slice.Booster(model, 4, 17, s) + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(4, 17, s)]) + + new_model <- model[seq(4, 17, s)] + new_pred <- predict(new_model, dm, predleaf = TRUE, reshape = TRUE) + expect_equal(new_pred, pred[, seq(4, 17, s)]) + } +}) From 1e0ccf7b879866dcf70bfe6982ee47b15d56a890 Mon Sep 17 00:00:00 2001 From: Hui Liu <96135754+hliuca@users.noreply.github.com> Date: Sun, 21 Jan 2024 12:48:41 -0800 Subject: [PATCH 107/109] fix random --- CMakeLists.txt | 3 ++- src/c_api/c_api.cu | 2 ++ src/collective/nccl_stub.cc | 1 - src/collective/nccl_stub.h | 10 +++++++++- src/common/random.cc | 2 +- src/common/random.h | 2 +- src/common/random.hip | 4 ++++ 7 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/common/random.hip diff --git a/CMakeLists.txt b/CMakeLists.txt index 5844da216..58b9b8fb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -256,9 +256,10 @@ if (USE_HIP) find_package(rocthrust REQUIRED) find_package(hipcub REQUIRED) - set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -I${HIP_INCLUDE_DIRS} -I${HIP_INCLUDE_DIRS}/hip") + set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -I${HIP_INCLUDE_DIRS}") set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -Wunused-result -w") set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -D__HIP_PLATFORM_AMD__") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${HIP_INCLUDE_DIRS}") add_subdirectory(${PROJECT_SOURCE_DIR}/rocgputreeshap) endif (USE_HIP) diff --git a/src/c_api/c_api.cu b/src/c_api/c_api.cu index ebcea1c2f..a0bd28b72 100644 --- a/src/c_api/c_api.cu +++ b/src/c_api/c_api.cu @@ -55,8 +55,10 @@ void XGBBuildInfoDevice(Json *p_info) { info["RCCL_VERSION"] = v; info["NCCL_VERSION"] = v; #if defined(XGBOOST_USE_DLOPEN_RCCL) + info["USE_DLOPEN_NCCL"] = Boolean{true}; info["USE_DLOPEN_RCCL"] = Boolean{true}; #else + info["USE_DLOPEN_NCCL"] = Boolean{false}; info["USE_DLOPEN_RCCL"] = Boolean{false}; #endif // defined(XGBOOST_USE_DLOPEN_RCCL) #else diff --git a/src/collective/nccl_stub.cc b/src/collective/nccl_stub.cc index 408432438..44bd3e9a1 100644 --- a/src/collective/nccl_stub.cc +++ b/src/collective/nccl_stub.cc @@ -13,7 +13,6 @@ #include // for system_error #elif defined(XGBOOST_USE_RCCL) #include "../common/cuda_to_hip.h" -#include "../common/device_helpers.hip.h" #include // for cudaPeekAtLastError #include // for dlclose, dlsym, dlopen #include diff --git a/src/collective/nccl_stub.h b/src/collective/nccl_stub.h index 978f34028..60388ac9e 100644 --- a/src/collective/nccl_stub.h +++ b/src/collective/nccl_stub.h @@ -9,7 +9,15 @@ #include #elif defined(XGBOOST_USE_RCCL) #include "../common/cuda_to_hip.h" -#include "../common/device_helpers.cuh" + +#ifndef __HIP_PLATFORM_AMD__ +#define __HIP_PLATFORM_AMD__ +#endif + +#ifndef THRUST_DEVICE_SYSTEM +#define THRUST_DEVICE_SYSTEM THRUST_DEVICE_SYSTEM_HIP +#endif + #include #include #endif diff --git a/src/common/random.cc b/src/common/random.cc index e0d1a2255..7d2c34dd8 100644 --- a/src/common/random.cc +++ b/src/common/random.cc @@ -19,7 +19,7 @@ std::shared_ptr> ColumnSampler::ColSample( auto p_new_features = std::make_shared>(); if (ctx_->IsCUDA()) { -#if defined(XGBOOST_USE_CUDA) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) cuda_impl::SampleFeature(ctx_, n, p_features, p_new_features, this->feature_weights_, &this->weight_buffer_, &this->idx_buffer_, &rng_); return p_new_features; diff --git a/src/common/random.h b/src/common/random.h index 2a94123a3..098e94b74 100644 --- a/src/common/random.h +++ b/src/common/random.h @@ -180,7 +180,7 @@ class ColumnSampler { if (ctx->IsCPU()) { std::iota(feature_set_tree_->HostVector().begin(), feature_set_tree_->HostVector().end(), 0); } else { -#if defined(XGBOOST_USE_CUDA) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) cuda_impl::InitFeatureSet(ctx, feature_set_tree_); #else AssertGPUSupport(); diff --git a/src/common/random.hip b/src/common/random.hip new file mode 100644 index 000000000..8f2a6f7a0 --- /dev/null +++ b/src/common/random.hip @@ -0,0 +1,4 @@ + +#if defined(XGBOOST_USE_HIP) +#include "random.cu" +#endif From d12cc1090a6a38dbdaba10ef97b780d5307b4e7c Mon Sep 17 00:00:00 2001 From: Jiaming Yuan Date: Wed, 24 Jan 2024 16:07:19 +0800 Subject: [PATCH 108/109] Refactor tests for training continuation. (#9997) --- .../xgboost/testing/continuation.py | 58 ++++++++++++++ tests/ci_build/lint_python.py | 2 + .../test_gpu_training_continuation.py | 52 ++---------- tests/python/test_training_continuation.py | 79 ++++++++++--------- 4 files changed, 105 insertions(+), 86 deletions(-) create mode 100644 python-package/xgboost/testing/continuation.py diff --git a/python-package/xgboost/testing/continuation.py b/python-package/xgboost/testing/continuation.py new file mode 100644 index 000000000..9d6dc0338 --- /dev/null +++ b/python-package/xgboost/testing/continuation.py @@ -0,0 +1,58 @@ +"""Tests for training continuation.""" +import json +from typing import Any, Dict, TypeVar + +import numpy as np +import pytest + +import xgboost as xgb + + +# pylint: disable=too-many-locals +def run_training_continuation_model_output(device: str, tree_method: str) -> None: + """Run training continuation test.""" + datasets = pytest.importorskip("sklearn.datasets") + n_samples = 64 + n_features = 32 + X, y = datasets.make_regression(n_samples, n_features, random_state=1) + + dtrain = xgb.DMatrix(X, y) + params = { + "tree_method": tree_method, + "max_depth": "2", + "gamma": "0.1", + "alpha": "0.01", + "device": device, + } + bst_0 = xgb.train(params, dtrain, num_boost_round=64) + dump_0 = bst_0.get_dump(dump_format="json") + + bst_1 = xgb.train(params, dtrain, num_boost_round=32) + bst_1 = xgb.train(params, dtrain, num_boost_round=32, xgb_model=bst_1) + dump_1 = bst_1.get_dump(dump_format="json") + + T = TypeVar("T", Dict[str, Any], float, str, int, list) + + def recursive_compare(obj_0: T, obj_1: T) -> None: + if isinstance(obj_0, float): + assert np.isclose(obj_0, obj_1, atol=1e-6) + elif isinstance(obj_0, str): + assert obj_0 == obj_1 + elif isinstance(obj_0, int): + assert obj_0 == obj_1 + elif isinstance(obj_0, dict): + for i in range(len(obj_0.items())): + assert list(obj_0.keys())[i] == list(obj_1.keys())[i] + if list(obj_0.keys())[i] != "missing": + recursive_compare(list(obj_0.values()), list(obj_1.values())) + else: + for i, lhs in enumerate(obj_0): + rhs = obj_1[i] + recursive_compare(lhs, rhs) + + assert len(dump_0) == len(dump_1) + + for i, lhs in enumerate(dump_0): + obj_0 = json.loads(lhs) + obj_1 = json.loads(dump_1[i]) + recursive_compare(obj_0, obj_1) diff --git a/tests/ci_build/lint_python.py b/tests/ci_build/lint_python.py index bd9b9bedb..91b748b4c 100644 --- a/tests/ci_build/lint_python.py +++ b/tests/ci_build/lint_python.py @@ -28,6 +28,7 @@ class LintersPaths: "tests/python/test_predict.py", "tests/python/test_quantile_dmatrix.py", "tests/python/test_tree_regularization.py", + "tests/python/test_training_continuation.py", "tests/python/test_shap.py", "tests/python/test_model_io.py", "tests/python/test_with_pandas.py", @@ -91,6 +92,7 @@ class LintersPaths: "tests/python/test_multi_target.py", "tests/python-gpu/test_gpu_data_iterator.py", "tests/python-gpu/load_pickle.py", + "tests/python-gpu/test_gpu_training_continuation.py", "tests/python/test_model_io.py", "tests/test_distributed/test_with_spark/test_data.py", "tests/test_distributed/test_gpu_with_spark/test_data.py", diff --git a/tests/python-gpu/test_gpu_training_continuation.py b/tests/python-gpu/test_gpu_training_continuation.py index a67d2f26b..6f948890d 100644 --- a/tests/python-gpu/test_gpu_training_continuation.py +++ b/tests/python-gpu/test_gpu_training_continuation.py @@ -1,54 +1,12 @@ -import json - import numpy as np +import pytest -import xgboost as xgb +from xgboost.testing.continuation import run_training_continuation_model_output rng = np.random.RandomState(1994) class TestGPUTrainingContinuation: - def test_training_continuation(self): - kRows = 64 - kCols = 32 - X = np.random.randn(kRows, kCols) - y = np.random.randn(kRows) - dtrain = xgb.DMatrix(X, y) - params = { - "tree_method": "gpu_hist", - "max_depth": "2", - "gamma": "0.1", - "alpha": "0.01", - } - bst_0 = xgb.train(params, dtrain, num_boost_round=64) - dump_0 = bst_0.get_dump(dump_format="json") - - bst_1 = xgb.train(params, dtrain, num_boost_round=32) - bst_1 = xgb.train(params, dtrain, num_boost_round=32, xgb_model=bst_1) - dump_1 = bst_1.get_dump(dump_format="json") - - def recursive_compare(obj_0, obj_1): - if isinstance(obj_0, float): - assert np.isclose(obj_0, obj_1, atol=1e-6) - elif isinstance(obj_0, str): - assert obj_0 == obj_1 - elif isinstance(obj_0, int): - assert obj_0 == obj_1 - elif isinstance(obj_0, dict): - keys_0 = list(obj_0.keys()) - keys_1 = list(obj_1.keys()) - values_0 = list(obj_0.values()) - values_1 = list(obj_1.values()) - for i in range(len(obj_0.items())): - assert keys_0[i] == keys_1[i] - if list(obj_0.keys())[i] != "missing": - recursive_compare(values_0[i], values_1[i]) - else: - for i in range(len(obj_0)): - recursive_compare(obj_0[i], obj_1[i]) - - assert len(dump_0) == len(dump_1) - for i in range(len(dump_0)): - obj_0 = json.loads(dump_0[i]) - obj_1 = json.loads(dump_1[i]) - recursive_compare(obj_0, obj_1) + @pytest.mark.parametrize("tree_method", ["hist", "approx"]) + def test_model_output(self, tree_method: str) -> None: + run_training_continuation_model_output("cuda", tree_method) diff --git a/tests/python/test_training_continuation.py b/tests/python/test_training_continuation.py index 1798a4d93..9e9f37ea5 100644 --- a/tests/python/test_training_continuation.py +++ b/tests/python/test_training_continuation.py @@ -6,6 +6,7 @@ import pytest import xgboost as xgb from xgboost import testing as tm +from xgboost.testing.continuation import run_training_continuation_model_output rng = np.random.RandomState(1337) @@ -15,54 +16,51 @@ class TestTrainingContinuation: def generate_parameters(self): xgb_params_01_binary = { - 'nthread': 1, + "nthread": 1, } xgb_params_02_binary = { - 'nthread': 1, - 'num_parallel_tree': self.num_parallel_tree + "nthread": 1, + "num_parallel_tree": self.num_parallel_tree, } xgb_params_03_binary = { - 'nthread': 1, - 'num_class': 5, - 'num_parallel_tree': self.num_parallel_tree + "nthread": 1, + "num_class": 5, + "num_parallel_tree": self.num_parallel_tree, } - return [ - xgb_params_01_binary, xgb_params_02_binary, xgb_params_03_binary - ] + return [xgb_params_01_binary, xgb_params_02_binary, xgb_params_03_binary] - def run_training_continuation(self, xgb_params_01, xgb_params_02, - xgb_params_03): + def run_training_continuation(self, xgb_params_01, xgb_params_02, xgb_params_03): from sklearn.datasets import load_digits from sklearn.metrics import mean_squared_error digits_2class = load_digits(n_class=2) digits_5class = load_digits(n_class=5) - X_2class = digits_2class['data'] - y_2class = digits_2class['target'] + X_2class = digits_2class["data"] + y_2class = digits_2class["target"] - X_5class = digits_5class['data'] - y_5class = digits_5class['target'] + X_5class = digits_5class["data"] + y_5class = digits_5class["target"] dtrain_2class = xgb.DMatrix(X_2class, label=y_2class) dtrain_5class = xgb.DMatrix(X_5class, label=y_5class) - gbdt_01 = xgb.train(xgb_params_01, dtrain_2class, - num_boost_round=10) + gbdt_01 = xgb.train(xgb_params_01, dtrain_2class, num_boost_round=10) ntrees_01 = len(gbdt_01.get_dump()) assert ntrees_01 == 10 - gbdt_02 = xgb.train(xgb_params_01, dtrain_2class, - num_boost_round=0) - gbdt_02.save_model('xgb_tc.json') + gbdt_02 = xgb.train(xgb_params_01, dtrain_2class, num_boost_round=0) + gbdt_02.save_model("xgb_tc.json") - gbdt_02a = xgb.train(xgb_params_01, dtrain_2class, - num_boost_round=10, xgb_model=gbdt_02) - gbdt_02b = xgb.train(xgb_params_01, dtrain_2class, - num_boost_round=10, xgb_model="xgb_tc.json") + gbdt_02a = xgb.train( + xgb_params_01, dtrain_2class, num_boost_round=10, xgb_model=gbdt_02 + ) + gbdt_02b = xgb.train( + xgb_params_01, dtrain_2class, num_boost_round=10, xgb_model="xgb_tc.json" + ) ntrees_02a = len(gbdt_02a.get_dump()) ntrees_02b = len(gbdt_02b.get_dump()) assert ntrees_02a == 10 @@ -76,20 +74,21 @@ class TestTrainingContinuation: res2 = mean_squared_error(y_2class, gbdt_02b.predict(dtrain_2class)) assert res1 == res2 - gbdt_03 = xgb.train(xgb_params_01, dtrain_2class, - num_boost_round=3) - gbdt_03.save_model('xgb_tc.json') + gbdt_03 = xgb.train(xgb_params_01, dtrain_2class, num_boost_round=3) + gbdt_03.save_model("xgb_tc.json") - gbdt_03a = xgb.train(xgb_params_01, dtrain_2class, - num_boost_round=7, xgb_model=gbdt_03) - gbdt_03b = xgb.train(xgb_params_01, dtrain_2class, - num_boost_round=7, xgb_model="xgb_tc.json") + gbdt_03a = xgb.train( + xgb_params_01, dtrain_2class, num_boost_round=7, xgb_model=gbdt_03 + ) + gbdt_03b = xgb.train( + xgb_params_01, dtrain_2class, num_boost_round=7, xgb_model="xgb_tc.json" + ) ntrees_03a = len(gbdt_03a.get_dump()) ntrees_03b = len(gbdt_03b.get_dump()) assert ntrees_03a == 10 assert ntrees_03b == 10 - os.remove('xgb_tc.json') + os.remove("xgb_tc.json") res1 = mean_squared_error(y_2class, gbdt_03a.predict(dtrain_2class)) res2 = mean_squared_error(y_2class, gbdt_03b.predict(dtrain_2class)) @@ -113,16 +112,14 @@ class TestTrainingContinuation: y_2class, gbdt_04.predict( dtrain_2class, iteration_range=(0, gbdt_04.num_boosted_rounds()) - ) + ), ) assert res1 == res2 - gbdt_05 = xgb.train(xgb_params_03, dtrain_5class, - num_boost_round=7) - gbdt_05 = xgb.train(xgb_params_03, - dtrain_5class, - num_boost_round=3, - xgb_model=gbdt_05) + gbdt_05 = xgb.train(xgb_params_03, dtrain_5class, num_boost_round=7) + gbdt_05 = xgb.train( + xgb_params_03, dtrain_5class, num_boost_round=3, xgb_model=gbdt_05 + ) res1 = gbdt_05.predict(dtrain_5class) res2 = gbdt_05.predict( @@ -163,3 +160,7 @@ class TestTrainingContinuation: clf.set_params(eval_metric="error") clf.fit(X, y, eval_set=[(X, y)], xgb_model=loaded) assert tm.non_increasing(clf.evals_result()["validation_0"]["error"]) + + @pytest.mark.parametrize("tree_method", ["hist", "approx", "exact"]) + def test_model_output(self, tree_method: str) -> None: + run_training_continuation_model_output("cpu", tree_method) From 069cf1d019de82cb25d016874378ec5db4456ee5 Mon Sep 17 00:00:00 2001 From: Hui Liu <96135754+hliuca@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:30:01 -0800 Subject: [PATCH 109/109] use __HIPCC__ for device code --- CMakeLists.txt | 2 +- include/xgboost/base.h | 8 ++++---- include/xgboost/host_device_vector.h | 4 ++-- include/xgboost/linalg.h | 10 +++++----- include/xgboost/span.h | 6 +++--- src/collective/nccl_stub.h | 4 ---- src/common/bitfield.h | 20 +++++++++---------- src/common/common.h | 4 ++-- src/common/compressed_iterator.h | 8 ++++---- src/common/math.h | 10 +++++----- src/common/survival_util.h | 4 ++-- src/common/transform.h | 12 +++++------ src/data/array_interface.h | 14 ++++++------- src/data/ellpack_page.cu | 2 +- src/data/validation.h | 2 +- src/tree/split_evaluator.h | 2 +- tests/cpp/common/test_hist_util.h | 6 +++--- tests/cpp/common/test_span.h | 2 +- tests/cpp/common/test_transform_range.cc | 4 ++-- tests/cpp/helpers.h | 6 +++--- tests/cpp/histogram_helpers.h | 4 ++-- tests/cpp/metric/test_rank_metric.cc | 2 +- .../cpp/objective/test_regression_obj_cpu.cc | 4 ++-- 23 files changed, 68 insertions(+), 72 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 58b9b8fb8..d828d2767 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -258,7 +258,7 @@ if (USE_HIP) set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -I${HIP_INCLUDE_DIRS}") set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -Wunused-result -w") - set(CMAKE_HIP_FLAGS "${CMAKE_HIP_FLAGS} -D__HIP_PLATFORM_AMD__") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__HIP_PLATFORM_AMD__") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${HIP_INCLUDE_DIRS}") add_subdirectory(${PROJECT_SOURCE_DIR}/rocgputreeshap) endif (USE_HIP) diff --git a/include/xgboost/base.h b/include/xgboost/base.h index 3bc79c2d8..1c4b6568e 100644 --- a/include/xgboost/base.h +++ b/include/xgboost/base.h @@ -58,19 +58,19 @@ /*! * \brief Tag function as usable by device */ -#if defined (__CUDA__) || defined(__NVCC__) || defined(__HIP_PLATFORM_AMD__) +#if defined (__CUDA__) || defined(__NVCC__) || defined(__HIPCC__) #define XGBOOST_DEVICE __host__ __device__ #else #define XGBOOST_DEVICE -#endif // defined (__CUDA__) || defined(__NVCC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined (__CUDA__) || defined(__NVCC__) || defined(__HIPCC__) -#if defined(__CUDA__) || defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA__) || defined(__CUDACC__) || defined(__HIPCC__) #define XGBOOST_HOST_DEV_INLINE XGBOOST_DEVICE __forceinline__ #define XGBOOST_DEV_INLINE __device__ __forceinline__ #else #define XGBOOST_HOST_DEV_INLINE #define XGBOOST_DEV_INLINE -#endif // defined(__CUDA__) || defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDA__) || defined(__CUDACC__) || defined(__HIPCC__) // These check are for Makefile. #if !defined(XGBOOST_MM_PREFETCH_PRESENT) && !defined(XGBOOST_BUILTIN_PREFETCH_PRESENT) diff --git a/include/xgboost/host_device_vector.h b/include/xgboost/host_device_vector.h index eb4b004dd..e70c8e910 100644 --- a/include/xgboost/host_device_vector.h +++ b/include/xgboost/host_device_vector.h @@ -58,11 +58,11 @@ namespace xgboost { -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) // Sets a function to call instead of cudaSetDevice(); // only added for testing void SetCudaSetDeviceHandler(void (*handler)(int)); -#endif // __CUDACC__ || __HIP_PLATFORM_AMD__ +#endif // __CUDACC__ || __HIPCC__ template struct HostDeviceVectorImpl; diff --git a/include/xgboost/linalg.h b/include/xgboost/linalg.h index 41a43ac84..26a072e52 100644 --- a/include/xgboost/linalg.h +++ b/include/xgboost/linalg.h @@ -30,11 +30,11 @@ // decouple it from xgboost. #ifndef LINALG_HD -#if defined(__CUDA__) || defined(__NVCC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA__) || defined(__NVCC__) || defined(__HIPCC__) #define LINALG_HD __host__ __device__ #else #define LINALG_HD -#endif // defined (__CUDA__) || defined(__NVCC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined (__CUDA__) || defined(__NVCC__) || defined(__HIPCC__) #endif // LINALG_HD namespace xgboost::linalg { @@ -118,7 +118,7 @@ using IndexToTag = std::conditional_t>::value, template LINALG_HD constexpr auto UnrollLoop(Fn fn) { -#if defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA_ARCH__) || defined(__HIPCC__) #pragma unroll n #endif // defined __CUDA_ARCH__ for (int32_t i = 0; i < n; ++i) { @@ -136,7 +136,7 @@ int32_t NativePopc(T v) { inline LINALG_HD int Popc(uint32_t v) { #if defined(__CUDA_ARCH__) return __popc(v); -#elif defined(__GNUC__) || defined(__clang__) || defined(__HIP_PLATFORM_AMD__) +#elif defined(__GNUC__) || defined(__clang__) || defined(__HIPCC__) return __builtin_popcount(v); #elif defined(_MSC_VER) return __popcnt(v); @@ -148,7 +148,7 @@ inline LINALG_HD int Popc(uint32_t v) { inline LINALG_HD int Popc(uint64_t v) { #if defined(__CUDA_ARCH__) return __popcll(v); -#elif defined(__GNUC__) || defined(__clang__) || defined(__HIP_PLATFORM_AMD__) +#elif defined(__GNUC__) || defined(__clang__) || defined(__HIPCC__) return __builtin_popcountll(v); #elif defined(_MSC_VER) && defined(_M_X64) return __popcnt64(v); diff --git a/include/xgboost/span.h b/include/xgboost/span.h index 6f2fabba1..b0c1a5c1e 100644 --- a/include/xgboost/span.h +++ b/include/xgboost/span.h @@ -41,7 +41,7 @@ #if defined(__CUDACC__) #include -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) #include #endif @@ -106,7 +106,7 @@ namespace common { #define SPAN_CHECK KERNEL_CHECK -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) // Usual logging facility is not available inside device code. #if defined(_MSC_VER) @@ -157,7 +157,7 @@ namespace common { #endif // defined(XGBOOST_STRICT_R_MODE) -#endif // __CUDA_ARCH__ || __HIP_PLATFORM_AMD__ +#endif // __CUDA_ARCH__ || __HIPCC__ #define SPAN_LT(lhs, rhs) SPAN_CHECK((lhs) < (rhs)) diff --git a/src/collective/nccl_stub.h b/src/collective/nccl_stub.h index 60388ac9e..159cfb00a 100644 --- a/src/collective/nccl_stub.h +++ b/src/collective/nccl_stub.h @@ -10,10 +10,6 @@ #elif defined(XGBOOST_USE_RCCL) #include "../common/cuda_to_hip.h" -#ifndef __HIP_PLATFORM_AMD__ -#define __HIP_PLATFORM_AMD__ -#endif - #ifndef THRUST_DEVICE_SYSTEM #define THRUST_DEVICE_SYSTEM THRUST_DEVICE_SYSTEM_HIP #endif diff --git a/src/common/bitfield.h b/src/common/bitfield.h index 30063fb6f..adc671fee 100644 --- a/src/common/bitfield.h +++ b/src/common/bitfield.h @@ -16,18 +16,18 @@ #include #include "device_helpers.cuh" -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) #include #include #include "device_helpers.hip.h" -#endif // defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDACC__) || defined(__HIPCC__) #include "common.h" #include "xgboost/span.h" // for Span namespace xgboost { -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) using BitFieldAtomicType = unsigned long long; // NOLINT __forceinline__ __device__ BitFieldAtomicType AtomicOr(BitFieldAtomicType* address, @@ -51,7 +51,7 @@ __forceinline__ __device__ BitFieldAtomicType AtomicAnd(BitFieldAtomicType* addr return old; } -#endif // defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDACC__) || defined(__HIPCC__) /** * @brief A non-owning type with auxiliary methods defined for manipulating bits. @@ -109,7 +109,7 @@ struct BitFieldContainer { XGBOOST_DEVICE static size_t ComputeStorageSize(index_type size) { return common::DivRoundUp(size, kValueSize); } -#if defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA_ARCH__) || defined(__HIPCC__) __device__ BitFieldContainer& operator|=(BitFieldContainer const& rhs) { auto tid = blockIdx.x * blockDim.x + threadIdx.x; size_t min_size = min(NumValues(), rhs.NumValues()); @@ -126,9 +126,9 @@ struct BitFieldContainer { } return *this; } -#endif // #if defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#endif // #if defined(__CUDA_ARCH__) || defined(__HIPCC__) -#if defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA_ARCH__) || defined(__HIPCC__) __device__ BitFieldContainer& operator&=(BitFieldContainer const& rhs) { size_t min_size = min(NumValues(), rhs.NumValues()); auto tid = blockIdx.x * blockDim.x + threadIdx.x; @@ -147,7 +147,7 @@ struct BitFieldContainer { } #endif // defined(__CUDA_ARCH__) -#if defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA_ARCH__) || defined(__HIPCC__) __device__ auto Set(index_type pos) noexcept(true) { Pos pos_v = Direction::Shift(ToBitPos(pos)); value_type& value = Data()[pos_v.int_pos]; @@ -164,7 +164,7 @@ struct BitFieldContainer { } /* compiler hack */ -#if defined(__HIP_PLATFORM_AMD__) +#if defined(__HIPCC__) void Clear(index_type pos) noexcept(true) { Pos pos_v = Direction::Shift(ToBitPos(pos)); value_type& value = Data()[pos_v.int_pos]; @@ -185,7 +185,7 @@ struct BitFieldContainer { value_type clear_bit = ~(kOne << pos_v.bit_pos); value &= clear_bit; } -#endif // defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDA_ARCH__) || defined(__HIPCC__) XGBOOST_DEVICE bool Check(Pos pos_v) const noexcept(true) { pos_v = Direction::Shift(pos_v); diff --git a/src/common/common.h b/src/common/common.h index 19bb7bc1c..051862e01 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -25,7 +25,7 @@ #define WITH_CUDA() true -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) #include "cuda_to_hip.h" #include #include @@ -39,7 +39,7 @@ #endif // defined(__CUDACC__) namespace dh { -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) /* * Error handling functions */ diff --git a/src/common/compressed_iterator.h b/src/common/compressed_iterator.h index eee08c488..abdf20266 100644 --- a/src/common/compressed_iterator.h +++ b/src/common/compressed_iterator.h @@ -13,9 +13,9 @@ #if defined(__CUDACC__) #include "device_helpers.cuh" -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) #include "device_helpers.hip.h" -#endif // __CUDACC__ || __HIP_PLATFORM_AMD__ +#endif // __CUDACC__ || __HIPCC__ namespace xgboost { namespace common { @@ -107,7 +107,7 @@ class CompressedBufferWriter { } } -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) __device__ void AtomicWriteSymbol (CompressedByteT* buffer, uint64_t symbol, size_t offset) { size_t ibit_start = offset * symbol_bits_; @@ -121,7 +121,7 @@ class CompressedBufferWriter { symbol >>= 8; } } -#endif // __CUDACC__ || __HIP_PLATFORM_AMD__ +#endif // __CUDACC__ || __HIPCC__ template void Write(CompressedByteT *buffer, IterT input_begin, IterT input_end) { diff --git a/src/common/math.h b/src/common/math.h index e62d2cbf6..8dc7966a5 100644 --- a/src/common/math.h +++ b/src/common/math.h @@ -143,7 +143,7 @@ CheckNAN(T) { return false; } -#if XGBOOST_STRICT_R_MODE && !defined(__CUDA_ARCH__) && !defined(__HIP_PLATFORM_AMD__) +#if XGBOOST_STRICT_R_MODE && !defined(__CUDA_ARCH__) && !defined(__HIPCC__) bool CheckNAN(double v); @@ -152,21 +152,21 @@ bool CheckNAN(double v); XGBOOST_DEVICE bool inline CheckNAN(float x) { #if defined(__CUDA_ARCH__) return isnan(x); -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) return __builtin_isnan(x); #else return std::isnan(x); -#endif // defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDA_ARCH__) || defined(__HIPCC__) } XGBOOST_DEVICE bool inline CheckNAN(double x) { #if defined(__CUDA_ARCH__) return isnan(x); -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) return __builtin_isnan(x); #else return std::isnan(x); -#endif // defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDA_ARCH__) || defined(__HIPCC__) } #endif // XGBOOST_STRICT_R_MODE && !defined(__CUDA_ARCH__) diff --git a/src/common/survival_util.h b/src/common/survival_util.h index c5f134fc1..545b951ef 100644 --- a/src/common/survival_util.h +++ b/src/common/survival_util.h @@ -25,12 +25,12 @@ DECLARE_FIELD_ENUM_CLASS(xgboost::common::ProbabilityDistributionType); namespace xgboost { namespace common { -#if !defined(__CUDACC__) && !defined(__HIP_PLATFORM_AMD__) +#if !defined(__CUDACC__) && !defined(__HIPCC__) using std::log; using std::fmax; -#endif // __CUDACC__ && __HIP_PLATFORM_AMD__ +#endif // __CUDACC__ && __HIPCC__ enum class CensoringType : uint8_t { kUncensored, kRightCensored, kLeftCensored, kIntervalCensored diff --git a/src/common/transform.h b/src/common/transform.h index 0457e26f3..56f832fbd 100644 --- a/src/common/transform.h +++ b/src/common/transform.h @@ -19,9 +19,9 @@ #if defined (__CUDACC__) #include "device_helpers.cuh" -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) #include "device_helpers.hip.h" -#endif // defined (__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined (__CUDACC__) || defined(__HIPCC__) namespace xgboost { namespace common { @@ -30,7 +30,7 @@ constexpr size_t kBlockThreads = 256; namespace detail { -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) template __global__ void LaunchCUDAKernel(Functor _func, Range _range, SpanType... _spans) { @@ -38,7 +38,7 @@ __global__ void LaunchCUDAKernel(Functor _func, Range _range, _func(i, _spans...); } } -#endif // defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDACC__) || defined(__HIPCC__) } // namespace detail @@ -129,7 +129,7 @@ class Transform { UnpackShard(device, _vectors...); } -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) template ::type* = nullptr, typename... HDV> void LaunchCUDA(Functor _func, HDV*... _vectors) const { @@ -161,7 +161,7 @@ class Transform { LOG(FATAL) << "Not part of device code. WITH_CUDA: " << WITH_CUDA(); } -#endif // defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#endif // defined(__CUDACC__) || defined(__HIPCC__) template void LaunchCPU(Functor func, HDV *...vectors) const { diff --git a/src/data/array_interface.h b/src/data/array_interface.h index 7ef2d3871..d9e8bc802 100644 --- a/src/data/array_interface.h +++ b/src/data/array_interface.h @@ -28,7 +28,7 @@ #if defined(XGBOOST_USE_CUDA) #include "cuda_fp16.h" -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(XGBOOST_USE_HIP) #include #endif @@ -323,7 +323,7 @@ class ArrayInterfaceHandler { template struct ToDType; // float -#if defined(XGBOOST_USE_CUDA) || defined(__HIP_PLATFORM_AMD__) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) template <> struct ToDType<__half> { static constexpr ArrayInterfaceHandler::Type kType = ArrayInterfaceHandler::kF2; @@ -473,7 +473,7 @@ class ArrayInterface { CHECK(sizeof(long double) == 16) << error::NoF128(); type = T::kF16; } else if (typestr[1] == 'f' && typestr[2] == '2') { -#if defined(XGBOOST_USE_CUDA) || defined(__HIP_PLATFORM_AMD__) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) type = T::kF2; #else LOG(FATAL) << "Half type is not supported."; @@ -512,7 +512,7 @@ class ArrayInterface { using T = ArrayInterfaceHandler::Type; switch (type) { case T::kF2: { -#if defined(XGBOOST_USE_CUDA) || defined(__HIP_PLATFORM_AMD__) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) return func(reinterpret_cast<__half const *>(data)); #endif // defined(XGBOOST_USE_CUDA) } @@ -520,7 +520,7 @@ class ArrayInterface { return func(reinterpret_cast(data)); case T::kF8: return func(reinterpret_cast(data)); -#if defined(__CUDA_ARCH__ ) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA_ARCH__ ) || defined(__HIPCC__) case T::kF16: { // CUDA device code doesn't support long double. SPAN_CHECK(false); @@ -567,7 +567,7 @@ class ArrayInterface { static_assert(sizeof...(index) <= D, "Invalid index."); return this->DispatchCall([=](auto const *p_values) -> T { std::size_t offset = linalg::detail::Offset<0ul>(strides, 0ul, index...); -#if defined(XGBOOST_USE_CUDA) || defined(__HIP_PLATFORM_AMD__) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) // No operator defined for half -> size_t using Type = std::conditional_t< std::is_same<__half, @@ -601,7 +601,7 @@ template auto DispatchDType(ArrayInterfaceHandler::Type dtype, Fn dispatch) { switch (dtype) { case ArrayInterfaceHandler::kF2: { -#if defined(XGBOOST_USE_CUDA) || defined(__HIP_PLATFORM_AMD__) +#if defined(XGBOOST_USE_CUDA) || defined(XGBOOST_USE_HIP) return dispatch(__half{}); #else LOG(FATAL) << "half type is only supported for CUDA input."; diff --git a/src/data/ellpack_page.cu b/src/data/ellpack_page.cu index c0f91380b..0b35670be 100644 --- a/src/data/ellpack_page.cu +++ b/src/data/ellpack_page.cu @@ -281,7 +281,7 @@ void CopyDataToEllpack(const AdapterBatchT& batch, common::Span()); diff --git a/src/data/validation.h b/src/data/validation.h index 914a2d740..e73a1e887 100644 --- a/src/data/validation.h +++ b/src/data/validation.h @@ -13,7 +13,7 @@ namespace xgboost { namespace data { struct LabelsCheck { XGBOOST_DEVICE bool operator()(float y) { -#if defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA_ARCH__) || defined(__HIPCC__) return ::isnan(y) || ::isinf(y); #else return std::isnan(y) || std::isinf(y); diff --git a/src/tree/split_evaluator.h b/src/tree/split_evaluator.h index 13085d1a0..10a994ac2 100644 --- a/src/tree/split_evaluator.h +++ b/src/tree/split_evaluator.h @@ -124,7 +124,7 @@ class TreeEvaluator { [[nodiscard]] XGBOOST_DEVICE float Divide(float a, float b) const { #ifdef __CUDA_ARCH__ return __fdividef(a, b); -#elif defined(__HIP_PLATFORM_AMD__) +#elif defined(__HIPCC__) return a / b; #else return a / b; diff --git a/tests/cpp/common/test_hist_util.h b/tests/cpp/common/test_hist_util.h index d31df0811..11bc30a6a 100644 --- a/tests/cpp/common/test_hist_util.h +++ b/tests/cpp/common/test_hist_util.h @@ -15,10 +15,10 @@ #include "../filesystem.h" // dmlc::TemporaryDirectory #include "../helpers.h" -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) #include #include "../../../src/data/device_adapter.cuh" -#endif // __CUDACC__, __HIP_PLATFORM_AMD__ +#endif // __CUDACC__, __HIPCC__ // Some helper functions used to test both GPU and CPU algorithms // @@ -47,7 +47,7 @@ inline std::vector GenerateRandomWeights(int num_rows) { return w; } -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) inline data::CupyAdapter AdapterFromData(const thrust::device_vector &x, int num_rows, int num_columns) { Json array_interface{Object()}; diff --git a/tests/cpp/common/test_span.h b/tests/cpp/common/test_span.h index a53d4300d..72555c486 100644 --- a/tests/cpp/common/test_span.h +++ b/tests/cpp/common/test_span.h @@ -99,7 +99,7 @@ struct TestRBeginREnd { Span s (arr); -#if defined(__CUDA_ARCH__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDA_ARCH__) || defined(__HIPCC__) auto rbeg = dh::trbegin(s); auto rend = dh::trend(s); #else diff --git a/tests/cpp/common/test_transform_range.cc b/tests/cpp/common/test_transform_range.cc index af130830b..0b14bdc8f 100644 --- a/tests/cpp/common/test_transform_range.cc +++ b/tests/cpp/common/test_transform_range.cc @@ -14,7 +14,7 @@ namespace xgboost::common { namespace { constexpr DeviceOrd TransformDevice() { -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) return DeviceOrd::CUDA(0); #else return DeviceOrd::CPU(); @@ -51,7 +51,7 @@ TEST(Transform, DeclareUnifiedTest(Basic)) { ASSERT_TRUE(std::equal(h_sol.begin(), h_sol.end(), res.begin())); } -#if !defined(__CUDACC__) && !defined(__HIP_PLATFORM_AMD__) +#if !defined(__CUDACC__) && !defined(__HIPCC__) TEST(TransformDeathTest, Exception) { size_t const kSize{16}; std::vector h_in(kSize); diff --git a/tests/cpp/helpers.h b/tests/cpp/helpers.h index 124104334..95260b991 100644 --- a/tests/cpp/helpers.h +++ b/tests/cpp/helpers.h @@ -28,19 +28,19 @@ #include "filesystem.h" // dmlc::TemporaryDirectory #include "xgboost/linalg.h" -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) #define DeclareUnifiedTest(name) GPU ## name #else #define DeclareUnifiedTest(name) name #endif -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) #define GPUIDX (common::AllVisibleGPUs() == 1 ? 0 : collective::GetRank()) #else #define GPUIDX (-1) #endif -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) #define DeclareUnifiedDistributedTest(name) MGPU ## name #else #define DeclareUnifiedDistributedTest(name) name diff --git a/tests/cpp/histogram_helpers.h b/tests/cpp/histogram_helpers.h index d09a1dce6..e5d603b42 100644 --- a/tests/cpp/histogram_helpers.h +++ b/tests/cpp/histogram_helpers.h @@ -3,7 +3,7 @@ */ #pragma once -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) #include "../../src/data/ellpack_page.cuh" #endif @@ -12,7 +12,7 @@ #include "./helpers.h" // for RandomDataGenerator namespace xgboost { -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) namespace { class HistogramCutsWrapper : public common::HistogramCuts { public: diff --git a/tests/cpp/metric/test_rank_metric.cc b/tests/cpp/metric/test_rank_metric.cc index 74eb2ea3e..9421b78bd 100644 --- a/tests/cpp/metric/test_rank_metric.cc +++ b/tests/cpp/metric/test_rank_metric.cc @@ -20,7 +20,7 @@ namespace xgboost { namespace metric { -#if !defined(__CUDACC__) && !defined(__HIP_PLATFORM_AMD__) +#if !defined(__CUDACC__) && !defined(__HIPCC__) TEST(Metric, AMS) { auto ctx = MakeCUDACtx(GPUIDX); EXPECT_ANY_THROW(Metric::Create("ams", &ctx)); diff --git a/tests/cpp/objective/test_regression_obj_cpu.cc b/tests/cpp/objective/test_regression_obj_cpu.cc index afc8cbb73..4e9c0e3c0 100644 --- a/tests/cpp/objective/test_regression_obj_cpu.cc +++ b/tests/cpp/objective/test_regression_obj_cpu.cc @@ -193,7 +193,7 @@ TEST(Objective, DeclareUnifiedTest(TweedieRegressionGPair)) { ASSERT_EQ(obj->DefaultEvalMetric(), std::string{"tweedie-nloglik@1.1"}); } -#if defined(__CUDACC__) || defined(__HIP_PLATFORM_AMD__) +#if defined(__CUDACC__) || defined(__HIPCC__) TEST(Objective, CPU_vs_CUDA) { Context ctx = MakeCUDACtx(GPUIDX); @@ -271,7 +271,7 @@ TEST(Objective, DeclareUnifiedTest(TweedieRegressionBasic)) { } // CoxRegression not implemented in GPU code, no need for testing. -#if !defined(__CUDACC__) && !defined(__HIP_PLATFORM_AMD__) +#if !defined(__CUDACC__) && !defined(__HIPCC__) TEST(Objective, CoxRegressionGPair) { Context ctx = MakeCUDACtx(GPUIDX); std::vector> args;